1414}
1515
1616
17- def run_model (params : dict , label_overrides : dict = None ):
17+ def run_model (params : dict , label_overrides : dict | None = None ) -> list [dict ]:
18+ """Run TB isolation model and return results as list of dicts per AGENTS.md.
19+
20+ Args:
21+ params: Dictionary of model parameters from YAML/Excel.
22+ label_overrides: Optional scenario label overrides.
23+
24+ Returns:
25+ list[dict]: List of result dictionaries with 'type' and 'data' keys.
26+ """
1827 getcontext ().prec = 28
1928 ONE = Decimal ("1" )
2029 CENT = Decimal ("0.01" )
@@ -40,17 +49,17 @@ def q2n(x: Decimal) -> Decimal:
4049 return x .quantize (CENT , rounding = ROUND_HALF_EVEN )
4150
4251 def getp (default , * names ) -> Decimal :
52+ """Get parameter from params dict, case-insensitive, with fallback."""
4353 normalized_params = {k .lower (): v for k , v in params .items ()}
4454
4555 for n in names :
4656 n_lower = n .lower ()
4757
4858 if n_lower in normalized_params and normalized_params [n_lower ] != "" :
49- return Decimal (str (normalized_params [n_lower ]))
50-
51- for key , val in normalized_params .items ():
52- if f"({ n_lower } )" in key and val != "" :
53- return Decimal (str (val ))
59+ try :
60+ return Decimal (str (normalized_params [n_lower ]))
61+ except (ValueError , TypeError ):
62+ pass
5463 return Decimal (str (default ))
5564
5665 # parameter extraction
@@ -60,25 +69,25 @@ def getp(default, *names) -> Decimal:
6069 workday_ratio = getp (0.714 , "Ratio of workdays to total days" )
6170
6271 # probabilities of progression
63- prob_latent_to_active_2yr = getp (0 , "prob_latent_to_active_2yr" , " First 2 years" )
64- prob_latent_to_active_lifetime = getp (0 , "prob_latent_to_active_lifetime" , " Rest of lifetime" )
72+ prob_latent_to_active_2yr = getp (0.05 , "First 2 years (prob_latent_to_active_2yr)" , "prob_latent_to_active_2yr " )
73+ prob_latent_to_active_lifetime = getp (0.05 , "Rest of lifetime (prob_latent_to_active_lifetime)" , "prob_latent_to_active_lifetime " )
6574
66- # secondary infection costs
67- cost_latent = getp (0 , "cost_latent" , " Cost of latent TB infection" )
68- cost_active = getp (0 , "cost_active" , " Cost of active TB infection" )
75+ # secondary infection costs - FIXED: match YAML key names with proper defaults
76+ cost_latent = getp (300 , "Cost of latent TB infection (cost_latent)" , "cost_latent " )
77+ cost_active = getp (34523 , "Cost of active TB infection (cost_active)" , "cost_active " )
6978
7079 # isolation scenario parameters
71- isolation_type = int (getp (3 , "isolation_type" , " Isolation type (1=hospital,2=motel,3=home)" ))
72- daily_hosp_cost = getp (0 , "isolation_cost" , " Daily isolation cost" )
73- direct_med_cost_day = getp (0 , "Direct medical cost of a day of isolation" ) # Often used for hospital stay
80+ isolation_type = int (getp (3 , "Isolation type (1=hospital,2=motel,3=home)" , "isolation_type " ))
81+ daily_hosp_cost = getp (85 , "Daily isolation cost (isolation_cost)" , "isolation_cost " )
82+ direct_med_cost_day = getp (3996 , "Direct medical cost of a day of isolation" )
7483
75- cost_motel_room = getp (0 , "Cost of motel room per day" )
76- hourly_wage_nurse = getp (0 , "Hourly wage for nurse" )
77- time_nurse_checkin = getp (0 , "Time for nurse to check in w/ pt in motel or home (hrs)" )
78- hourly_wage_worker = getp (0 , "Hourly wage for worker" )
84+ cost_motel_room = getp (150 , "Cost of motel room per day" )
85+ hourly_wage_nurse = getp (42.42 , "Hourly wage for nurse" )
86+ time_nurse_checkin = getp (2 , "Time for nurse to check in w/ pt in motel or home (hrs)" )
87+ hourly_wage_worker = getp (29.36 , "Hourly wage for worker" )
7988
80- discount_rate = getp (0 , "discount_rate" , "Discount rate" )
81- remaining_years = int (getp (40 , "remaining_years" , " Remaining years of life" ))
89+ discount_rate = getp (0.03 , "Discount rate" )
90+ remaining_years = int (getp (40 , "Remaining years of life" ))
8291
8392 # determine daily isolation cost based on isolation type
8493 if isolation_type == 1 :
@@ -147,14 +156,33 @@ def getp(default, *names) -> Decimal:
147156 lbl_5 : [direct_cost_5_day , productivity_loss_5_day , secondary_cost_5_day , total_5_day ],
148157 })
149158
150- return {
151- "df_infections" : df_infections ,
152- "df_costs" : df_costs ,
153- }
154-
155-
156- def build_sections (results ):
159+ # Return list[dict] per AGENTS.md function contract
157160 return [
158- {"title " : "Number of Secondary Infections " , "content " : [ results [ " df_infections" ]] },
159- {"title " : "Costs " , "content " : [ results [ " df_costs" ]] },
161+ {"type " : "infections " , "data " : df_infections },
162+ {"type " : "costs " , "data " : df_costs },
160163 ]
164+
165+
166+ def build_sections (results : list [dict ], label_overrides : dict | None = None ) -> list [dict ]:
167+ """Build sections from model results per AGENTS.md.
168+
169+ Args:
170+ results: List of result dicts from run_model().
171+ label_overrides: Optional label overrides (unused but present for compatibility).
172+
173+ Returns:
174+ list[dict]: List of section dicts for render_sections().
175+ """
176+ sections = []
177+ for item in results :
178+ if item ["type" ] == "infections" :
179+ sections .append ({
180+ "title" : "Number of Secondary Infections" ,
181+ "content" : [item ["data" ]]
182+ })
183+ elif item ["type" ] == "costs" :
184+ sections .append ({
185+ "title" : "Costs" ,
186+ "content" : [item ["data" ]]
187+ })
188+ return sections
0 commit comments