diff --git a/resources/ResourceFile_HIV/parameters.csv b/resources/ResourceFile_HIV/parameters.csv index 835dfa942a..97b58be577 100644 --- a/resources/ResourceFile_HIV/parameters.csv +++ b/resources/ResourceFile_HIV/parameters.csv @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c662f8cd5d5242cc9d73abce71d781adc514271bd73bec7587465fccc8ba2a10 -size 3360 +oid sha256:0171ff89893441ab88e713e9b77d0f81ef4b6602a9538bc61a1153e726c9c72b +size 8626 diff --git a/resources/ResourceFile_HIV/scaleup_parameters.csv b/resources/ResourceFile_HIV/scaleup_parameters.csv index 1e3bcd9fa2..e0922bd010 100644 --- a/resources/ResourceFile_HIV/scaleup_parameters.csv +++ b/resources/ResourceFile_HIV/scaleup_parameters.csv @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:31392b914573011c37159f09947d088e8f316cf6dc55141547c8a0a538da8f18 -size 464 +oid sha256:f33aeada28bb68e7b2927659f5daef4920d77965c30243b9032d10d98d28acd2 +size 709 diff --git a/src/tlo/methods/hiv.py b/src/tlo/methods/hiv.py index 8b40e37a34..2c1da26e57 100644 --- a/src/tlo/methods/hiv.py +++ b/src/tlo/methods/hiv.py @@ -204,6 +204,10 @@ def __init__(self, name=None, run_with_checks=False): "Proportion reduction in risk of HIV acquisition if on PrEP. 0 for no efficacy; 1.0 for perfect efficacy.", ), # Natural history - survival (adults) + "min_months_between_aids_and_death": Parameter( + Types.INT, + "Minimum number of months for the time between AIDS and AIDS Death", + ), "mean_months_between_aids_and_death": Parameter( Types.REAL, "Mean number of months (distributed exponentially) for the time between AIDS and AIDS Death", @@ -393,14 +397,22 @@ def __init__(self, name=None, run_with_checks=False): "number of repeat visits assumed for healthcare services", ), "dispensation_period_months": Parameter( - Types.REAL, + Types.INT, "length of prescription for ARVs in months, same for all PLHIV", ), + "dispensation_min_days": Parameter( + Types.REAL, + "Minimum days to attain ARV dispensation", + ), "length_of_inpatient_stay_if_terminal": Parameter( Types.LIST, "length in days of inpatient stay for end-of-life HIV patients: list has two elements [low-bound-inclusive," " high-bound-exclusive]", ), + "aids_death_beddays_default": Parameter( + Types.INT, + "Default number of bed days for end-of-life AIDS care" + ), # ------------------ scale-up parameters for scenario analysis ------------------ # "type_of_scaleup": Parameter( Types.STRING, "argument to determine type scale-up of program which will be implemented, " @@ -418,6 +430,208 @@ def __init__(self, name=None, run_with_checks=False): Types.REAL, " the interval for viral load monitoring in months" ), + # ------------------ Module design parameters ------------------ # + "hiv_regular_polling_event_frequency_months": Parameter( + Types.INT, + "Frequency in months for HivRegularPollingEvent" + ), + "regular_polling_event_initialisation_delay_days": Parameter( + Types.INT, + "Delay in days for initial HivRegularPollingEvent" + ), + "hiv_check_properties_event_delay_months": Parameter( + Types.INT, + "Delay in months for HivCheckPropertiesEvent, which checks configuration of all properties" + ), + # ------------------ Time periods and data availability ------------------ # + "baseline_year": Parameter( + Types.INT, + "Baseline year at which simulation starts" + ), + "end_year_circumcision_data": Parameter( + Types.INT, + "End year for circumcision data availability" + ), + "end_year_treatment_data": Parameter( + Types.INT, + "End year for treatment data availability" + ), + "end_year_testing_data": Parameter( + Types.INT, + "End year for HIV testing data availability" + ), + "years_since_infection_threshold_for_higher_art_probability": Parameter( + Types.INT, + "Minimum years since HIV infection required to assign higher ART probability at baseline" + ), + "years_offset_for_dummy_test_date_at_baseline": Parameter( + Types.INT, + "Years prior to simulation start date to assign test date at baseline; " + "Required for logical consistency so that all persons on ART have been tested and diagnosed, " + ), + # ------------------ Age categorization parameters ------------------ # + "age_threshold_child_adult_distinction": Parameter( + Types.INT, + "Age threshold for child/adult distinction" + ), + "age_max_adult": Parameter( + Types.INT, + "Maximum age for adult categorization" + ), + "age_max_infant": Parameter( + Types.INT, + "Maximum age for infant categorization" + ), + "age_default_adult": Parameter( + Types.INT, + "Default age for adult viral suppression calculations" + ), + "age_default_children": Parameter( + Types.INT, + "Default age for children viral suppression calculations" + ), + "age_min_agyw": Parameter( + Types.INT, + "Minimum age for adolescent girls and young women (AGYW) category" + ), + "age_max_agyw": Parameter( + Types.INT, + "Maximum age for adolescent girls and young women (AGYW) category" + ), + "age_max_prep": Parameter( + Types.INT, + "Maximum age for PrEP eligibility" + ), + "age_max_hiv_early_infant_test_years": Parameter( + Types.REAL, + "Maximum age in years for early infant HIV test eligibility" + ), + "age_max_prophylaxis_years": Parameter( + Types.REAL, + "Maximum age in years for infant HIV prophylaxis" + ), + # ------------------ Disease progression ------------------ # + "hiv_infection_to_aids_untreated_threshold_years": Parameter( + Types.INT, + "Number of years after HIV infection at which a person is assumed to have AIDS " + "if not virally suppressed on ART" + ), + "prop_aids_death_is_tb_coinfection": Parameter( + Types.REAL, + "Proportion of AIDS deaths that have TB co-infection" + ), + # ------------------ Testing parameters ------------------ # + "hiv_early_infant_test_sensitivity": Parameter( + Types.REAL, + "Sensitivity of early infant HIV test" + ), + "hiv_early_infant_test_specificity": Parameter( + Types.REAL, + "Specificity of early infant HIV test" + ), + "hv_min_test_interval_days": Parameter( + Types.INT, + "Minimum interval in days between HIV tests" + ), + "infant_hiv_test_first_weeks": Parameter( + Types.INT, + "Timing of first infant HIV test in weeks after birth in weeks" + ), + "infant_hiv_test_second_months": Parameter( + Types.INT, + "Timing of second infant HIV test in months after birth in months" + ), + "infant_hiv_test_third_months": Parameter( + Types.INT, + "Timing of third infant HIV test in months after birth in months" + ), + "hiv_testing_initial_window_for_individuals_with_aids_at_init_days": Parameter( + Types.INT, + "Initial window in days for HIV testing via HSI event HSI_Test_and_Refer " + "for individuals with AIDS at initialization" + ), + "hiv_testing_close_window_for_individuals_with_aids_at_init_days": Parameter( + Types.INT, + "Close window in days for HIV testing via HSI event HSI_Test_and_Refer" + "for individuals with AIDS at initialization" + ), + # ------------------ Health-seeking behavior ------------------ # + "odds_ratio_health_seeking_aids_symptoms_adults": Parameter( + Types.REAL, + "Odds ratio for health seeking behavior in adults with AIDS symptoms" + ), + "odds_ratio_health_seeking_aids_symptoms_children": Parameter( + Types.REAL, + "Odds ratio for health seeking behavior in children with AIDS symptoms" + ), + # ------------------ PrEP-related parameters ------------------ # + "hiv_prep_refill_appointment_window_days": Parameter( + Types.INT, + "Window in days for PrEP refill appointments, checked if retained on prep every 3 months" + ), + "prep_continuation_reevaluation_period_months": Parameter( + Types.INT, + "Months between PrEP continuation re-evaluation appointments" + ), + "prep_cons_notavailable_retry_days": Parameter( + Types.INT, + "Days to retry PrEP appointment if consumables not available" + ), + # ------------------ ART appointment timing ------------------ # + "art_continuation_appointment_window_days": Parameter( + Types.INT, + "Window in days for ART continuation appointments" + ), + "art_restart_appointment_delay_months": Parameter( + Types.INT, + "Delay in months for scheduling ART restart appointment" + ), + "art_drug_notavailable_default_retry_months": Parameter( + Types.INT, + "Default months to retry ART appointment if drugs not available" + ), + "art_level1a_healthseekingbehaviour_cap": Parameter( + Types.INT, + "Maximum number of retry attempts at level 1a facility before referral" + ), + "art_level1a_appointment_delay_months": Parameter( + Types.INT, + "Delay in months for ART appointment retry at level 1a facility" + ), + "art_level1b_appointment_delay_days": Parameter( + Types.INT, + "Delay in days for ART appointment at level 1b facility after referral" + ), + "seek_further_art_appointment_if_appointment_not_available_min_delay_days": Parameter( + Types.INT, + "Minimum delay in days to seek further ART appointment if appointment not available" + ), + "seek_further_art_appointment_if_appointment_not_available_max_delay_days": Parameter( + Types.INT, + "Maximum delay in days to seek further ART appointment if appointment not available" + ), + # ------------------ Circumcision-related parameters ------------------ # + "hiv_circ_procedure_first_followup_days": Parameter( + Types.INT, + "Days after circumcision procedure for first follow-up appointment" + ), + "hiv_circ_procedure_second_followup_days": Parameter( + Types.INT, + "Days after circumcision procedure for second follow-up appointment" + ), + "hiv_circ_cons_notavailable_retry_procedure_weeks": Parameter( + Types.INT, + "Weeks to retry circumcision procedure if consumables not available" + ), + # ------------------ Infant prophylaxis parameters ------------------ # + "infant_prophylaxis_followup_months": Parameter( + Types.INT, + "Months after starting infant prophylaxis for follow-up appointment" + ), + "infant_prophylaxis_cons_notavailable_retry_days": Parameter( + Types.INT, + "Days to retry infant prophylaxis if consumables not available" + ), } def read_parameters(self, resourcefilepath: Optional[Path] = None): @@ -475,8 +689,8 @@ def read_parameters(self, resourcefilepath: Optional[Path] = None): self.sim.modules["SymptomManager"].register_symptom( Symptom( name="aids_symptoms", - odds_ratio_health_seeking_in_adults=10.0, # High chance of seeking care when aids_symptoms onset - odds_ratio_health_seeking_in_children=10.0, + odds_ratio_health_seeking_in_adults=p["odds_ratio_health_seeking_aids_symptoms_adults"], + odds_ratio_health_seeking_in_children=p["odds_ratio_health_seeking_aids_symptoms_children"], ) ) @@ -659,15 +873,15 @@ def initialise_baseline_prevalence(self, population): params = self.parameters df = population.props - # prob of infection based on age and sex in baseline year (2010: + # prob of infection based on age and sex in baseline year (baseline_year): prevalence_db = params["hiv_prev"] - prev_2010 = prevalence_db.loc[ - prevalence_db.year == 2010, ["age_from", "sex", "prop_hiv_mw2021_v14"] + prev_baseline = prevalence_db.loc[ + prevalence_db.year == params['baseline_year'], ["age_from", "sex", "prop_hiv_mw2021_v14"] ] - prev_2010 = prev_2010.rename(columns={"age_from": "age_years"}) + prev_baseline = prev_baseline.rename(columns={"age_from": "age_years"}) prob_of_infec = df.loc[df.is_alive, ["age_years", "sex"]].merge( - prev_2010, on=["age_years", "sex"], how="left" + prev_baseline, on=["age_years", "sex"], how="left" )["prop_hiv_mw2021_v14"] # probability of being hiv-positive based on risk factors @@ -740,7 +954,7 @@ def initialise_baseline_art(self, population): # 1) Determine who is currently on ART worksheet = self.parameters["art_coverage"] art_data = worksheet.loc[ - worksheet.year == 2010, ["year", "single_age", "sex", "prop_coverage"] + worksheet.year == params['baseline_year'], ["year", "single_age", "sex", "prop_coverage"] ] # merge all susceptible individuals with their coverage probability based on sex and age @@ -754,8 +968,14 @@ def initialise_baseline_art(self, population): # make a series with relative risks of art which depends on >10 years infected (5x higher) rr_art = pd.Series(1, index=df.index) rr_art.loc[ - df.is_alive & (df.hv_date_inf < (self.sim.date - pd.DateOffset(years=10))) - ] = params["rel_probability_art_baseline_aids"] + df.is_alive & ( + df.hv_date_inf < ( + self.sim.date - pd.DateOffset( + years=params["years_since_infection_threshold_for_higher_art_probability"] + ) + ) + ) + ] = params["rel_probability_art_baseline_aids"] # Rescale relative probability of infection so that its average is 1.0 within each age/sex group p = pd.DataFrame( @@ -783,12 +1003,17 @@ def initialise_baseline_art(self, population): # 2) Determine adherence levels for those currently on ART, for each of adult men, adult women and children adult_f_art_idx = df.loc[ - (df.index.isin(art_idx) & (df.sex == "F") & (df.age_years >= 15)) + (df.index.isin(art_idx) & (df.sex == "F") & + (df.age_years >= params["age_threshold_child_adult_distinction"])) ].index adult_m_art_idx = df.loc[ - (df.index.isin(art_idx) & (df.sex == "M") & (df.age_years >= 15)) + (df.index.isin(art_idx) & (df.sex == "M") & + (df.age_years >= params["age_threshold_child_adult_distinction"])) + ].index + child_art_idx = df.loc[ + (df.index.isin(art_idx) & + (df.age_years < params["age_threshold_child_adult_distinction"])) ].index - child_art_idx = df.loc[(df.index.isin(art_idx) & (df.age_years < 15))].index suppr = list() # list of all indices for persons on ART and suppressed notsuppr = list() # list of all indices for persons on ART and not suppressed @@ -799,8 +1024,12 @@ def split_into_vl_and_notvl(all_idx, prob): notsuppr.extend(all_idx[~vl_suppr]) # get expected viral suppression rates by age and year - prob_vs_adult = self.prob_viral_suppression(self.sim.date.year, age_of_person=20) - prob_vs_child = self.prob_viral_suppression(self.sim.date.year, age_of_person=5) + prob_vs_adult = self.prob_viral_suppression( + self.sim.date.year, age_of_person=params["age_default_adult"] + ) + prob_vs_child = self.prob_viral_suppression( + self.sim.date.year, age_of_person=params["age_default_children"] + ) split_into_vl_and_notvl(adult_f_art_idx, prob_vs_adult) split_into_vl_and_notvl(adult_m_art_idx, prob_vs_adult) @@ -814,15 +1043,18 @@ def split_into_vl_and_notvl(all_idx, prob): assert not (df.loc[art_idx, "hv_art"] == "not").any() # for logical consistency, ensure that all persons on ART have been tested and diagnosed - # set last test date prior to 2010 so not counted as a current new test - # don't record individual property hv_number_tests for logging (occurred prior to 2010) - df.loc[art_idx, "hv_last_test_date"] = self.sim.date - pd.DateOffset(years=3) + # set last test date prior to baseline_year so not counted as a current new test + # don't record individual property hv_number_tests for logging (occurred prior to baseline_year) + df.loc[art_idx, "hv_last_test_date"] = self.sim.date - pd.DateOffset( + years=params["years_offset_for_dummy_test_date_at_baseline"] + ) df.loc[art_idx, "hv_diagnosed"] = True # all those on ART need to have event scheduled for continuation/cessation of treatment # this window is 1-90 days (3-monthly prescribing) for person in art_idx: - days = self.rng.randint(low=1, high=self.parameters['dispensation_period_months'] * 30.5, dtype=np.int64) + days = self.rng.randint(low=params['dispensation_min_days'], + high=params['dispensation_period_months'] * 30.5, dtype=np.int64) date_treated = (params['dispensation_period_months'] * 30.5) - days df.at[person, "hv_date_treated"] = self.sim.date - pd.to_timedelta(date_treated, unit="days") @@ -844,7 +1076,7 @@ def initialise_baseline_tested(self, population): # get proportion plhiv who know staus from spectum estimates worksheet = p["treatment_cascade"] testing_data = worksheet.loc[ - worksheet.year == 2010, ["year", "age", "know_status"] + worksheet.year == p['baseline_year'], ["year", "age", "know_status"] ] adult_know_status = testing_data.loc[(testing_data.age == "adults"), "know_status"].values[0] / 100 children_know_status = testing_data.loc[(testing_data.age == "children"), "know_status"].values[0] / 100 @@ -853,11 +1085,11 @@ def initialise_baseline_tested(self, population): # find proportion of adult PLHIV diagnosed (currently on ART) adults_diagnosed = len(df[df.is_alive & df.hv_diagnosed - & (df.age_years >= 15)]) + & (df.age_years >= p["age_threshold_child_adult_distinction"])]) adults_infected = len(df[df.is_alive & df.hv_inf - & (df.age_years >= 15)]) + & (df.age_years >= p["age_threshold_child_adult_distinction"])]) prop_currently_diagnosed = adults_diagnosed / adults_infected if adults_infected > 0 else 0 hiv_test_deficit = adult_know_status - prop_currently_diagnosed @@ -869,7 +1101,7 @@ def initialise_baseline_tested(self, population): adult_undiagnosed = df.loc[df.is_alive & df.hv_inf & ~df.hv_diagnosed - & (df.age_years >= 15)].index + & (df.age_years >= p["age_threshold_child_adult_distinction"])].index adult_test_index = self.rng.choice(adult_undiagnosed, size=number_deficit, replace=False) @@ -877,11 +1109,11 @@ def initialise_baseline_tested(self, population): # find proportion of adult PLHIV diagnosed (currently on ART) children_diagnosed = len(df[df.is_alive & df.hv_diagnosed - & (df.age_years < 15)]) + & (df.age_years < p["age_threshold_child_adult_distinction"])]) children_infected = len(df[df.is_alive & df.hv_inf - & (df.age_years < 15)]) + & (df.age_years < p["age_threshold_child_adult_distinction"])]) prop_currently_diagnosed = children_diagnosed / children_infected if children_infected > 0 else 0 hiv_test_deficit = children_know_status - prop_currently_diagnosed @@ -892,7 +1124,7 @@ def initialise_baseline_tested(self, population): child_undiagnosed = df.loc[df.is_alive & df.hv_inf & ~df.hv_diagnosed - & (df.age_years < 15)].index + & (df.age_years < p["age_threshold_child_adult_distinction"])].index child_test_index = self.rng.choice(child_undiagnosed, size=number_deficit, replace=False) @@ -902,7 +1134,7 @@ def initialise_baseline_tested(self, population): df.loc[df.index.isin(test_index), "hv_diagnosed"] = True # dummy date for date last hiv test (before sim start), otherwise see big spike in testing 01-01-2010 df.loc[test_index, "hv_last_test_date"] = self.sim.date - pd.DateOffset( - years=3 + years=p["years_offset_for_dummy_test_date_at_baseline"] ) def initialise_simulation(self, sim): @@ -920,7 +1152,8 @@ def initialise_simulation(self, sim): # 1) Schedule the Main HIV Regular Polling Event sim.schedule_event( - HivRegularPollingEvent(self), sim.date + DateOffset(days=0) + HivRegularPollingEvent(self), sim.date + + DateOffset(days=p['regular_polling_event_initialisation_delay_days']) ) # 2) Schedule the Logging Event @@ -937,12 +1170,12 @@ def initialise_simulation(self, sim): # Those on ART currently (will not get any further events scheduled): on_art_idx = df.loc[df.is_alive & df.hv_inf & (df.hv_art == "on_VL_suppressed")].index - # Those that lived more than ten years and not currently on ART are assumed to currently have AIDS + # Those that lived more than threshold years and not currently on ART are assumed to currently have AIDS # (will have AIDS Death event scheduled) has_aids_idx = df.loc[ df.is_alive & df.hv_inf - & ((self.sim.date - df.hv_date_inf).dt.days > 10 * 365) + & ((self.sim.date - df.hv_date_inf).dt.days > p["hiv_infection_to_aids_untreated_threshold_years"] * 365) & (df.hv_art != "on_VL_suppressed") ].index @@ -987,22 +1220,31 @@ def initialise_simulation(self, sim): # Schedule the AIDS death events for those who have got AIDS already for person_id in has_aids_idx: # schedule a HSI_Test_and_Refer otherwise initial AIDS rates and deaths are far too high - date_test = self.sim.date + pd.DateOffset(days=self.rng.randint(0, 60)) + date_test = self.sim.date + pd.DateOffset( + days=self.rng.randint(0, p["hiv_testing_initial_window_for_individuals_with_aids_at_init_days"]) + ) self.sim.modules["HealthSystem"].schedule_hsi_event( hsi_event=HSI_Hiv_TestAndRefer( person_id=person_id, module=self, referred_from="initialise_simulation"), priority=1, topen=date_test, - tclose=self.sim.date + pd.DateOffset(days=365), + tclose=self.sim.date + pd.DateOffset( + days=p["hiv_testing_close_window_for_individuals_with_aids_at_init_days"] + ), ) date_aids_death = ( self.sim.date + pd.DateOffset( - months=self.rng.randint(low=0, high=p['mean_months_between_aids_and_death'])) + months=self.rng.randint(low=p['min_months_between_aids_and_death'], + high=p['mean_months_between_aids_and_death'])) ) - # 30% AIDS deaths have TB co-infection - cause_of_death = self.rng.choice(a=["AIDS_non_TB", "AIDS_TB"], size=1, p=[0.7, 0.3]) + # proportion of AIDS deaths that have TB co-infection + cause_of_death = self.rng.choice( + a=["AIDS_non_TB", "AIDS_TB"], + size=1, + p=[1 - p['prop_aids_death_is_tb_coinfection'], p['prop_aids_death_is_tb_coinfection']] + ) sim.schedule_event( HivAidsDeathEvent(person_id=person_id, module=self, cause=cause_of_death), @@ -1029,7 +1271,8 @@ def initialise_simulation(self, sim): # 5) (Optionally) Schedule the event to check the configuration of all properties if self.run_with_checks: sim.schedule_event( - HivCheckPropertiesEvent(self), sim.date + pd.DateOffset(months=1) + HivCheckPropertiesEvent(self), + sim.date + pd.DateOffset(months=p["hiv_check_properties_event_delay_months"]) ) # 6) Store codes for the consumables needed @@ -1105,8 +1348,8 @@ def initialise_simulation(self, sim): self.sim.modules['HealthSystem'].dx_manager.register_dx_test( hiv_early_infant_test=DxTest( property='hv_inf', - sensitivity=1.0, - specificity=1.0, + sensitivity=p["hiv_early_infant_test_sensitivity"], + specificity=p["hiv_early_infant_test_specificity"], item_codes=self.item_codes_for_consumables_required['hiv_early_infant_test'], optional_item_codes=[ self.item_codes_for_consumables_required['blood_tube'], @@ -1120,9 +1363,9 @@ def update_parameters_for_program_scaleup(self): scaled_params_workbook = p["scaleup_parameters"] if p['type_of_scaleup'] == 'target': - scaled_params = scaled_params_workbook.set_index('parameter')['target_value'].to_dict() + scaled_params = scaled_params_workbook.set_index('parameter_name')['value'].to_dict() else: - scaled_params = scaled_params_workbook.set_index('parameter')['max_value'].to_dict() + scaled_params = scaled_params_workbook.set_index('parameter_name')['prior_max'].to_dict() # scale-up HIV program # reduce risk of HIV - applies to whole adult population @@ -1268,7 +1511,7 @@ def on_birth(self, mother_id, child_id): module=self, referred_from='Infant_testing'), priority=1, - topen=self.sim.date + pd.DateOffset(weeks=6), + topen=self.sim.date + pd.DateOffset(weeks=p["infant_hiv_test_first_weeks"]), tclose=None, ) @@ -1280,7 +1523,7 @@ def on_birth(self, mother_id, child_id): module=self, referred_from='Infant_testing'), priority=1, - topen=self.sim.date + pd.DateOffset(months=9), + topen=self.sim.date + pd.DateOffset(months=p["infant_hiv_test_second_months"]), tclose=None, ) @@ -1290,7 +1533,7 @@ def on_birth(self, mother_id, child_id): module=self, referred_from='Infant_testing'), priority=1, - topen=self.sim.date + pd.DateOffset(months=18), + topen=self.sim.date + pd.DateOffset(months=p["infant_hiv_test_third_months"]), tclose=None, ) @@ -1426,6 +1669,7 @@ def do_when_hiv_diagnosed(self, person_id): The person should not yet be on ART. """ df = self.sim.population.props + p = self.parameters if not (df.loc[person_id, "hv_art"] == "not"): logger.warning( @@ -1434,7 +1678,7 @@ def do_when_hiv_diagnosed(self, person_id): ) # Consider if the person will be referred to start ART - if df.loc[person_id, "age_years"] <= 15: + if df.loc[person_id, "age_years"] <= p["age_threshold_child_adult_distinction"]: starts_art = True else: starts_art = self.rng.random_sample() < self.prob_art_start_after_test(self.sim.date.year) @@ -1453,8 +1697,9 @@ def prob_art_start_after_test(self, year): this value for initiation can be higher than the current reported coverage levels to account for defaulters """ - prob_art = self.parameters["prob_start_art_or_vs"] - current_year = year if year <= 2025 else 2025 + p = self.parameters + prob_art = p["prob_start_art_or_vs"] + current_year = year if year <= p["end_year_treatment_data"] else p["end_year_treatment_data"] # use iloc to index by position as index will change by year return_prob = prob_art.loc[ @@ -1470,10 +1715,12 @@ def prob_viral_suppression(self, year, age_of_person): assume constant values 2010-2012 and 2020 on time-series ends at 2025 """ - prob_vs = self.parameters["prob_start_art_or_vs"] - current_year = year if year <= 2025 else 2025 + + p = self.parameters + prob_vs = p["prob_start_art_or_vs"] + current_year = year if year <= p["end_year_treatment_data"] else p["end_year_treatment_data"] age_of_person = age_of_person - age_group = "adults" if age_of_person >= 15 else "children" + age_group = "adults" if age_of_person >= p['age_threshold_child_adult_distinction'] else "children" return_prob = prob_vs.loc[ (prob_vs.year == current_year) & @@ -1579,6 +1826,7 @@ def decide_whether_hiv_test_for_infant(self, mother_id, child_id) -> None: """ df = self.sim.population.props + p = self.parameters mother_id = mother_id child_id = child_id @@ -1593,7 +1841,7 @@ def decide_whether_hiv_test_for_infant(self, mother_id, child_id) -> None: person_id=child_id, module=self, referred_from="newborn_outcomes"), - topen=self.sim.date + pd.DateOffset(weeks=6), + topen=self.sim.date + pd.DateOffset(weeks=p["infant_hiv_test_first_weeks"]), tclose=None, priority=0 ) @@ -1691,8 +1939,8 @@ class HivRegularPollingEvent(RegularEvent, PopulationScopeEventMixin): def __init__(self, module): super().__init__( - module, frequency=DateOffset(months=12) - ) # repeats every 12 months, but this can be changed + module, frequency=DateOffset(months=module.parameters["hiv_regular_polling_event_frequency_months"]) + ) # repeats based on frequency of 'hiv_regular_polling_event_frequency_months', but this can be changed def apply(self, population): @@ -1710,7 +1958,7 @@ def horizontal_transmission(to_sex, from_sex): n_infectious = len( df.loc[ df.is_alive - & df.age_years.between(15, 80) + & df.age_years.between(p['age_threshold_child_adult_distinction'], p['age_max_adult']) & df.hv_inf & (df.hv_art != "on_VL_suppressed") & (df.sex == from_sex) @@ -1723,7 +1971,7 @@ def horizontal_transmission(to_sex, from_sex): susc_idx = df.loc[ df.is_alive & ~df.hv_inf - & df.age_years.between(15, 80) + & df.age_years.between(p['age_threshold_child_adult_distinction'], p['age_max_adult']) & (df.sex == to_sex) ].index n_susceptible = len(susc_idx) @@ -1754,7 +2002,7 @@ def horizontal_transmission(to_sex, from_sex): & ~df.hv_inf & df.li_is_sexworker & ~df.hv_is_on_prep - & df.age_years.between(15, 80) + & df.age_years.between(p['age_threshold_child_adult_distinction'], p['age_max_adult']) ].index # - probability of infection - relative risk applies only to fsw @@ -1790,13 +2038,16 @@ def spontaneous_testing(current_year): # adult testing trends also informed by demographic characteristics # relative probability of testing - this may skew testing rates higher or lower than moh reports - rr_of_test = self.module.lm["lm_spontaneous_test_12m"].predict(df[df.is_alive & (df.age_years >= 15)]) + rr_of_test = self.module.lm["lm_spontaneous_test_12m"].predict( + df[df.is_alive & (df.age_years >= p['age_threshold_child_adult_distinction'])]) mean_prob_test = (rr_of_test * testing_rate_adults).mean() scaled_prob_test = (rr_of_test * testing_rate_adults) / mean_prob_test overall_prob_test = scaled_prob_test * testing_rate_adults - random_draw = rng.random_sample(size=len(df[df.is_alive & (df.age_years >= 15)])) - adult_tests_idx = df.loc[df.is_alive & (df.age_years >= 15) & (random_draw < overall_prob_test)].index + random_draw = rng.random_sample(size=len(df[df.is_alive & + (df.age_years >= p['age_threshold_child_adult_distinction'])])) + adult_tests_idx = df.loc[df.is_alive & (df.age_years >= p['age_threshold_child_adult_distinction']) & + (random_draw < overall_prob_test)].index idx_will_test = adult_tests_idx @@ -1820,7 +2071,7 @@ def prep_for_agyw(): agyw_idx = df.loc[ df.is_alive & ~df.hv_diagnosed - & df.age_years.between(15, 30) + & df.age_years.between(p['age_min_agyw'], p['age_max_agyw']) & (df.sex == "F") & ~df.hv_is_on_prep ].index @@ -1840,7 +2091,7 @@ def prep_for_agyw(): < overall_risk_and_prob_of_prep) & df.is_alive & ~df.hv_diagnosed - & df.age_years.between(15, 30) + & df.age_years.between(p['age_min_agyw'], p['age_max_agyw']) & (df.sex == "F") & ~df.hv_is_on_prep ].index @@ -1865,7 +2116,7 @@ def vmmc_for_child(): df.loc[ df.is_alive & (df.sex == "M") - & (df.age_years < 15) + & (df.age_years < p["age_threshold_child_adult_distinction"]) & (~df.li_is_circ) ], self.module.rng, @@ -1889,10 +2140,10 @@ def vmmc_for_child(): # testing # if year later than 2020, set testing rates to those reported in 2020 - if self.sim.date.year < 2021: + if self.sim.date.year <= p['end_year_testing_data']: current_year = self.sim.date.year else: - current_year = 2020 + current_year = p['end_year_testing_data'] spontaneous_testing(current_year=current_year) # PrEP for AGYW @@ -1977,6 +2228,7 @@ def __init__(self, module, person_id, cause): def apply(self, person_id): df = self.sim.population.props + p = self.module.parameters # Return if person is dead or no HIV+ if not df.at[person_id, "is_alive"] or not df.at[person_id, "hv_inf"]: @@ -2036,7 +2288,8 @@ def apply(self, person_id): date=date_of_aids_death, ) # schedule hospital stay - beddays = self.sim.modules["Hiv"].rng.randint(low=14, high=20) + beddays = self.sim.modules["Hiv"].rng.randint( + low=p['length_of_inpatient_stay_if_terminal'][0], high=p['length_of_inpatient_stay_if_terminal'][1]) date_admission = date_of_aids_death - pd.DateOffset(days=beddays) self.sim.modules["HealthSystem"].schedule_hsi_event( hsi_event=HSI_Hiv_EndOfLifeCare( @@ -2062,7 +2315,8 @@ def apply(self, person_id): date=date_of_aids_death, ) # schedule hospital stay - beddays = self.sim.modules["Hiv"].rng.randint(low=14, high=20) + beddays = self.sim.modules["Hiv"].rng.randint( + low=p['length_of_inpatient_stay_if_terminal'][0], high=p['length_of_inpatient_stay_if_terminal'][1]) date_admission = date_of_aids_death - pd.DateOffset(days=beddays) self.sim.modules["HealthSystem"].schedule_hsi_event( hsi_event=HSI_Hiv_EndOfLifeCare( @@ -2179,7 +2433,8 @@ def apply(self, person_id): date=date_of_aids_death, ) # schedule hospital stay - beddays = self.module.rng.randint(low=14, high=20) + beddays = self.module.rng.randint( + low=p['length_of_inpatient_stay_if_terminal'][0], high=p['length_of_inpatient_stay_if_terminal'][1]) date_admission = date_of_aids_death - pd.DateOffset(days=beddays) self.sim.modules["HealthSystem"].schedule_hsi_event( hsi_event=HSI_Hiv_EndOfLifeCare( @@ -2214,6 +2469,7 @@ def __init__(self, module, person_id): def apply(self, person_id): df = self.sim.population.props + p = self.module.parameters person = df.loc[person_id] m = self.module @@ -2231,7 +2487,7 @@ def apply(self, person_id): data="This event should not be running: Hiv_DecisionToContinueOnPrEP is for those currently on prep") # check still eligible, person must be <30 years old or a fsw - if (person["age_years"] > 30) or not person["li_is_sexworker"]: + if (person["age_years"] > p['age_max_prep']) or not person["li_is_sexworker"]: return # Determine if this appointment is actually attended by the person who has already started on PrEP @@ -2243,7 +2499,7 @@ def apply(self, person_id): self.sim.modules["HealthSystem"].schedule_hsi_event( HSI_Hiv_StartOrContinueOnPrep(person_id=person_id, module=m), topen=self.sim.date, - tclose=self.sim.date + pd.DateOffset(days=7), + tclose=self.sim.date + pd.DateOffset(days=p["hiv_prep_refill_appointment_window_days"]), priority=0, ) @@ -2262,6 +2518,7 @@ def __init__(self, module, person_id): def apply(self, person_id): df = self.sim.population.props + p = self.module.parameters person = df.loc[person_id] m = self.module @@ -2284,7 +2541,7 @@ def apply(self, person_id): HSI_Hiv_StartOrContinueTreatment(person_id=person_id, module=m, facility_level_of_this_hsi="1a"), topen=self.sim.date, - tclose=self.sim.date + pd.DateOffset(days=14), + tclose=self.sim.date + pd.DateOffset(days=p["art_continuation_appointment_window_days"]), priority=0, ) @@ -2296,7 +2553,7 @@ def apply(self, person_id): self.sim.modules["HealthSystem"].schedule_hsi_event( HSI_Hiv_StartOrContinueTreatment(person_id=person_id, module=m, facility_level_of_this_hsi="1a"), - topen=self.sim.date + pd.DateOffset(months=1), + topen=self.sim.date + pd.DateOffset(months=p['art_restart_appointment_delay_months']), tclose=None, priority=0, ) @@ -2361,6 +2618,7 @@ def apply(self, person_id, squeeze_factor): """Do the testing and referring to other services""" df = self.sim.population.props + p = self.module.parameters person = df.loc[person_id] if not person["is_alive"]: @@ -2371,11 +2629,11 @@ def apply(self, person_id, squeeze_factor): return self.sim.modules["HealthSystem"].get_blank_appt_footprint() # if person has had test in last week, do not repeat test - if person["hv_last_test_date"] >= (self.sim.date - DateOffset(days=7)): + if person["hv_last_test_date"] >= (self.sim.date - DateOffset(days=p['hv_min_test_interval_days'])): return self.sim.modules["HealthSystem"].get_blank_appt_footprint() # Run test - if person["age_years"] < 1.0: + if person["age_years"] < p['age_max_hiv_early_infant_test_years']: test_result = self.sim.modules["HealthSystem"].dx_manager.run_dx_test( dx_tests_to_run="hiv_early_infant_test", hsi_event=self ) @@ -2482,7 +2740,7 @@ def apply(self, person_id, squeeze_factor): # repeat appt for HIV test self.sim.modules["HealthSystem"].schedule_hsi_event( self, - topen=self.sim.date + pd.DateOffset(days=7), + topen=self.sim.date + pd.DateOffset(days=p['hv_min_test_interval_days']), tclose=None, priority=0, ) @@ -2507,6 +2765,7 @@ def apply(self, person_id, squeeze_factor): """ Do the circumcision for this man. If he is already circumcised, this is a follow-up appointment.""" self.number_of_occurrences += 1 # The current appointment is included in the count. df = self.sim.population.props # shortcut to the dataframe + p = self.module.parameters person = df.loc[person_id] @@ -2538,14 +2797,14 @@ def apply(self, person_id, squeeze_factor): # schedule first follow-up appt, 3 days from procedure; self.sim.modules["HealthSystem"].schedule_hsi_event( self, - topen=self.sim.date + DateOffset(days=3), + topen=self.sim.date + DateOffset(days=p['hiv_circ_procedure_first_followup_days']), tclose=None, priority=0, ) # schedule second follow-up appt, 7 days from procedure; self.sim.modules["HealthSystem"].schedule_hsi_event( self, - topen=self.sim.date + DateOffset(days=7), + topen=self.sim.date + DateOffset(days=p['hiv_circ_procedure_second_followup_days']), tclose=None, priority=0, ) @@ -2557,7 +2816,7 @@ def apply(self, person_id, squeeze_factor): ): self.sim.modules["HealthSystem"].schedule_hsi_event( self, - topen=self.sim.date + DateOffset(weeks=1), + topen=self.sim.date + DateOffset(weeks=p['hiv_circ_cons_notavailable_retry_procedure_weeks']), tclose=None, priority=0, ) @@ -2581,6 +2840,7 @@ def apply(self, person_id, squeeze_factor): """ df = self.sim.population.props + p = self.module.parameters person = df.loc[person_id] # Do not run if the child is not alive or is diagnosed with hiv @@ -2589,7 +2849,7 @@ def apply(self, person_id, squeeze_factor): # if breastfeeding has ceased or child >18 months, no further prophylaxis required if (df.at[person_id, "nb_breastfeeding_status"] == "none") \ - or (df.at[person_id, "age_years"] >= 1.5): + or (df.at[person_id, "age_years"] >= p['age_max_prophylaxis_years']): return self.sim.modules["HealthSystem"].get_blank_appt_footprint() # Check that infant prophylaxis is available and if it is, initiate: @@ -2606,7 +2866,7 @@ def apply(self, person_id, squeeze_factor): referred_from="repeat3months", repeat_visits=0), priority=1, - topen=self.sim.date + DateOffset(months=3), + topen=self.sim.date + DateOffset(months=p['infant_prophylaxis_followup_months']), tclose=None, ) @@ -2628,7 +2888,7 @@ def apply(self, person_id, squeeze_factor): referred_from="repeatNoCons", repeat_visits=self.repeat_visits), priority=1, - topen=self.sim.date + pd.DateOffset(days=7), + topen=self.sim.date + pd.DateOffset(days=p['infant_prophylaxis_cons_notavailable_retry_days']), tclose=None, ) @@ -2652,6 +2912,7 @@ def apply(self, person_id, squeeze_factor): """Start PrEP for this person; or continue them on PrEP for 3 more months""" df = self.sim.population.props + p = self.module.parameters person = df.loc[person_id] # Do not run if the person is not alive or is diagnosed with hiv @@ -2688,7 +2949,7 @@ def apply(self, person_id, squeeze_factor): # Schedule 'decision about whether to continue on PrEP' for 3 months time self.sim.schedule_event( Hiv_DecisionToContinueOnPrEP(person_id=person_id, module=self.module), - self.sim.date + pd.DateOffset(months=3), + self.sim.date + pd.DateOffset(months=p['prep_continuation_reevaluation_period_months']), ) else: @@ -2707,7 +2968,7 @@ def apply(self, person_id, squeeze_factor): self.sim.modules["HealthSystem"].schedule_hsi_event( self, priority=1, - topen=self.sim.date + pd.DateOffset(days=7), + topen=self.sim.date + pd.DateOffset(days=p['prep_cons_notavailable_retry_days']), tclose=None, ) @@ -2734,6 +2995,7 @@ def apply(self, person_id, squeeze_factor): df = self.sim.population.props person = df.loc[person_id] art_status_at_beginning_of_hsi = person["hv_art"] + p = self.module.parameters if not person["is_alive"]: return @@ -2793,11 +3055,8 @@ def apply(self, person_id, squeeze_factor): if art_status_at_beginning_of_hsi != "not": self.module.stops_treatment(person_id) - p = self.module.parameters[ - "probability_of_seeking_further_art_appointment_if_drug_not_available" - ] - - if self.module.rng.random_sample() >= p: + if (self.module.rng.random_sample() >= + p['probability_of_seeking_further_art_appointment_if_drug_not_available']): # add in referral straight back to tx # if defaulting, seek another treatment appointment in 6 months @@ -2807,7 +3066,7 @@ def apply(self, person_id, squeeze_factor): module=self.module, facility_level_of_this_hsi="1a", ), - topen=self.sim.date + pd.DateOffset(months=6), + topen=self.sim.date + pd.DateOffset(months=p['art_drug_notavailable_default_retry_months']), priority=0, ) @@ -2817,11 +3076,11 @@ def apply(self, person_id, squeeze_factor): # NB. With a probability of 1.0, this will keep occurring, # if person has already tried unsuccessfully to get ART at level 1a 2 times # then refer to level 1b - if self.counter_for_drugs_not_available <= 2: + if self.counter_for_drugs_not_available <= p['art_level1a_healthseekingbehaviour_cap']: # repeat attempt for ARVs at level 1a self.sim.modules["HealthSystem"].schedule_hsi_event( self, - topen=self.sim.date + pd.DateOffset(months=1), + topen=self.sim.date + pd.DateOffset(months=p['art_level1a_appointment_delay_months']), priority=0, ) @@ -2833,7 +3092,7 @@ def apply(self, person_id, squeeze_factor): module=self.module, facility_level_of_this_hsi="2", ), - topen=self.sim.date + pd.DateOffset(days=1), + topen=self.sim.date + pd.DateOffset(days=p['art_level1b_appointment_delay_days']), priority=0, ) @@ -3001,6 +3260,7 @@ def never_ran(self): # stop treatment for this person person_id = self.target self.module.stops_treatment(person_id) + p = self.module.parameters # sample whether person will seek further appt if self.module.rng.random_sample() < self.module.parameters[ @@ -3011,8 +3271,12 @@ def never_ran(self): HSI_Hiv_StartOrContinueTreatment( person_id=person_id, module=self.module, facility_level_of_this_hsi="1a" ), - topen=self.sim.date + pd.DateOffset(days=14), - tclose=self.sim.date + pd.DateOffset(days=21), + topen=self.sim.date + pd.DateOffset( + days=p['seek_further_art_appointment_if_appointment_not_available_min_delay_days'] + ), + tclose=self.sim.date + pd.DateOffset( + days=p['seek_further_art_appointment_if_appointment_not_available_max_delay_days'] + ), priority=1, ) @@ -3025,8 +3289,9 @@ def EXPECTED_APPT_FOOTPRINT(self): * `Peds` for a child - whether newly starting or already on treatment """ person_id = self.target + p = self.module.parameters - if self.sim.population.props.at[person_id, 'age_years'] < 15: + if self.sim.population.props.at[person_id, 'age_years'] < p['age_threshold_child_adult_distinction']: return self.make_appt_footprint({"Peds": 1}) # Child if (self.sim.population.props.at[person_id, 'hv_art'] == "not") & ( @@ -3046,14 +3311,15 @@ class HSI_Hiv_EndOfLifeCare(HSI_Event, IndividualScopeEventMixin): already within 2 weeks """ - def __init__(self, module, person_id, beddays=17): + def __init__(self, module, person_id, beddays=None): super().__init__(module, person_id=person_id) assert isinstance(module, Hiv) self.TREATMENT_ID = "Hiv_PalliativeCare" self.EXPECTED_APPT_FOOTPRINT = self.make_appt_footprint({}) self.ACCEPTED_FACILITY_LEVEL = "2" - + if beddays is None: + beddays = module.parameters['aids_death_beddays_default'] self.beddays = beddays self.BEDDAYS_FOOTPRINT = self.make_beddays_footprint({"general_bed": self.beddays}) diff --git a/tests/test_htm_scaleup.py b/tests/test_htm_scaleup.py index 63f6cba6ba..3bdaaf80cd 100644 --- a/tests/test_htm_scaleup.py +++ b/tests/test_htm_scaleup.py @@ -98,13 +98,13 @@ def test_hiv_scale_up(seed): assert sim.modules["Hiv"].parameters["beta"] < original_params.loc[ original_params.parameter_name == "beta", "value"].values[0] assert sim.modules["Hiv"].parameters["prob_prep_for_fsw_after_hiv_test"] == new_params.loc[ - new_params.parameter == "prob_prep_for_fsw_after_hiv_test", "target_value"].values[0] + new_params.parameter_name == "prob_prep_for_fsw_after_hiv_test", "value"].values[0] assert sim.modules["Hiv"].parameters["prob_prep_for_agyw"] == new_params.loc[ - new_params.parameter == "prob_prep_for_agyw", "target_value"].values[0] + new_params.parameter_name == "prob_prep_for_agyw", "value"].values[0] assert sim.modules["Hiv"].parameters["probability_of_being_retained_on_prep_every_3_months"] == new_params.loc[ - new_params.parameter == "probability_of_being_retained_on_prep_every_3_months", "target_value"].values[0] + new_params.parameter_name == "probability_of_being_retained_on_prep_every_3_months", "value"].values[0] assert sim.modules["Hiv"].parameters["prob_circ_after_hiv_test"] == new_params.loc[ - new_params.parameter == "prob_circ_after_hiv_test", "target_value"].values[0] + new_params.parameter_name == "prob_circ_after_hiv_test", "value"].values[0] # check malaria parameters unchanged mal_original_params = read_csv_files(resourcefilepath / 'malaria' / 'ResourceFile_malaria', @@ -176,13 +176,13 @@ def test_htm_scale_up(seed): assert sim.modules["Hiv"].parameters["beta"] < original_hiv_params.loc[ original_hiv_params.parameter_name == "beta", "value"].values[0] assert sim.modules["Hiv"].parameters["prob_prep_for_fsw_after_hiv_test"] == new_hiv_params.loc[ - new_hiv_params.parameter == "prob_prep_for_fsw_after_hiv_test", "target_value"].values[0] + new_hiv_params.parameter_name == "prob_prep_for_fsw_after_hiv_test", "value"].values[0] assert sim.modules["Hiv"].parameters["prob_prep_for_agyw"] == new_hiv_params.loc[ - new_hiv_params.parameter == "prob_prep_for_agyw", "target_value"].values[0] + new_hiv_params.parameter_name == "prob_prep_for_agyw", "value"].values[0] assert sim.modules["Hiv"].parameters["probability_of_being_retained_on_prep_every_3_months"] == new_hiv_params.loc[ - new_hiv_params.parameter == "probability_of_being_retained_on_prep_every_3_months", "target_value"].values[0] + new_hiv_params.parameter_name == "probability_of_being_retained_on_prep_every_3_months", "value"].values[0] assert sim.modules["Hiv"].parameters["prob_circ_after_hiv_test"] == new_hiv_params.loc[ - new_hiv_params.parameter == "prob_circ_after_hiv_test", "target_value"].values[0] + new_hiv_params.parameter_name == "prob_circ_after_hiv_test", "value"].values[0] # check malaria parameters changed new_mal_params = read_csv_files(resourcefilepath / 'malaria' / 'ResourceFile_malaria',