diff --git a/docs/write-ups/Depression.docx b/docs/write-ups/Depression.docx index 0d03c044b6..b2af7525c6 100644 --- a/docs/write-ups/Depression.docx +++ b/docs/write-ups/Depression.docx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74cc9af2d9e114bee5f695cddcab49c92d79b6fa5abd7c8bb59b1c2a284b8ead -size 233268 +oid sha256:947d9954ead452370841b3e8bec8b6bf17fcea40e240ea342dcf169e6c6bb68d +size 235099 diff --git a/docs/write-ups/OesophagealCancer.docx b/docs/write-ups/OesophagealCancer.docx index 80654e9cb9..c52e9e261a 100644 --- a/docs/write-ups/OesophagealCancer.docx +++ b/docs/write-ups/OesophagealCancer.docx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae9f447c5ab5d296f4d5d766a0675865998e386c5b17c97095e36799435f9277 -size 180430 +oid sha256:ef6ac87512767a2c70dc67d22f5c409c425bf26736624ba039285a1d906750ca +size 185471 diff --git a/docs/write-ups/OtherAdultCancer.docx b/docs/write-ups/OtherAdultCancer.docx index 99c7c562e1..42bf5f2b09 100644 --- a/docs/write-ups/OtherAdultCancer.docx +++ b/docs/write-ups/OtherAdultCancer.docx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:299b9025a1fe1fad73a42ed321061e655e638a9c3ce8ab67e3ee691815ae6d98 -size 71556 +oid sha256:1f3ff79a3f4e2ad48981cffd95eb6d96d7aa3dc8777015e2b71e63995bf336c9 +size 76152 diff --git a/resources/ResourceFile_Depression.xlsx b/resources/ResourceFile_Depression.xlsx index c4feb268fa..b6daab2002 100644 --- a/resources/ResourceFile_Depression.xlsx +++ b/resources/ResourceFile_Depression.xlsx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0bb3ed875059fa60e8708db0c19da56942d46297c07185849b1ff604f9c67ca8 -size 12357 +oid sha256:d7016feb63786ef5e87ed199dd79f8b6b4a8b8ead227196a4fb578bf33bc1a2f +size 12432 diff --git a/resources/ResourceFile_HIV.xlsx b/resources/ResourceFile_HIV.xlsx index 6e73cf07fe..64ef25c261 100644 --- a/resources/ResourceFile_HIV.xlsx +++ b/resources/ResourceFile_HIV.xlsx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d20e316c9b00816da27cc8c104b70cec214787f0d96c80a437f8690806c5e2fb -size 158196 +oid sha256:e2345032931c1360046dc7394681cc39669687888f7f8f3e42469d8add067438 +size 160376 diff --git a/resources/ResourceFile_Improved_Healthsystem_And_Healthcare_Seeking.xlsx b/resources/ResourceFile_Improved_Healthsystem_And_Healthcare_Seeking.xlsx index 5b2d55972e..ff88584e3f 100644 --- a/resources/ResourceFile_Improved_Healthsystem_And_Healthcare_Seeking.xlsx +++ b/resources/ResourceFile_Improved_Healthsystem_And_Healthcare_Seeking.xlsx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f18f5a91e48fc846c332b55f1742d148816b1fee08ee34383c9995ea36dafdb3 -size 48167 +oid sha256:da900375dda86f999e744bfb6d6dec7347d5b13f176046ba182740421a43d256 +size 48274 diff --git a/resources/ResourceFile_Other_Adult_Cancers.xlsx b/resources/ResourceFile_Other_Adult_Cancers.xlsx index bf6997cb24..160247c80d 100644 --- a/resources/ResourceFile_Other_Adult_Cancers.xlsx +++ b/resources/ResourceFile_Other_Adult_Cancers.xlsx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be37486adb1684f56f4a03a490b7a952d5ddd4e0a19b5aa146d5aaee8dc5e680 -size 10830 +oid sha256:3d7a8daecbf8a3a6fb7efdcefffb5ece5e91c6abc811da5a038e4fc1fbb44774 +size 10865 diff --git a/resources/ResourceFile_TB.xlsx b/resources/ResourceFile_TB.xlsx index 494e6b31fd..d40eb40490 100644 --- a/resources/ResourceFile_TB.xlsx +++ b/resources/ResourceFile_TB.xlsx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6760fe2b529eb1538bbbfbd6d5e7350f9dbc2272ab997b363771c8b901739bb3 -size 54894 +oid sha256:3fc295cc70b8e86c75e1725a92ceb95bc86a26d3fbe1f680db379726bcab3ab3 +size 55662 diff --git a/resources/cmd/ResourceFile_cmd_condition_death.xlsx b/resources/cmd/ResourceFile_cmd_condition_death.xlsx index 725996cd2c..536c8ebbc9 100644 --- a/resources/cmd/ResourceFile_cmd_condition_death.xlsx +++ b/resources/cmd/ResourceFile_cmd_condition_death.xlsx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bedbf3304e4078e2d495a136eb838757fa06d0e5c7cad13714ba469f1c9a63ae -size 19087 +oid sha256:b13090d0ecf6d95c6001133bed733a1642f5e698cc0f048d7526357506088862 +size 19200 diff --git a/resources/cmd/ResourceFile_cmd_condition_onset.xlsx b/resources/cmd/ResourceFile_cmd_condition_onset.xlsx index d53e29356f..292f9b0f7b 100644 --- a/resources/cmd/ResourceFile_cmd_condition_onset.xlsx +++ b/resources/cmd/ResourceFile_cmd_condition_onset.xlsx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:154eb80d1676dd56bd832b77bcd40965ab9c69ee9dc6c146d1a3613cac2848a6 -size 30573 +oid sha256:b7db0f7a4aca08cdb1852bde35262295c294354a1838b72d65fb14b5cc257364 +size 30669 diff --git a/resources/cmd/ResourceFile_cmd_condition_removal.xlsx b/resources/cmd/ResourceFile_cmd_condition_removal.xlsx index 801b4b69f9..49a7ea79ee 100644 --- a/resources/cmd/ResourceFile_cmd_condition_removal.xlsx +++ b/resources/cmd/ResourceFile_cmd_condition_removal.xlsx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b66ae1f6017e1c506970092b975bd6d002dacce13e5c325e59b17cee984eec2f -size 18556 +oid sha256:b0d42326a048511264bedf856bb757d0a609ac01361ea1da2b0ed89c066dfa8b +size 18625 diff --git a/resources/cmd/ResourceFile_cmd_condition_testing.xlsx b/resources/cmd/ResourceFile_cmd_condition_testing.xlsx index 63b6baf08f..ead6ce011e 100644 --- a/resources/cmd/ResourceFile_cmd_condition_testing.xlsx +++ b/resources/cmd/ResourceFile_cmd_condition_testing.xlsx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3fbe4450eb795d3f3c42f71e459f1e703ec4e9aa41c75ed738a6f9c098c5db53 -size 11437 +oid sha256:82ad9558d816133b020e942c7962b1c783469fa3ba322db164a7305c60caf698 +size 11491 diff --git a/resources/cmd/ResourceFile_cmd_events.xlsx b/resources/cmd/ResourceFile_cmd_events.xlsx index cce09206ec..a7d2cb9bae 100644 --- a/resources/cmd/ResourceFile_cmd_events.xlsx +++ b/resources/cmd/ResourceFile_cmd_events.xlsx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a34707bd325d919b8e94cb7135e5a3f17da4bca67d8cc2023418f7b1099e070 -size 14047 +oid sha256:1419799a637737654950aa137f2f7fd131a934deea6fa2e48ee5b37e531e4517 +size 14104 diff --git a/resources/cmd/ResourceFile_cmd_events_death.xlsx b/resources/cmd/ResourceFile_cmd_events_death.xlsx index 1dc7d3d67a..1983a2f88e 100644 --- a/resources/cmd/ResourceFile_cmd_events_death.xlsx +++ b/resources/cmd/ResourceFile_cmd_events_death.xlsx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9f6e0e87f17ac90e074eb9434c90f67ec6207f3475c11e4340ba38fe2e94e13 -size 13343 +oid sha256:328f3c2a624dbbf9072712f573213c744dee7f962f854475b08ffb2a306392c0 +size 13425 diff --git a/resources/healthsystem/priority_policies/ResourceFile_PriorityRanking_ALLPOLICIES.xlsx b/resources/healthsystem/priority_policies/ResourceFile_PriorityRanking_ALLPOLICIES.xlsx index 5b734150cd..d9dbac2e99 100644 --- a/resources/healthsystem/priority_policies/ResourceFile_PriorityRanking_ALLPOLICIES.xlsx +++ b/resources/healthsystem/priority_policies/ResourceFile_PriorityRanking_ALLPOLICIES.xlsx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ebfc8af480d9bea1c5321c16acfc64e9ed0a90dea1c15c8a21eb692715ffcf0f -size 42109 +oid sha256:734d46d83dccf15bf38ee171a487664f01035da6cf68660d4af62097a6160fb6 +size 42716 diff --git a/resources/malaria/ResourceFile_malaria.xlsx b/resources/malaria/ResourceFile_malaria.xlsx index 12d3014efb..70902b7480 100644 --- a/resources/malaria/ResourceFile_malaria.xlsx +++ b/resources/malaria/ResourceFile_malaria.xlsx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b703bd5ee7979d349f5dfa825929d36c174a43381ab6fdf08f1b9264814a802 -size 67624 +oid sha256:6ba5849e265103ee799d1982325b6fed1ef4d3df559ffce9d6790395c201fcaf +size 67562 diff --git a/src/scripts/hiv/projections_jan2023/analysis_full_model.py b/src/scripts/hiv/projections_jan2023/analysis_full_model.py index e1213cd28e..2386ea56c6 100644 --- a/src/scripts/hiv/projections_jan2023/analysis_full_model.py +++ b/src/scripts/hiv/projections_jan2023/analysis_full_model.py @@ -23,8 +23,8 @@ # %% Run the simulation start_date = Date(2010, 1, 1) -end_date = Date(2020, 1, 1) -popsize = 5000 +end_date = Date(2012, 1, 1) +popsize = 500 # scenario = 0 # set up the log config diff --git a/src/scripts/hiv/projections_jan2023/analysis_logged_deviance.py b/src/scripts/hiv/projections_jan2023/analysis_logged_deviance.py index c004748ad4..eca9f999bc 100644 --- a/src/scripts/hiv/projections_jan2023/analysis_logged_deviance.py +++ b/src/scripts/hiv/projections_jan2023/analysis_logged_deviance.py @@ -34,8 +34,8 @@ # %% Run the simulation start_date = Date(2010, 1, 1) -end_date = Date(2015, 1, 1) -popsize = 5000 +end_date = Date(2014, 1, 1) +popsize = 1000 # scenario = 1 diff --git a/src/tlo/methods/bladder_cancer.py b/src/tlo/methods/bladder_cancer.py index 4d81432d04..4c94fd8f51 100644 --- a/src/tlo/methods/bladder_cancer.py +++ b/src/tlo/methods/bladder_cancer.py @@ -45,7 +45,7 @@ def __init__(self, name=None, resourcefilepath=None): INIT_DEPENDENCIES = {'Demography', 'Lifestyle', 'HealthSystem', 'SymptomManager'} - OPTIONAL_INIT_DEPENDENCIES = {'HealthBurden'} + OPTIONAL_INIT_DEPENDENCIES = {'HealthBurden', 'Schisto'} METADATA = { Metadata.DISEASE_MODULE, @@ -242,17 +242,23 @@ def initialise_population(self, population): # check parameters are sensible: probability of having any cancer stage cannot exceed 1.0 assert sum(p['init_prop_bladder_cancer_stage']) <= 1.0 - lm_init_bc_status_any_stage = LinearModel( - LinearModelType.MULTIPLICATIVE, - sum(p['init_prop_bladder_cancer_stage']), + predictors = [ Predictor('li_tob').when(True, p['rp_bladder_cancer_tobacco']), - # todo: add line when schisto is merged - # Predictor('sh_infection_status').when('High-infection', p['rp_bladder_cancer_schisto_h']), Predictor('age_years', conditions_are_mutually_exclusive=True) .when('.between(30,49)', p['rp_bladder_cancer_age3049']) .when('.between(50,69)', p['rp_bladder_cancer_age5069']) .when('.between(70,120)', p['rp_bladder_cancer_agege70']) .when('.between(0,14)', 0.0) + ] + + conditional_predictors = [ + Predictor('ss_sh_infection_status').when('High-infection', p['rp_bladder_cancer_schisto_h']), + ] if "Schisto" in self.sim.modules else [] + + lm_init_bc_status_any_stage = LinearModel( + LinearModelType.MULTIPLICATIVE, + sum(p['init_prop_bladder_cancer_stage']), + *(predictors + conditional_predictors) ) bc_status_any_stage = lm_init_bc_status_any_stage.predict(df.loc[df.is_alive], self.rng) @@ -397,20 +403,26 @@ def initialise_simulation(self, sim): p = self.parameters lm = self.linear_models_for_progession_of_bc_status - lm['tis_t1'] = LinearModel( - LinearModelType.MULTIPLICATIVE, - p['r_tis_t1_bladder_cancer_none'], - # todo: add in when schisto is in - # Predictor('sh_infection_status').when('High-infection', p['rp_bladder_cancer_schisto_h']), + predictors = [ Predictor('age_years', conditions_are_mutually_exclusive=True) .when('.between(30,49)', p['rp_bladder_cancer_age3049']) .when('.between(50,69)', p['rp_bladder_cancer_age5069']) .when('.between(70,120)', p['rp_bladder_cancer_agege70']) .when('.between(0,14)', 0.0), Predictor('li_tob').when(True, p['rr_tis_t1_bladder_cancer_none_tobacco']), - # todo: add in when schisto module in master - # Predictor('sh_').when(True, p['rr_tis_t1_bladder_cancer_none_ex_alc']), - Predictor('bc_status').when('none', 1.0).otherwise(0.0) + # todo: + # Predictor('tmp_').when(True, p['rr_tis_t1_bladder_cancer_none_ex_alc']), + Predictor('bc_status').when('none', 1.0).otherwise(0.0), + ] + + conditional_predictors = [ + Predictor('ss_sh_infection_status').when('High-infection', p['rp_bladder_cancer_schisto_h']), + ] if "Schisto" in self.sim.modules else [] + + lm["tis_t1"] = LinearModel( + LinearModelType.MULTIPLICATIVE, + p['r_tis_t1_bladder_cancer_none'], + *(predictors + conditional_predictors) ) lm['t2p'] = LinearModel( diff --git a/src/tlo/methods/cardio_metabolic_disorders.py b/src/tlo/methods/cardio_metabolic_disorders.py index a77199072b..921b2e0f71 100644 --- a/src/tlo/methods/cardio_metabolic_disorders.py +++ b/src/tlo/methods/cardio_metabolic_disorders.py @@ -61,7 +61,7 @@ class CardioMetabolicDisorders(Module): INIT_DEPENDENCIES = {'Demography', 'Lifestyle', 'HealthSystem', 'SymptomManager'} - OPTIONAL_INIT_DEPENDENCIES = {'HealthBurden'} + OPTIONAL_INIT_DEPENDENCIES = {'HealthBurden', 'Hiv'} ADDITIONAL_DEPENDENCIES = {'Depression'} @@ -598,9 +598,7 @@ def build_linear_model(self, condition, interval_between_polls, lm_type): # LinearModel expects native python types - if it's numpy type, convert it baseline_annual_probability = float(baseline_annual_probability) - linearmodel = LinearModel( - LinearModelType.MULTIPLICATIVE, - baseline_annual_probability, + predictors = [ Predictor('sex').when('M', p['rr_male']), Predictor( 'age_years', @@ -693,6 +691,17 @@ def build_linear_model(self, condition, interval_between_polls, lm_type): 'rr_chronic_ischemic_heart_disease_on_medication']), Predictor('nc_ever_stroke_on_medication').when(True, p['rr_stroke_on_medication']), Predictor('nc_ever_heart_attack_on_medication').when(True, p['rr_heart_attack_on_medication']) + ] + + conditional_predictors = [ + Predictor().when('hv_inf & ' + '(hv_art != "on_VL_suppressed")', p['rr_hiv']), + ] if "Hiv" in self.sim.modules else [] + + linearmodel = LinearModel( + LinearModelType.MULTIPLICATIVE, + baseline_annual_probability, + *(predictors + conditional_predictors) ) return linearmodel diff --git a/src/tlo/methods/depression.py b/src/tlo/methods/depression.py index d689d8e343..c7f2577382 100644 --- a/src/tlo/methods/depression.py +++ b/src/tlo/methods/depression.py @@ -39,7 +39,7 @@ def __init__(self, name=None, resourcefilepath=None): 'Demography', 'Contraception', 'HealthSystem', 'Lifestyle', 'SymptomManager' } - OPTIONAL_INIT_DEPENDENCIES = {'HealthBurden'} + OPTIONAL_INIT_DEPENDENCIES = {'HealthBurden', 'Hiv'} # Declare Metadata METADATA = { @@ -145,6 +145,8 @@ def __init__(self, name=None, resourcefilepath=None): 'rr_depr_agege60': Parameter(Types.REAL, 'Relative rate of depression associated with age > 60'), + 'rr_depr_hiv': Parameter(Types.REAL, 'Relative rate of depression associated with HIV infection'), + 'depr_resolution_rates': Parameter( Types.LIST, 'Risk of depression resolving in 3 months if no chronic conditions and no treatments.' @@ -227,11 +229,13 @@ def read_parameters(self, data_folder): ) p = self.parameters - # Build the Linear Models: + # Build the Linear Models + + # ----- Initialisation of population ----- self.linearModels = dict() - self.linearModels['Depression_At_Population_Initialisation'] = LinearModel( - LinearModelType.MULTIPLICATIVE, - self.parameters['init_pr_depr_m_age1519_no_cc_wealth123'], + + # risk of depression in initial population + predictors = [ Predictor('de_cc').when(True, p['init_rp_depr_cc']), Predictor('li_wealth').when('.isin([4,5])', p['init_rp_depr_wealth45']), Predictor().when('(sex=="F") & de_recently_pregnant', p['init_rp_depr_f_rec_preg']), @@ -244,44 +248,63 @@ def read_parameters(self, data_folder): .when('.between(0, 14)', 0) .when('.between(15, 19)', 1.0) .when('.between(20, 59)', p['init_rp_depr_age2059']) - .when('>= 60', p['init_rp_depr_agege60']) + .when('>= 60', p['init_rp_depr_agege60']), + ] + + conditional_predictors = [ + Predictor().when( + 'hv_inf & hv_diagnosed', + p["rr_depr_hiv"]), + ] if "Hiv" in self.sim.modules else [] + + self.linearModels["Depression_At_Population_Initialisation"] = LinearModel( + LinearModelType.MULTIPLICATIVE, + p['init_pr_depr_m_age1519_no_cc_wealth123'], + *(predictors + conditional_predictors) ) + # risk of ever having depression in initial population self.linearModels['Depression_Ever_At_Population_Initialisation_Males'] = LinearModel.multiplicative( Predictor('age_years').apply( lambda x: (x if x > 15 else 0) * self.parameters['init_rp_ever_depr_per_year_older_m'] ) ) + # risk of ever having depression in initial population (female) self.linearModels['Depression_Ever_At_Population_Initialisation_Females'] = LinearModel.multiplicative( Predictor('age_years').apply(lambda x: (x if x > 15 else 0) * p['init_rp_ever_depr_per_year_older_f']) ) + # risk of ever having diagnosed depression in initial population self.linearModels['Depression_Ever_Diagnosed_At_Population_Initialisation'] = LinearModel.multiplicative( Predictor('de_ever_depr').when(True, p['init_pr_ever_diagnosed_depression']) .otherwise(0.0) ) + # risk of currently using anti-depressants in initial population self.linearModels['Using_AntiDepressants_Initialisation'] = LinearModel.multiplicative( Predictor('de_depr').when(True, p['init_pr_antidepr_curr_depr']), Predictor().when('~de_depr & de_ever_diagnosed_depression', p['init_rp_antidepr_ever_depr_not_curr']) ) + # risk of ever having talking therapy in initial population self.linearModels['Ever_Talking_Therapy_Initialisation'] = LinearModel( LinearModelType.MULTIPLICATIVE, p['init_pr_ever_talking_therapy_if_diagnosed'], Predictor('de_ever_diagnosed_depression').when(False, 0) ) + # risk of ever having self-harmed in initial population self.linearModels['Ever_Self_Harmed_Initialisation'] = LinearModel( LinearModelType.MULTIPLICATIVE, p['init_pr_ever_self_harmed_if_ever_depr'], Predictor('de_ever_depr').when(False, 0) ) - self.linearModels['Risk_of_Depression_Onset_per3mo'] = LinearModel( - LinearModelType.MULTIPLICATIVE, - p['base_3m_prob_depr'], + # ----- Recurring events ----- + + # risk of depression every 3 months + predictors = [ Predictor('de_cc').when(True, p['rr_depr_cc']), Predictor('age_years', conditions_are_mutually_exclusive=True) .when('.between(0, 14)', 0) @@ -291,9 +314,22 @@ def read_parameters(self, data_folder): Predictor('sex').when('F', p['rr_depr_female']), Predictor('de_recently_pregnant').when(True, p['rr_depr_pregnancy']), Predictor('de_ever_depr').when(True, p['rr_depr_prev_epis']), - Predictor('de_on_antidepr').when(True, p['rr_depr_on_antidepr']) + Predictor('de_on_antidepr').when(True, p['rr_depr_on_antidepr']), + ] + + conditional_predictors = [ + Predictor().when( + 'hv_inf & hv_diagnosed', + p["rr_depr_hiv"]), + ] if "Hiv" in self.sim.modules else [] + + self.linearModels["Risk_of_Depression_Onset_per3mo"] = LinearModel( + LinearModelType.MULTIPLICATIVE, + p['base_3m_prob_depr'], + *(predictors + conditional_predictors) ) + # risk of depression resolution every 3 months self.linearModels['Risk_of_Depression_Resolution_per3mo'] = LinearModel.multiplicative( Predictor('de_intrinsic_3mo_risk_of_depr_resolution').apply(lambda x: x), Predictor('de_cc').when(True, p['rr_resol_depr_cc']), @@ -301,16 +337,19 @@ def read_parameters(self, data_folder): Predictor('de_ever_talk_ther').when(True, p['rr_resol_depr_current_talk_ther']) ) + # risk of stopping anti-depressants every 3 months self.linearModels['Risk_of_Stopping_Antidepressants_per3mo'] = LinearModel.multiplicative( Predictor('de_depr').when(True, p['prob_3m_default_antidepr']) .when(False, p['prob_3m_stop_antidepr']) ) + # risk of self-harm every 3 months self.linearModels['Risk_of_SelfHarm_per3mo'] = LinearModel( LinearModelType.MULTIPLICATIVE, p['prob_3m_selfharm_depr'] ) + # risk of suicide every 3 months self.linearModels['Risk_of_Suicide_per3mo'] = LinearModel( LinearModelType.MULTIPLICATIVE, p['prob_3m_suicide_depr_m'], @@ -519,7 +558,7 @@ def _check_for_suspected_depression( ): """ Returns True if any signs of depression are present, otherwise False. - + Raises an error if the treatment type cannot be identified. """ if treatment_id == "FirstAttendance_NonEmergency": @@ -574,7 +613,7 @@ def do_when_suspected_depression( """ This is called by any HSI event when depression is suspected or otherwise investigated. - At least one of the diagnosis_function or hsi_event arguments must be provided; if both + At least one of the diagnosis_function or hsi_event arguments must be provided; if both are provided, the hsi_event argument is ignored. - If the hsi_event argument is provided, that event is used to access the diagnosis manager and run diagnosis tests. diff --git a/src/tlo/methods/hiv.py b/src/tlo/methods/hiv.py index b51ac520aa..a132c6e008 100644 --- a/src/tlo/methods/hiv.py +++ b/src/tlo/methods/hiv.py @@ -9,7 +9,7 @@ - with viral suppression: when the person with not develop AIDS, or if they have already, it is relieved and they will not die of AIDS; and the person is not infectious - without viral suppression: when there is no benefit in avoiding AIDS and infectiousness is unchanged. -Maintenance on ART and PrEP is re-assessed at periodic 'Decision Events', at which is it is determined if the person +Maintenance on ART and PrEP is re-assessed at periodic 'Decision Events', at which it is determined if the person will attend the "next" HSI for continuation of the service; and if not, they are removed from that service and "stop treatment". If a stock-out or non-availability of health system resources prevent treatment continuation, the person "stops treatment". Stopping treatment leads to a new AIDS Event being scheduled. Persons can restart treatment. If a @@ -189,6 +189,9 @@ def __init__(self, name=None, resourcefilepath=None, run_with_checks=False): "rr_edlevel_higher": Parameter( Types.REAL, "Relative risk of HIV with higher education" ), + "rr_schisto": Parameter( + Types.REAL, "Relative risk of HIV with high intensity S. haematobium infection" + ), # Natural history - transmission - relative risk of HIV acquisition (interventions) "rr_behaviour_change": Parameter( Types.REAL, "Relative risk of HIV with behaviour modification" @@ -377,10 +380,19 @@ def __init__(self, name=None, resourcefilepath=None, run_with_checks=False): Types.REAL, "probability of death if aids and tb, person on treatment for tb", ), + "hiv_healthseekingbehaviour_cap": Parameter( + Types.INT, + "number of repeat visits assumed for healthcare services", + ), "dispensation_period_months": Parameter( Types.REAL, "length of prescription for ARVs in months, same for all PLHIV", ), + "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]", + ), } def read_parameters(self, data_folder): @@ -391,7 +403,7 @@ def read_parameters(self, data_folder): # 1) Read the ResourceFiles - # Short cut to parameters dict + # Shortcut to parameters dict p = self.parameters workbook = pd.read_excel( @@ -449,7 +461,7 @@ def pre_initialise_population(self): # ---- LINEAR MODELS ----- # LinearModel for the relative risk of becoming infected during the simulation # N.B. age assumed not to have an effect on incidence - self.lm["rr_of_infection"] = LinearModel.multiplicative( + predictors = [ Predictor("age_years").when("<15", 0.0).when("<49", 1.0).otherwise(0.0), Predictor("sex").when("F", p["rr_sex_f"]), Predictor("li_is_circ").when(True, p["rr_circumcision"]), @@ -464,17 +476,32 @@ def pre_initialise_population(self): Predictor("li_ed_lev", conditions_are_mutually_exclusive=True) .when(2, p["rr_edlevel_primary"]) .when(3, p["rr_edlevel_secondary"]), - Predictor("hv_behaviour_change").when(True, p["rr_behaviour_change"]), - ) + Predictor("hv_behaviour_change").when(True, p["rr_behaviour_change"]) + ] + + conditional_predictors = [ + Predictor().when( + '(ss_sh_infection_status == "High-infection") &' + '(sex == "F")', + p["rr_schisto"] + ), + ] if "Schisto" in self.sim.modules else [] + + self.lm["rr_of_infection"] = LinearModel.multiplicative( + *(predictors + conditional_predictors)) # LinearModels to give the shape and scale for the Weibull distribution describing time from infection to death self.lm["scale_parameter_for_infection_to_death"] = LinearModel.multiplicative( Predictor( "age_years", conditions_are_mutually_exclusive=True, - conditions_are_exhaustive=True) + conditions_are_exhaustive=True, + ) .when("==0", p["mean_survival_for_infants_infected_prior_to_birth"]) - .when(".between(1,4)", p["infection_to_death_infant_infection_after_birth_weibull_scale"]) + .when( + ".between(1,4)", + p["infection_to_death_infant_infection_after_birth_weibull_scale"], + ) .when(".between(5, 19)", p["infection_to_death_weibull_scale_1519"]) .when(".between(20, 24)", p["infection_to_death_weibull_scale_2024"]) .when(".between(25, 29)", p["infection_to_death_weibull_scale_2529"]) @@ -503,13 +530,16 @@ def pre_initialise_population(self): ) # -- Linear Model to give the mean months between aids and death depending on age - self.lm["offset_parameter_for_months_from_aids_to_death"] = LinearModel.multiplicative( - Predictor( - "age_years", - conditions_are_mutually_exclusive=True, - conditions_are_exhaustive=True) - .when("<5", p["mean_months_between_aids_and_death_infant"]) - .when(">=5", p["mean_months_between_aids_and_death"]) + self.lm["offset_parameter_for_months_from_aids_to_death"] = ( + LinearModel.multiplicative( + Predictor( + "age_years", + conditions_are_mutually_exclusive=True, + conditions_are_exhaustive=True, + ) + .when("<5", p["mean_months_between_aids_and_death_infant"]) + .when(">=5", p["mean_months_between_aids_and_death"]) + ) ) # -- Linear Models for the Uptake of Services @@ -567,7 +597,6 @@ def initialise_population(self, population): # --- Current status df.loc[df.is_alive, "hv_inf"] = False df.loc[df.is_alive, "hv_art"] = "not" - df.loc[df.is_alive, "hv_date_treated"] = pd.NaT df.loc[df.is_alive, "hv_is_on_prep"] = False df.loc[df.is_alive, "hv_behaviour_change"] = False df.loc[df.is_alive, "hv_diagnosed"] = False @@ -576,6 +605,8 @@ def initialise_population(self, population): # --- Dates on which things have happened df.loc[df.is_alive, "hv_date_inf"] = pd.NaT df.loc[df.is_alive, "hv_last_test_date"] = pd.NaT + df.loc[df.is_alive, "hv_date_treated"] = pd.NaT + df.loc[df.is_alive, "hv_date_last_ART"] = pd.NaT # Launch sub-routines for allocating the right number of people into each category self.initialise_baseline_prevalence(population) # allocate baseline prevalence @@ -608,16 +639,14 @@ def initialise_baseline_prevalence(self, population): Predictor("li_is_sexworker").when(True, params["rr_fsw"]), Predictor("li_is_circ").when(True, params["rr_circumcision"]), Predictor("li_urban").when(False, params["rr_rural"]), - Predictor( - "li_wealth", conditions_are_mutually_exclusive=True).when( - 2, params["rr_windex_poorer"]).when( - 3, params["rr_windex_middle"]).when( - 4, params["rr_windex_richer"]).when( - 5, params["rr_windex_richest"]), - Predictor( - "li_ed_lev", conditions_are_mutually_exclusive=True).when( - 2, params["rr_edlevel_primary"]).when( - 3, params["rr_edlevel_secondary"]) + Predictor("li_wealth", conditions_are_mutually_exclusive=True) + .when(2, params["rr_windex_poorer"]) + .when(3, params["rr_windex_middle"]) + .when(4, params["rr_windex_richer"]) + .when(5, params["rr_windex_richest"]), + Predictor("li_ed_lev", conditions_are_mutually_exclusive=True) + .when(2, params["rr_edlevel_primary"]) + .when(3, params["rr_edlevel_secondary"]), ).predict(df.loc[df.is_alive]) # Rescale relative probability of infection so that its average is 1.0 within each age/sex group @@ -758,6 +787,11 @@ def split_into_vl_and_notvl(all_idx, prob): # 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) + + 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") + df.at[person, "hv_date_last_ART"] = self.sim.date - pd.to_timedelta(date_treated, unit="days") + self.sim.schedule_event( Hiv_DecisionToContinueTreatment(person_id=person, module=self), self.sim.date + pd.to_timedelta(days, unit="days"), @@ -846,6 +880,7 @@ def initialise_simulation(self, sim): * 7) Look-up and save the codes for consumables """ df = sim.population.props + p = self.parameters # 1) Schedule the Main HIV Regular Polling Event sim.schedule_event( @@ -858,7 +893,7 @@ def initialise_simulation(self, sim): # 3) Determine who has AIDS and impose the Symptoms 'aids_symptoms' # 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 != "not")].index + 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 # (will have AIDS Death event scheduled) @@ -866,7 +901,7 @@ def initialise_simulation(self, sim): df.is_alive & df.hv_inf & ((self.sim.date - df.hv_date_inf).dt.days > 10 * 365) - & (df.hv_art == "not") + & (df.hv_art != "on_VL_suppressed") ].index # Those that are in neither category are "before AIDS" (will have AIDS Onset Event scheduled) @@ -920,7 +955,8 @@ def initialise_simulation(self, sim): ) date_aids_death = ( - self.sim.date + pd.DateOffset(months=self.rng.randint(low=0, high=18)) + self.sim.date + pd.DateOffset( + months=self.rng.randint(low=0, high=p['mean_months_between_aids_and_death'])) ) # 30% AIDS deaths have TB co-infection @@ -931,6 +967,23 @@ def initialise_simulation(self, sim): date=date_aids_death, ) + # schedule hospital stay for end of life care if untreated + beddays = self.rng.randint( + low=p['length_of_inpatient_stay_if_terminal'][0], + high=p['length_of_inpatient_stay_if_terminal'][1]) + date_admission = date_aids_death - pd.DateOffset(days=beddays) + self.sim.modules["HealthSystem"].schedule_hsi_event( + hsi_event=HSI_Hiv_EndOfLifeCare( + person_id=person_id, module=self, beddays=beddays + ), + priority=0, + topen=( + date_admission + if (date_admission >= self.sim.date) + else self.sim.date + ), + ) + # 5) (Optionally) Schedule the event to check the configuration of all properties if self.run_with_checks: sim.schedule_event( @@ -1027,7 +1080,6 @@ def on_birth(self, mother_id, child_id): df.at[child_id, "hv_inf"] = False df.at[child_id, "hv_art"] = "not" df.at[child_id, "hv_on_cotrimoxazole"] = False - df.at[child_id, "hv_date_treated"] = pd.NaT df.at[child_id, "hv_is_on_prep"] = False df.at[child_id, "hv_behaviour_change"] = False df.at[child_id, "hv_diagnosed"] = False @@ -1036,6 +1088,8 @@ def on_birth(self, mother_id, child_id): # --- Dates on which things have happened df.at[child_id, "hv_date_inf"] = pd.NaT df.at[child_id, "hv_last_test_date"] = pd.NaT + df.at[child_id, "hv_date_treated"] = pd.NaT + df.at[child_id, "hv_date_last_ART"] = pd.NaT # ----------------------------------- MTCT - AT OR PRIOR TO BIRTH -------------------------- # DETERMINE IF THE CHILD IS INFECTED WITH HIV FROM THEIR MOTHER DURING PREGNANCY / DELIVERY @@ -1098,7 +1152,7 @@ def on_birth(self, mother_id, child_id): tclose=None, ) - # if mother known HIV+, schedule virological test for infant + # if mother known HIV+, schedule virological test for infant and give prep if mother.hv_diagnosed and df.at[child_id, "is_alive"]: self.sim.modules["HealthSystem"].schedule_hsi_event( hsi_event=HSI_Hiv_StartInfantProphylaxis( @@ -1266,10 +1320,10 @@ def get_time_from_aids_to_death(self): """ mean = self.parameters["mean_months_between_aids_and_death"] draw_number_of_months = int(np.round(self.rng.exponential(mean))) - return pd.DateOffset(months=draw_number_of_months) + return pd.DateOffset(months=(draw_number_of_months + 1)) def do_when_hiv_diagnosed(self, person_id): - """Things to do when a person has been tested and found (newly) be be HIV-positive:. + """Things to do when a person has been tested and found (newly) to be HIV-positive:. * Consider if ART should be initiated, and schedule HSI if so. The person should not yet be on ART. """ @@ -1337,7 +1391,8 @@ def prob_viral_suppression(self, year, age_of_person): def stops_treatment(self, person_id): """Helper function that is called when someone stops being on ART. - Sets the flag for ART status. If the person was already on ART, it schedules a new AIDSEvent""" + Sets the flag for ART status. If the person was already on ART, it schedules a new AIDSEvent + """ df = self.sim.population.props @@ -1429,11 +1484,13 @@ def decide_whether_hiv_test_for_infant(self, mother_id, child_id) -> None: mother_id = mother_id child_id = child_id - if not df.at[child_id, 'hv_diagnosed'] and \ - df.at[mother_id, 'hv_diagnosed'] and ( - df.at[child_id, 'nb_pnc_check'] == 1) and ( - self.rng.random_sample() < self.parameters['prob_hiv_test_for_newborn_infant']): - self.sim.modules['HealthSystem'].schedule_hsi_event( + if ( + not df.at[child_id, "hv_diagnosed"] + and df.at[mother_id, "hv_diagnosed"] + and (df.at[child_id, "nb_pnc_check"] == 1) + and (self.rng.random_sample() < self.parameters["prob_hiv_test_for_newborn_infant"]) + ): + self.sim.modules["HealthSystem"].schedule_hsi_event( HSI_Hiv_TestAndRefer( person_id=child_id, module=self, @@ -1879,6 +1936,23 @@ def apply(self, person_id): ), date=date_of_aids_death, ) + # schedule hospital stay + beddays = self.sim.modules["Hiv"].rng.randint(low=14, high=20) + date_admission = date_of_aids_death - pd.DateOffset(days=beddays) + self.sim.modules["HealthSystem"].schedule_hsi_event( + hsi_event=HSI_Hiv_EndOfLifeCare( + person_id=person_id, + module=self.sim.modules["Hiv"], + beddays=beddays, + ), + priority=0, + topen=( + date_admission + if (date_admission > self.sim.date) + else self.sim.date + ), + tclose=date_of_aids_death, + ) else: # cause is active TB @@ -1888,6 +1962,23 @@ def apply(self, person_id): ), date=date_of_aids_death, ) + # schedule hospital stay + beddays = self.sim.modules["Hiv"].rng.randint(low=14, high=20) + date_admission = date_of_aids_death - pd.DateOffset(days=beddays) + self.sim.modules["HealthSystem"].schedule_hsi_event( + hsi_event=HSI_Hiv_EndOfLifeCare( + person_id=person_id, + module=self.sim.modules["Hiv"], + beddays=beddays, + ), + priority=0, + topen=( + date_admission + if (date_admission >= self.sim.date) + else self.sim.date + ), + tclose=date_of_aids_death, + ) class HivAidsDeathEvent(Event, IndividualScopeEventMixin): @@ -1954,16 +2045,22 @@ def __init__(self, module, person_id, cause): def apply(self, person_id): df = self.sim.population.props + p = self.sim.modules["Hiv"].parameters # Check person is_alive if not df.at[person_id, "is_alive"]: return if df.at[person_id, 'tb_on_treatment']: - prob = self.module.rng.rand() + + risk_of_death = p["aids_tb_treatment_adjustment"] + + if "CardioMetabolicDisorders" in self.sim.modules: + if df.at[person_id, "nc_diabetes"]: + risk_of_death *= self.sim.modules["Tb"].parameters["rr_death_diabetes"] # treatment adjustment reduces probability of death - if prob < self.sim.modules["Hiv"].parameters["aids_tb_treatment_adjustment"]: + if self.module.rng.rand() < risk_of_death: self.sim.modules["Demography"].do_death( individual_id=person_id, cause="AIDS_TB", @@ -1982,6 +2079,23 @@ def apply(self, person_id): ), date=date_of_aids_death, ) + # schedule hospital stay + beddays = self.module.rng.randint(low=14, high=20) + date_admission = date_of_aids_death - pd.DateOffset(days=beddays) + self.sim.modules["HealthSystem"].schedule_hsi_event( + hsi_event=HSI_Hiv_EndOfLifeCare( + person_id=person_id, + module=self.sim.modules["Hiv"], + beddays=beddays, + ), + priority=0, + topen=( + date_admission + if (date_admission >= self.sim.date) + else self.sim.date + ), + tclose=date_of_aids_death, + ) # aids-tb and not on tb treatment elif not df.at[person_id, 'tb_on_treatment']: @@ -2093,9 +2207,10 @@ def apply(self, person_id): # Health System Interactions (HSI) # --------------------------------------------------------------------------- + class HSI_Hiv_TestAndRefer(HSI_Event, IndividualScopeEventMixin): """ - The is the Test-and-Refer HSI. Individuals may seek an HIV test at any time. From this, they can be referred on to + This is the Test-and-Refer HSI. Individuals may seek an HIV test at any time. From this, they can be referred on to other services. This event is scheduled by: * the main event poll, @@ -2126,7 +2241,8 @@ def __init__( # Define the necessary information for an HSI self.TREATMENT_ID = "Hiv_Test" self.EXPECTED_APPT_FOOTPRINT = self.make_appt_footprint({"VCTNegative": 1}) - self.ACCEPTED_FACILITY_LEVEL = '1a' + self.ACCEPTED_FACILITY_LEVEL = "1a" + self.counter_for_test_not_available = 0 def apply(self, person_id, squeeze_factor): """Do the testing and referring to other services""" @@ -2241,13 +2357,21 @@ def apply(self, person_id, squeeze_factor): # Test was not possible, set blank footprint and schedule another test ACTUAL_APPT_FOOTPRINT = self.make_appt_footprint({"VCTNegative": 1}) - # repeat appt for HIV test - self.sim.modules["HealthSystem"].schedule_hsi_event( - HSI_Hiv_TestAndRefer(person_id=person_id, module=self.module, referred_from='HIV_test'), - topen=self.sim.date + pd.DateOffset(months=1), - tclose=None, - priority=0, - ) + # set cap for number of repeat tests + self.counter_for_test_not_available += 1 # The current appointment is included in the count. + + + if ( + self.counter_for_test_not_available + <= self.module.parameters["hiv_healthseekingbehaviour_cap"] + ): + # repeat appt for HIV test + self.sim.modules["HealthSystem"].schedule_hsi_event( + self, + topen=self.sim.date + pd.DateOffset(days=7), + tclose=None, + priority=0, + ) # Return the footprint. If it should be suppressed, return a blank footprint. if self.suppress_footprint: @@ -2262,7 +2386,7 @@ def __init__(self, module, person_id): self.TREATMENT_ID = "Hiv_Prevention_Circumcision" self.EXPECTED_APPT_FOOTPRINT = self.make_appt_footprint({"MaleCirc": 1}) - self.ACCEPTED_FACILITY_LEVEL = '1a' + self.ACCEPTED_FACILITY_LEVEL = '1b' self.number_of_occurrences = 0 def apply(self, person_id, squeeze_factor): @@ -2301,7 +2425,10 @@ def apply(self, person_id, squeeze_factor): ) else: # schedule repeating appt when consumables not available - if self.number_of_occurrences <= 3: + if ( + self.number_of_occurrences + <= self.module.parameters["hiv_healthseekingbehaviour_cap"] + ): self.sim.modules["HealthSystem"].schedule_hsi_event( self, topen=self.sim.date + DateOffset(weeks=1), @@ -2356,10 +2483,13 @@ def apply(self, person_id, squeeze_factor): ) else: - if self.repeat_visits <= 4: - # infant does not get NVP now but has repeat visit scheduled up to 5 times - df.at[person_id, "hv_is_on_prep"] = False + # infant does not get NVP now but has repeat visit scheduled up to 5 times + df.at[person_id, "hv_is_on_prep"] = False + if ( + self.repeat_visits + <= self.module.parameters["hiv_healthseekingbehaviour_cap"] + ): self.repeat_visits += 1 # Schedule repeat visit for one week's time @@ -2388,6 +2518,7 @@ def __init__(self, module, person_id): self.TREATMENT_ID = "Hiv_Prevention_Prep" self.EXPECTED_APPT_FOOTPRINT = self.make_appt_footprint({"PharmDispensing": 1, "VCTNegative": 1}) self.ACCEPTED_FACILITY_LEVEL = '1a' + self.counter_for_drugs_not_available = 0 def apply(self, person_id, squeeze_factor): """Start PrEP for this person; or continue them on PrEP for 3 more months""" @@ -2433,6 +2564,22 @@ def apply(self, person_id, squeeze_factor): # If PrEP is not available, the person will default and not be on PrEP df.at[person_id, "hv_is_on_prep"] = False + self.counter_for_drugs_not_available += ( + 1 # The current appointment is included in the count. + ) + + if ( + self.counter_for_drugs_not_available + <= self.module.parameters["hiv_healthseekingbehaviour_cap"] + ): + # Schedule repeat visit for one week's time + self.sim.modules["HealthSystem"].schedule_hsi_event( + self, + priority=1, + topen=self.sim.date + pd.DateOffset(days=7), + tclose=None, + ) + def never_ran(self): """This is called if this HSI was never run. Default the person to being off PrEP""" @@ -2465,8 +2612,8 @@ def apply(self, person_id, squeeze_factor): # check whether person had Rx at least 3 months ago and is now due repeat prescription # alternate routes into testing/tx may mean person already has recent ARV dispensation - if person['hv_date_last_ART'] >= ( - self.sim.date - pd.DateOffset(months=self.module.parameters['dispensation_period_months'])): + if person['hv_date_last_ART'] > ( + self.sim.date - pd.DateOffset(months=self.module.parameters['dispensation_period_months'])): return self.sim.modules["HealthSystem"].get_blank_appt_footprint() if art_status_at_beginning_of_hsi == "not": @@ -2506,57 +2653,58 @@ def apply(self, person_id, squeeze_factor): } logger.info(key='hiv_arv_NA', data=person_details_for_tx) - # As drugs were not available, the person will default to being off ART (...if they were on ART at the - # beginning of the HSI.) - # NB. If the person was not on ART at the beginning of the HSI, then there is no need to stop them (which - # causes a new AIDSOnsetEvent to be scheduled.) - self.counter_for_drugs_not_available += 1 # The current appointment is included in the count. + # As drugs were not available, the person will default to being off ART (...if they were on ART at the + # beginning of the HSI.) + # NB. If the person was not on ART at the beginning of the HSI, then there is no need to stop them (which + # causes a new AIDSOnsetEvent to be scheduled.) + self.counter_for_drugs_not_available += 1 # The current appointment is included in the count. - if art_status_at_beginning_of_hsi != "not": - self.module.stops_treatment(person_id) + 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: + p = self.module.parameters[ + "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 - self.sim.modules["HealthSystem"].schedule_hsi_event( - hsi_event=HSI_Hiv_StartOrContinueTreatment( - person_id=person_id, module=self.module, - facility_level_of_this_hsi="1a", - ), - topen=self.sim.date + pd.DateOffset(months=6), - priority=0, - ) + if self.module.rng.random_sample() >= p: - else: - # If person 'decides to' seek another treatment appointment, - # schedule a new HSI appointment for next month - # 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: - # repeat attempt for ARVs at level 1a + # add in referral straight back to tx + # if defaulting, seek another treatment appointment in 6 months self.sim.modules["HealthSystem"].schedule_hsi_event( hsi_event=HSI_Hiv_StartOrContinueTreatment( - person_id=person_id, module=self.module, - facility_level_of_this_hsi="1a" + person_id=person_id, + module=self.module, + facility_level_of_this_hsi="1a", ), - topen=self.sim.date + pd.DateOffset(months=1), + topen=self.sim.date + pd.DateOffset(months=6), priority=0, ) else: - # refer to higher facility level - self.sim.modules["HealthSystem"].schedule_hsi_event( - hsi_event=HSI_Hiv_StartOrContinueTreatment( - person_id=person_id, module=self.module, - facility_level_of_this_hsi="2" - ), - topen=self.sim.date + pd.DateOffset(days=1), - priority=0, - ) + # If person 'decides to' seek another treatment appointment, + # schedule a new HSI appointment for next month + # 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: + # repeat attempt for ARVs at level 1a + self.sim.modules["HealthSystem"].schedule_hsi_event( + self, + topen=self.sim.date + pd.DateOffset(months=1), + priority=0, + ) + + else: + # refer to higher facility level + self.sim.modules["HealthSystem"].schedule_hsi_event( + hsi_event=HSI_Hiv_StartOrContinueTreatment( + person_id=person_id, + module=self.module, + facility_level_of_this_hsi="2", + ), + topen=self.sim.date + pd.DateOffset(days=1), + priority=0, + ) # also screen for tb if "Tb" in self.sim.modules: @@ -2732,6 +2880,42 @@ def EXPECTED_APPT_FOOTPRINT(self): return self.make_appt_footprint({"EstNonCom": 1}) # Adult already on treatment +class HSI_Hiv_EndOfLifeCare(HSI_Event, IndividualScopeEventMixin): + """ + this is a hospital stay for terminally-ill patients with AHD + it does not affect disability weight or probability of death + no consumables are logged but health system capacity (HR) is allocated + there are no consequences if hospital bed is not available as person has scheduled death + already within 2 weeks + """ + + def __init__(self, module, person_id, beddays=17): + 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" + + self.beddays = beddays + self.BEDDAYS_FOOTPRINT = self.make_beddays_footprint({"general_bed": self.beddays}) + + def apply(self, person_id, squeeze_factor): + df = self.sim.population.props + hs = self.sim.modules["HealthSystem"] + + if not df.at[person_id, "is_alive"]: + return hs.get_blank_appt_footprint() + + if df.at[person_id, "hv_art"] == "virally_suppressed": + return hs.get_blank_appt_footprint() + + logger.debug( + key="message", + data=f"HSI_Hiv_EndOfLifeCare: inpatient admission for {person_id}", + ) + + # --------------------------------------------------------------------------- # Logging # --------------------------------------------------------------------------- diff --git a/src/tlo/methods/malaria.py b/src/tlo/methods/malaria.py index 84dcc3cff1..7731ea923c 100644 --- a/src/tlo/methods/malaria.py +++ b/src/tlo/methods/malaria.py @@ -49,9 +49,7 @@ def __init__(self, name=None, resourcefilepath=None): 'Contraception', 'Demography', 'HealthSystem', 'SymptomManager' } - OPTIONAL_INIT_DEPENDENCIES = {'HealthBurden'} - - ADDITIONAL_DEPENDENCIES = {'Hiv', 'Tb'} + OPTIONAL_INIT_DEPENDENCIES = {'HealthBurden', 'Hiv'} METADATA = { Metadata.DISEASE_MODULE, @@ -194,7 +192,9 @@ def __init__(self, name=None, resourcefilepath=None): } PROPERTIES = { - 'ma_is_infected': Property(Types.BOOL, 'Current status of malaria'), + 'ma_is_infected': Property( + Types.BOOL, 'Current status of malaria, infected with malaria parasitaemia' + ), 'ma_date_infected': Property(Types.DATE, 'Date of latest infection'), 'ma_date_symptoms': Property( Types.DATE, 'Date of symptom start for clinical infection' @@ -411,9 +411,11 @@ def malaria_poll2(self, population): def _draw_incidence_for(_col, _where): """a helper function to perform random draw for selected individuals on column of probabilities""" # create an index from the individuals to lookup entries in the current incidence table - district_age_subset = df.loc[_where, ['district_num_of_residence', 'ma_age_edited']] - district_age_subset.set_index(district_age_subset.columns.to_list(), inplace=True) - district_age_lookup = district_age_subset.index + district_age_lookup = ( + df[_where] + .set_index(['district_num_of_residence', 'ma_age_edited']) + .index + ) # get the monthly incidence probabilities for these individuals monthly_prob = curr_inc.loc[district_age_lookup, _col] # update the index so it's the same as the original population dataframe for these individuals @@ -433,7 +435,7 @@ def _draw_incidence_for(_col, _where): df.loc[_where] ) - random_draw = rng.random_sample(_where.sum()) < monthly_prob * individual_risk + random_draw = rng.random_sample(_where.sum()) < (monthly_prob * individual_risk) selected = _where & random_draw @@ -448,7 +450,8 @@ def _draw_incidence_for(_col, _where): df.loc[alive_over_one, 'ma_age_edited'] = df.loc[alive_over_one, 'age_years'].astype(float) # select new infections - alive_uninfected = alive & ~df.ma_is_infected + # eligible: uninfected or asym + alive_uninfected = alive & df.ma_inf_type.isin(['none', 'asym']) now_infected = _draw_incidence_for('monthly_prob_inf', alive_uninfected) df.loc[now_infected, 'ma_inf_type'] = 'asym' @@ -1305,7 +1308,7 @@ def apply(self, population): * assigns symptoms * schedules rdt * cures people currently on treatment for malaria - * clears symptoms for those not on treatment + * clears symptoms for those not on treatment but self-cured * clears parasites if treated """ @@ -1388,14 +1391,22 @@ def apply(self, population): # select people with clinical malaria and treatment for at least 5 days # if treated, will clear symptoms and parasitaemia # this will also clear parasitaemia for asymptomatic cases picked up by routine rdt - clinical_and_treated = df.index[df.is_alive & - (df.ma_date_tx < (self.sim.date - DateOffset(days=5))) & - (df.ma_inf_type == 'clinical')] + random_draw = self.module.rng.random_sample(size=len(df)) + + clinical_and_treated = df.index[ + df.is_alive + & (df.ma_date_tx < (self.sim.date - DateOffset(days=5))) + & (df.ma_inf_type == 'clinical') + & (random_draw < p['prob_of_treatment_success']) + ] # select people with severe malaria and treatment for at least 7 days - severe_and_treated = df.index[df.is_alive & - (df.ma_date_tx < (self.sim.date - DateOffset(days=7))) & - (df.ma_inf_type == 'severe')] + severe_and_treated = df.index[ + df.is_alive + & (df.ma_date_tx < (self.sim.date - DateOffset(days=7))) + & (df.ma_inf_type == 'severe') + & (random_draw < p['prob_of_treatment_success']) + ] # create list of all cases to be resolved through treatment infections_to_clear = sorted(set(clinical_and_treated).union(severe_and_treated)) @@ -1409,13 +1420,14 @@ def apply(self, population): df.loc[infections_to_clear, 'ma_is_infected'] = False df.loc[infections_to_clear, 'ma_inf_type'] = 'none' - # UNTREATED - # if not treated, self-cure occurs after 6 days of symptoms + # UNTREATED or TREATMENT FAILURE + # if not treated or treatment failed, self-cure occurs after 6 days of symptoms # but parasites remain in blood - clinical_not_treated = df.index[df.is_alive & - (df.ma_inf_type == 'clinical') & - (df.ma_date_symptoms < (self.sim.date - DateOffset(days=6))) & - (df.ma_tx == 'none')] + clinical_not_treated = df.index[ + df.is_alive + & (df.ma_inf_type == 'clinical') + & (df.ma_date_symptoms < (self.sim.date - DateOffset(days=6))) + ] self.sim.modules['SymptomManager'].clear_symptoms( person_id=clinical_not_treated, disease_module=self.module diff --git a/src/tlo/methods/other_adult_cancers.py b/src/tlo/methods/other_adult_cancers.py index 1e8d5c1e22..3bea8933e6 100644 --- a/src/tlo/methods/other_adult_cancers.py +++ b/src/tlo/methods/other_adult_cancers.py @@ -43,7 +43,7 @@ def __init__(self, name=None, resourcefilepath=None): INIT_DEPENDENCIES = {'Demography', 'HealthSystem', 'SymptomManager'} - OPTIONAL_INIT_DEPENDENCIES = {'HealthBurden'} + OPTIONAL_INIT_DEPENDENCIES = {'HealthBurden', 'Hiv'} METADATA = { Metadata.DISEASE_MODULE, @@ -121,6 +121,9 @@ def __init__(self, name=None, resourcefilepath=None): "rr_site_confined_agege70": Parameter( Types.REAL, "rate ratio for site-confined other_adult cancer for age ge 70" ), + "rr_site_confined_hiv": Parameter( + Types.REAL, "rate ratio for site-confined other_adult_cancer if infected with HIV" + ), "r_local_ln_site_confined_other_adult_ca": Parameter( Types.REAL, "probabilty per 3 months of local ln other_adult cancer amongst people with site confined", @@ -392,15 +395,26 @@ def initialise_simulation(self, sim): p = self.parameters lm = self.linear_models_for_progession_of_oac_status - lm['site_confined'] = LinearModel( - LinearModelType.MULTIPLICATIVE, - p['r_site_confined_none'], + predictors = [ Predictor('age_years', conditions_are_mutually_exclusive=True) .when('.between(30,49)', p['rr_site_confined_age3049']) .when('.between(50,69)', p['rr_site_confined_age5069']) .when('.between(0,14)', 0.0) .when('.between(70,120)', p['rr_site_confined_agege70']), Predictor('oac_status').when('none', 1.0).otherwise(0.0) + ] + + conditional_predictors = [ + Predictor().when( + 'hv_inf & ' + '(hv_art != "on_VL_suppressed")', + p["rr_site_confined_hiv"]), + ] if "Hiv" in self.sim.modules else [] + + lm['site_confined'] = LinearModel( + LinearModelType.MULTIPLICATIVE, + p['r_site_confined_none'], + *(predictors + conditional_predictors) ) lm['local_ln'] = LinearModel( diff --git a/src/tlo/methods/schisto.py b/src/tlo/methods/schisto.py index 07922f99ca..8ccc593601 100644 --- a/src/tlo/methods/schisto.py +++ b/src/tlo/methods/schisto.py @@ -293,7 +293,7 @@ def _get_disability_weight(self) -> dict: def _get_item_code_for_praziquantel(self) -> int: """Look-up the item code for Praziquantel""" - return self.sim.modules['HealthSystem'].get_item_code_from_item_name("Praziquantel, 600 mg (donated)") + return self.sim.modules['HealthSystem'].get_item_code_from_item_name("Praziquantel 600mg_1000_CMST") def _schedule_mda_events(self) -> None: """Schedule MDA events, historical and prognosed.""" diff --git a/src/tlo/methods/tb.py b/src/tlo/methods/tb.py index d40e04a048..b769e60fb9 100644 --- a/src/tlo/methods/tb.py +++ b/src/tlo/methods/tb.py @@ -15,7 +15,6 @@ from tlo.methods import Metadata, hiv from tlo.methods.causes import Cause from tlo.methods.dxmanager import DxTest -from tlo.methods.healthsystem import HealthSystemChangeParameters from tlo.methods.hsi_event import HSI_Event from tlo.methods.symptommanager import Symptom from tlo.util import random_date @@ -49,7 +48,13 @@ def __init__(self, name=None, resourcefilepath=None, run_with_checks=False): # initialise empty dict with set keys self.tb_outputs = {k: [] for k in keys} - INIT_DEPENDENCIES = {"Demography", "HealthSystem", "Lifestyle", "SymptomManager", "Epi"} + INIT_DEPENDENCIES = { + "Demography", + "HealthSystem", + "Lifestyle", + "SymptomManager", + "Epi", + } OPTIONAL_INIT_DEPENDENCIES = {"HealthBurden", "Hiv"} @@ -124,10 +129,9 @@ def __init__(self, name=None, resourcefilepath=None, run_with_checks=False): "none", "tb_tx_adult", "tb_tx_child", - "tb_tx_child_shorter", "tb_retx_adult", "tb_retx_child", - "tb_mdrtx" + "tb_mdrtx", ], description="current tb treatment regimen", ), @@ -165,9 +169,6 @@ def __init__(self, name=None, resourcefilepath=None, run_with_checks=False): "rr_tb_child": Parameter( Types.REAL, "relative risk of tb infection if under 16 years of age" ), - "rr_bcg_inf": Parameter( - Types.REAL, "relative risk of tb infection with bcg vaccination" - ), "monthly_prob_relapse_tx_complete": Parameter( Types.REAL, "monthly probability of relapse once treatment complete" ), @@ -181,6 +182,9 @@ def __init__(self, name=None, resourcefilepath=None, run_with_checks=False): "rr_relapse_hiv": Parameter( Types.REAL, "relative risk of relapse for HIV-positive people" ), + "rr_relapse_diabetes": Parameter( + Types.REAL, "relative risk of relapse for people with diabetes (treated/untreated)" + ), # ------------------ active disease ------------------ # "scaling_factor_WHO": Parameter( Types.REAL, @@ -216,6 +220,9 @@ def __init__(self, name=None, resourcefilepath=None, run_with_checks=False): "death_rate_adult_treated": Parameter( Types.REAL, "probability of death in adult aged >=15 years with treated tb" ), + "rr_death_diabetes": Parameter( + Types.REAL, "additional risk of death if person has diabetes (treated/untreated)" + ), # ------------------ progression to active disease ------------------ # "rr_tb_bcg": Parameter( Types.REAL, @@ -239,9 +246,9 @@ def __init__(self, name=None, resourcefilepath=None, run_with_checks=False): "rr_tb_obese": Parameter( Types.REAL, "relative risk of progression to active disease if obese" ), - "rr_tb_diabetes1": Parameter( + "rr_tb_diabetes": Parameter( Types.REAL, - "relative risk of progression to active disease with type 1 diabetes", + "relative risk of progression to active disease with any type diabetes", ), "rr_tb_alcohol": Parameter( Types.REAL, @@ -316,9 +323,6 @@ def __init__(self, name=None, resourcefilepath=None, run_with_checks=False): "prob_tx_success_5_14": Parameter( Types.REAL, "Probability of treatment success for children aged 5-14 years" ), - "prob_tx_success_shorter": Parameter( - Types.REAL, "Probability of treatment success for children aged <16 years on shorter regimen" - ), # ------------------ testing rates ------------------ # "rate_testing_general_pop": Parameter( Types.REAL, @@ -340,9 +344,6 @@ def __init__(self, name=None, resourcefilepath=None, run_with_checks=False): "mdr_treatment_length": Parameter( Types.REAL, "length of treatment for mdr-tb in months" ), - "child_shorter_treatment_length": Parameter( - Types.REAL, "length of treatment for shorter paediatric regimen in months" - ), "prob_retained_ipt_6_months": Parameter( Types.REAL, "probability of being retained on IPT every 6 months if still eligible", @@ -355,14 +356,6 @@ def __init__(self, name=None, resourcefilepath=None, run_with_checks=False): Types.INT, "year from which IPT is available for paediatric contacts of diagnosed active TB cases", ), - "scenario": Parameter( - Types.INT, - "integer value labelling the scenario to be run: default is 0" - ), - "scenario_start_date": Parameter( - Types.DATE, - "date from which different scenarios are run" - ), "first_line_test": Parameter( Types.STRING, "name of first test to be used for TB diagnosis" @@ -371,18 +364,17 @@ def __init__(self, name=None, resourcefilepath=None, run_with_checks=False): Types.STRING, "name of second test to be used for TB diagnosis" ), - "probability_access_to_xray": Parameter( - Types.REAL, - "probability a person will have access to chest x-ray" + "tb_healthseekingbehaviour_cap": Parameter( + Types.INT, + "number of repeat visits assumed for healthcare services", ), - "prob_tb_referral_in_generic_hsi": Parameter( - Types.REAL, - "probability of referral to TB screening HSI if presenting with TB-related symptoms" + "data_end": Parameter( + Types.INT, + "last year for which data are available", ), - # ------------------ scale-up parameters for scenario analysis ------------------ # - "scaleup_parameters": Parameter( - Types.DATA_FRAME, - "list of parameters and values changed in scenario analysis", + "length_of_inpatient_stay_if_terminal": Parameter( + Types.LIST, + "length of inpatient stay for end-of-life TB patients", ), } @@ -414,8 +406,6 @@ def read_parameters(self, data_folder): p["ipt_coverage"] = workbook["ipt_coverage"] - p["scaleup_parameters"] = workbook["scaleup_parameters"] - self.district_list = ( self.sim.modules["Demography"] .parameters["pop_2010"]["District"] @@ -541,16 +531,14 @@ def pre_initialise_population(self): ), ] conditional_predictors = [ - Predictor("nc_diabetes").when(True, p['rr_tb_diabetes1']), - ] if "cardio_metabolic_disorders" in self.sim.modules else [] + Predictor("nc_diabetes").when(True, p['rr_tb_diabetes']), + ] if "CardioMetabolicDisorders" in self.sim.modules else [] self.lm["active_tb"] = LinearModel.multiplicative( *(predictors + conditional_predictors)) # risk of relapse <2 years following treatment - self.lm["risk_relapse_2yrs"] = LinearModel( - LinearModelType.MULTIPLICATIVE, - p["monthly_prob_relapse_tx_complete"], + predictors = [ Predictor("hv_inf").when(True, p["rr_relapse_hiv"]), Predictor("tb_treatment_failure") .when(True, (p["monthly_prob_relapse_tx_incomplete"] / p["monthly_prob_relapse_tx_complete"])), @@ -562,7 +550,16 @@ def pre_initialise_population(self): 'tb_on_ipt & ' 'age_years > 15', p["rr_ipt_adult"]), - ) + ] + + conditional_predictors = [ + Predictor("nc_diabetes").when(True, p['rr_relapse_diabetes']), + ] if "CardioMetabolicDisorders" in self.sim.modules else [] + + self.lm["risk_relapse_2yrs"] = LinearModel( + LinearModelType.MULTIPLICATIVE, + p["monthly_prob_relapse_tx_complete"], + *(predictors + conditional_predictors)) # risk of relapse if >=2 years post treatment self.lm["risk_relapse_late"] = LinearModel( @@ -580,9 +577,7 @@ def pre_initialise_population(self): ) # probability of death - self.lm["death_rate"] = LinearModel( - LinearModelType.MULTIPLICATIVE, - 1, + predictors = [ Predictor().when( "(tb_on_treatment == True) & " "(age_years <=4)", @@ -608,7 +603,14 @@ def pre_initialise_population(self): "(tb_smear == False)", p["death_rate_smear_neg_untreated"], ), - ) + ] + + conditional_predictors = [ + Predictor("nc_diabetes").when(True, p['rr_death_diabetes']), + ] if "CardioMetabolicDisorders" in self.sim.modules else [] + + self.lm["death_rate"] = LinearModel.multiplicative( + *(predictors + conditional_predictors)) def send_for_screening_general(self, population): @@ -749,10 +751,6 @@ def get_consumables_for_dx_and_tx(self): self.item_codes_for_consumables_required['tb_tx_child'] = \ hs.get_item_code_from_item_name("Cat. I & III Patient Kit B") - # child treatment - primary, shorter regimen - self.item_codes_for_consumables_required['tb_tx_child_shorter'] = \ - hs.get_item_code_from_item_name("Cat. I & III Patient Kit B") - # adult treatment - secondary self.item_codes_for_consumables_required['tb_retx_adult'] = \ hs.get_item_code_from_item_name("Cat. II Patient Kit A1") @@ -836,22 +834,19 @@ def initialise_population(self, population): def initialise_simulation(self, sim): """ * 1) Schedule the regular TB events - * 2) Schedule the scenario change + * 2) schedule logging * 3) Define the DxTests and treatment options """ # 1) Regular events sim.schedule_event(TbActiveEvent(self), sim.date) - sim.schedule_event(TbTreatmentAndRelapseEvents(self), sim.date) + sim.schedule_event(TbRegularEvents(self), sim.date) sim.schedule_event(TbSelfCureEvent(self), sim.date) sim.schedule_event(TbActiveCasePoll(self), sim.date + DateOffset(years=1)) - # log at the end of the year + # 2) log at the end of the year sim.schedule_event(TbLoggingEvent(self), sim.date + DateOffset(years=1)) - # 2) Scenario change - sim.schedule_event(ScenarioSetupEvent(self), self.parameters["scenario_start_date"]) - # 3) Define the DxTests and get the consumables required self.get_consumables_for_dx_and_tx() @@ -1164,22 +1159,9 @@ def end_treatment(self, population): ) ].index - # ---------------------- treatment end: shorter paediatric regimen ---------------------- # - # end treatment for paediatric cases on 4 month regimen - end_tx_shorter_idx = df.loc[ - df.is_alive - & df.tb_on_treatment - & (df.tb_treatment_regimen == "tb_tx_child_shorter") - & ( - now - > (df.tb_date_treated + pd.DateOffset(months=p["child_shorter_treatment_length"])) - ) - ].index - # join indices end_tx_idx = end_ds_tx_idx.union(end_ds_retx_idx) end_tx_idx = end_tx_idx.union(end_mdr_tx_idx) - end_tx_idx = end_tx_idx.union(end_tx_shorter_idx) # ---------------------- treatment failure ---------------------- # # sample some to have treatment failure @@ -1200,13 +1182,6 @@ def end_treatment(self, population): & (random_var < (1 - p["prob_tx_success_5_14"])) ].index - # children aged <16 and on shorter regimen - ds_tx_failure_shorter_idx = df.loc[ - (df.index.isin(end_tx_shorter_idx)) - & (df.age_years < 16) - & (random_var < (1 - p["prob_tx_success_shorter"])) - ].index - # adults ds-tb ds_tx_failure_adult_idx = df.loc[ (df.index.isin(end_ds_tx_idx)) @@ -1225,7 +1200,6 @@ def end_treatment(self, population): (df.index.isin(end_mdr_tx_idx)) & (df.tb_strain == "mdr") & (random_var < (1 - p["prob_tx_success_mdr"])) - ].index # join indices of failing cases together @@ -1234,7 +1208,6 @@ def end_treatment(self, population): ( ds_tx_failure0_4_idx, ds_tx_failure5_14_idx, - ds_tx_failure_shorter_idx, ds_tx_failure_adult_idx, failure_in_mdr_with_ds_tx_idx, failure_due_to_mdr_idx, @@ -1329,118 +1302,6 @@ def is_subset(col_for_set, col_for_subset): # # --------------------------------------------------------------------------- # # TB infection event # # --------------------------------------------------------------------------- -class ScenarioSetupEvent(RegularEvent, PopulationScopeEventMixin): - """ This event exists to change parameters or functions - depending on the scenario for projections which has been set - * scenario 0 is the default which uses baseline parameters - * scenario 1 achieves all program targets with consumables constraints - * scenario 2 achieves all program targets without consumables constraints - It only occurs once at param: scenario_start_date, - called by initialise_simulation - """ - - def __init__(self, module): - super().__init__(module, frequency=DateOffset(years=100)) - - def apply(self, population): - - p = self.module.parameters - scenario = p["scenario"] - scaled_params = p["scaleup_parameters"] - - logger.debug( - key="message", data=f"ScenarioSetupEvent: scenario {scenario}" - ) - - # baseline scenario 0: no change to parameters/functions - if scenario == 0: - return - - # scenario 1 or 2 scale-up all HIV/TB program activities - if scenario > 0: - # HIV - # reduce risk of HIV - applies to whole adult population - self.sim.modules["Hiv"].parameters["beta"] = self.sim.modules["Hiv"].parameters["beta"] * scaled_params.loc[ - scaled_params.parameter == "hiv_beta", "value"].values[0] - - # increase PrEP coverage for FSW after HIV test - self.sim.modules["Hiv"].parameters["prob_prep_for_fsw_after_hiv_test"] = scaled_params.loc[ - scaled_params.parameter == "prob_prep_for_fsw_after_hiv_test", "value"].values[0] - - # prep poll for AGYW - target to the highest risk - # increase retention to 75% for FSW and AGYW - self.sim.modules["Hiv"].parameters["prob_prep_for_agyw"] = scaled_params.loc[ - scaled_params.parameter == "prob_prep_for_agyw", "value"].values[0] - self.sim.modules["Hiv"].parameters[ - "probability_of_being_retained_on_prep_every_3_months"] = scaled_params.loc[ - scaled_params.parameter == "probability_of_being_retained_on_prep_every_3_months", "value"].values[0] - - # increase probability of VMMC after hiv test - self.sim.modules["Hiv"].parameters["prob_circ_after_hiv_test"] = scaled_params.loc[ - scaled_params.parameter == "prob_circ_after_hiv_test", "value"].values[0] - - # increase testing/diagnosis rates, default 2020 0.03/0.25 -> 93% dx - self.sim.modules["Hiv"].parameters["hiv_testing_rates"]["annual_testing_rate_children"] = scaled_params.loc[ - scaled_params.parameter == "annual_testing_rate_children", "value"].values[0] - self.sim.modules["Hiv"].parameters["hiv_testing_rates"]["annual_testing_rate_adults"] = scaled_params.loc[ - scaled_params.parameter == "annual_testing_rate_adults", "value"].values[0] - - # ANC testing - value for mothers and infants testing - self.sim.modules["Hiv"].parameters["prob_hiv_test_at_anc_or_delivery"] = scaled_params.loc[ - scaled_params.parameter == "prob_hiv_test_at_anc_or_delivery", "value"].values[0] - self.sim.modules["Hiv"].parameters["prob_hiv_test_for_newborn_infant"] = scaled_params.loc[ - scaled_params.parameter == "prob_hiv_test_for_newborn_infant", "value"].values[0] - - # prob ART start if dx, this is already 95% at 2020 - # self.sim.modules["Hiv"].parameters["prob_start_art_after_hiv_test"] = scaled_params.loc[ - # scaled_params.parameter == - # "prob_start_art_after_hiv_test", "value"].values[0] - - # viral suppression rates - # adults already at 95% by 2020 - # change all column values - self.sim.modules["Hiv"].parameters["prob_start_art_or_vs"]["virally_suppressed_on_art"] = scaled_params.loc[ - scaled_params.parameter == "virally_suppressed_on_art", "value"].values[0] - - # TB - # use NTP treatment rates - self.sim.modules["Tb"].parameters["rate_testing_active_tb"]["treatment_coverage"] = scaled_params.loc[ - scaled_params.parameter == "tb_treatment_coverage", "value"].values[0] - - # increase tb treatment success rates - self.sim.modules["Tb"].parameters["prob_tx_success_ds"] = scaled_params.loc[ - scaled_params.parameter == "tb_prob_tx_success_ds", "value"].values[0] - self.sim.modules["Tb"].parameters["prob_tx_success_mdr"] = scaled_params.loc[ - scaled_params.parameter == "tb_prob_tx_success_mdr", "value"].values[0] - self.sim.modules["Tb"].parameters["prob_tx_success_0_4"] = scaled_params.loc[ - scaled_params.parameter == "tb_prob_tx_success_0_4", "value"].values[0] - self.sim.modules["Tb"].parameters["prob_tx_success_5_14"] = scaled_params.loc[ - scaled_params.parameter == "tb_prob_tx_success_5_14", "value"].values[0] - self.sim.modules["Tb"].parameters["prob_tx_success_shorter"] = scaled_params.loc[ - scaled_params.parameter == "tb_prob_tx_success_shorter", "value"].values[0] - - # change first-line testing for TB to xpert - p["first_line_test"] = scaled_params.loc[ - scaled_params.parameter == "first_line_test", "value"].values[0] - p["second_line_test"] = scaled_params.loc[ - scaled_params.parameter == "second_line_test", "value"].values[0] - - # increase coverage of IPT - p["ipt_coverage"]["coverage_plhiv"] = scaled_params.loc[ - scaled_params.parameter == "ipt_coverage_plhiv", "value"].values[0] - p["ipt_coverage"]["coverage_paediatric"] = scaled_params.loc[ - scaled_params.parameter == "ipt_coverage_paediatric", "value"].values[0] - - # remove consumables constraints, all cons available - if scenario == 2: - # list only things that change: constraints on consumables - new_parameters = { - 'cons_availability': 'all', - } - self.sim.schedule_event( - HealthSystemChangeParameters( - self.sim.modules['HealthSystem'], parameters=new_parameters), - self.sim.date) class TbActiveCasePoll(RegularEvent, PopulationScopeEventMixin): @@ -1455,20 +1316,26 @@ def __init__(self, module): def apply(self, population): p = self.module.parameters + + current_year = min(self.sim.date.year, p["data_end"]) + inc_estimates = p["who_incidence_estimates"] incidence_year = (inc_estimates.loc[ - (inc_estimates.year == self.sim.date.year), "incidence_per_100k" + (inc_estimates.year == current_year), "incidence_per_100k" ].values[0]) / 100000 prop_untreated_ds = self.module.calculate_untreated_proportion(population, strain="ds") prop_untreated_mdr = self.module.calculate_untreated_proportion(population, strain="mdr") - scaled_incidence_ds = incidence_year * \ - p["scaling_factor_WHO"] * prop_untreated_ds - scaled_incidence_mdr = incidence_year * \ - p["prop_mdr2010"] * \ - p["scaling_factor_WHO"] * \ - prop_untreated_mdr + scaled_incidence_ds = ( + incidence_year * p["scaling_factor_WHO"] * prop_untreated_ds + ) + scaled_incidence_mdr = ( + incidence_year + * p["prop_mdr2010"] + * p["scaling_factor_WHO"] + * prop_untreated_mdr + ) # transmission ds-tb self.module.assign_active_tb(population, strain="ds", incidence=scaled_incidence_ds) @@ -1477,8 +1344,8 @@ def apply(self, population): self.module.assign_active_tb(population, strain="mdr", incidence=scaled_incidence_mdr) -class TbTreatmentAndRelapseEvents(RegularEvent, PopulationScopeEventMixin): - """ This event runs each month and calls three functions: +class TbRegularEvents(RegularEvent, PopulationScopeEventMixin): + """This event runs each month and calls three functions: * scheduling TB screening for the general population * ending treatment if end of treatment regimen has been reached * determining who will relapse after a primary infection @@ -1549,7 +1416,7 @@ def apply(self, population): active_and_hiv = df.loc[ (df.index.isin(active_idx) & df.hv_inf)].index - # higher probability of being smear positive than HIV- + # lower probability of being smear positive than HIV- smear_pos = ( rng.random_sample(len(active_and_hiv)) < p["prop_smear_positive_hiv"] ) @@ -1568,10 +1435,12 @@ def apply(self, population): # if Hiv not registered, give HIV+ person same time to death as HIV- for person_id in active_and_hiv: date_of_tb_death = self.sim.date + pd.DateOffset( - months=int(rng.uniform(low=1, high=6)) + months=int(rng.uniform(low=1, high=5)) ) self.sim.schedule_event( - event=TbDeathEvent(person_id=person_id, module=self.module, cause="AIDS_TB"), + event=TbDecideDeathEvent( + person_id=person_id, module=self.module, cause="AIDS_TB" + ), date=date_of_tb_death, ) @@ -1586,7 +1455,9 @@ def apply(self, population): months=int(rng.uniform(low=1, high=6)) ) self.sim.schedule_event( - event=TbDeathEvent(person_id=person_id, module=self.module, cause="TB"), + event=TbDecideDeathEvent( + person_id=person_id, module=self.module, cause="TB" + ), date=date_of_tb_death, ) @@ -1597,11 +1468,12 @@ def apply(self, population): active_testing_rates = p["rate_testing_active_tb"] # change to NTP testing rates - current_active_testing_rate = active_testing_rates.loc[ - ( - active_testing_rates.year == year), - "treatment_coverage"].values[ - 0] / 100 + current_active_testing_rate = ( + active_testing_rates.loc[ + (active_testing_rates.year == year), "treatment_coverage" + ].values[0] + / 100 + ) # multiply testing rate by average treatment availability to match treatment coverage current_active_testing_rate = current_active_testing_rate * (1 / 0.6) @@ -1612,7 +1484,8 @@ def apply(self, population): # would only be screened if have symptoms for >= 14 days # sample some of active_idx to go for screening screen_active_idx = df.loc[ - (df.index.isin(active_idx) & (random_draw < current_active_testing_rate))].index + (df.index.isin(active_idx) & (random_draw < current_active_testing_rate)) + ].index # TB screening checks for symptoms lasting at least 14 days, so add delay for person in screen_active_idx: @@ -1692,7 +1565,7 @@ def apply(self, population): class HSI_Tb_ScreeningAndRefer(HSI_Event, IndividualScopeEventMixin): """ - The is the Screening-and-Refer HSI. + This is the Screening-and-Refer HSI. A positive outcome from symptom-based screening will prompt referral to tb tests (sputum/xpert/xray) no consumables are required for screening (4 clinical questions) @@ -1712,16 +1585,17 @@ class HSI_Tb_ScreeningAndRefer(HSI_Event, IndividualScopeEventMixin): * give IPT for paediatric contacts of diagnosed case """ - def __init__(self, module, person_id, suppress_footprint=False): + def __init__(self, module, person_id, suppress_footprint=False, facility_level='1a'): super().__init__(module, person_id=person_id) assert isinstance(module, Tb) + self.facility_level = facility_level assert isinstance(suppress_footprint, bool) self.suppress_footprint = suppress_footprint self.TREATMENT_ID = "Tb_Test_Screening" self.EXPECTED_APPT_FOOTPRINT = self.make_appt_footprint({"Over5OPD": 1}) - self.ACCEPTED_FACILITY_LEVEL = '1a' + self.ACCEPTED_FACILITY_LEVEL = "1a" if self.facility_level == "1a" else "2" def apply(self, person_id, squeeze_factor): """Do the screening and referring to next tests""" @@ -1764,13 +1638,17 @@ def apply(self, person_id, squeeze_factor): if not person["hv_diagnosed"] or (person["hv_last_test_date"] >= (now - DateOffset(days=7))): self.sim.modules["HealthSystem"].schedule_hsi_event( hsi_event=hiv.HSI_Hiv_TestAndRefer( - person_id=person_id, module=self.sim.modules["Hiv"], referred_from='Tb' + person_id=person_id, + module=self.sim.modules["Hiv"], + referred_from="Tb", ), priority=1, topen=now, tclose=None, ) + # ------------------------- x-ray for children ------------------------- # + # child under 5 -> chest x-ray, but access is limited # if xray not available, HSI_Tb_Xray_level1b will refer if person["age_years"] < 5: @@ -1787,6 +1665,8 @@ def apply(self, person_id, squeeze_factor): ) test_result = False # to avoid calling a clinical diagnosis + # ------------------------- select test for adults ------------------------- # + # for all presumptive cases over 5 years of age else: # this selects a test for the person @@ -1801,35 +1681,58 @@ def apply(self, person_id, squeeze_factor): # relevant test depends on smear status (changes parameters on sensitivity/specificity if smear_status: - test_result = self.sim.modules["HealthSystem"].dx_manager.run_dx_test( + test_result = self.sim.modules[ + "HealthSystem" + ].dx_manager.run_dx_test( dx_tests_to_run="tb_sputum_test_smear_positive", hsi_event=self ) else: # if smear-negative, sputum smear should always return negative # run the dx test to log the consumable - test_result = self.sim.modules["HealthSystem"].dx_manager.run_dx_test( + test_result = self.sim.modules[ + "HealthSystem" + ].dx_manager.run_dx_test( dx_tests_to_run="tb_sputum_test_smear_negative", hsi_event=self ) # if negative, check for presence of all symptoms (clinical diagnosis) if all(x in self.module.symptom_list for x in persons_symptoms): - test_result = self.sim.modules["HealthSystem"].dx_manager.run_dx_test( + test_result = self.sim.modules[ + "HealthSystem" + ].dx_manager.run_dx_test( dx_tests_to_run="tb_clinical", hsi_event=self ) elif test == "xpert": - ACTUAL_APPT_FOOTPRINT = self.make_appt_footprint( - {"Over5OPD": 1} - ) - # relevant test depends on smear status (changes parameters on sensitivity/specificity - if smear_status: - test_result = self.sim.modules["HealthSystem"].dx_manager.run_dx_test( - dx_tests_to_run="tb_xpert_test_smear_positive", hsi_event=self + + # this can only be performed at level 1b/2, refer if necessary + if self.facility_level == "1a": + self.sim.modules["HealthSystem"].schedule_hsi_event( + hsi_event=HSI_Tb_ScreeningAndRefer( + person_id=person_id, module=self.module, facility_level="2" + ), + topen=self.sim.date + DateOffset(days=1), + tclose=None, + priority=0, ) - # for smear-negative people + return self.make_appt_footprint({"Over5OPD": 1}) + else: - test_result = self.sim.modules["HealthSystem"].dx_manager.run_dx_test( - dx_tests_to_run="tb_xpert_test_smear_negative", hsi_event=self - ) + if smear_status: + # relevant test depends on smear status (changes parameters on sensitivity/specificity + test_result = self.sim.modules[ + "HealthSystem" + ].dx_manager.run_dx_test( + dx_tests_to_run="tb_xpert_test_smear_positive", + hsi_event=self, + ) + # for smear-negative people + else: + test_result = self.sim.modules[ + "HealthSystem" + ].dx_manager.run_dx_test( + dx_tests_to_run="tb_xpert_test_smear_negative", + hsi_event=self, + ) # ------------------------- testing referrals ------------------------- # @@ -1906,9 +1809,7 @@ def apply(self, person_id, squeeze_factor): if ipt_eligible.any(): # select persons at highest risk of tb - rr_of_tb = self.module.lm["active_tb"].predict( - df.loc[ipt_eligible] - ) + rr_of_tb = self.module.lm["active_tb"].predict(df.loc[ipt_eligible]) # choose top 5 highest risk contacts ipt_sample = rr_of_tb.sort_values(ascending=False).head(5).index @@ -1999,7 +1900,9 @@ def apply(self, person_id, squeeze_factor): ) self.sim.modules["HealthSystem"].schedule_hsi_event( - HSI_Tb_StartTreatment(person_id=person_id, module=self.module), + HSI_Tb_StartTreatment( + person_id=person_id, module=self.module, facility_level="1a" + ), topen=now, tclose=None, priority=0, @@ -2065,7 +1968,9 @@ def apply(self, person_id, squeeze_factor): df.at[person_id, "tb_date_diagnosed"] = self.sim.date self.sim.modules["HealthSystem"].schedule_hsi_event( - HSI_Tb_StartTreatment(person_id=person_id, module=self.module), + HSI_Tb_StartTreatment( + person_id=person_id, module=self.module, facility_level="1a" + ), topen=self.sim.date, tclose=None, priority=0, @@ -2080,7 +1985,7 @@ def apply(self, person_id, squeeze_factor): class HSI_Tb_Xray_level2(HSI_Event, IndividualScopeEventMixin): """ - The is the x-ray HSI performed at level 2 + This is the x-ray HSI performed at level 2 usually used for testing children unable to produce sputum positive result will prompt referral to start treatment """ @@ -2136,7 +2041,9 @@ def apply(self, person_id, squeeze_factor): df.at[person_id, "tb_date_diagnosed"] = self.sim.date self.sim.modules["HealthSystem"].schedule_hsi_event( - HSI_Tb_StartTreatment(person_id=person_id, module=self.module), + HSI_Tb_StartTreatment( + person_id=person_id, module=self.module, facility_level="1a" + ), topen=self.sim.date, tclose=None, priority=0, @@ -2157,13 +2064,15 @@ def apply(self, person_id, squeeze_factor): class HSI_Tb_StartTreatment(HSI_Event, IndividualScopeEventMixin): - def __init__(self, module, person_id): + def __init__(self, module, person_id, facility_level="1a"): super().__init__(module, person_id=person_id) assert isinstance(module, Tb) + self.facility_level = facility_level + self.TREATMENT_ID = "Tb_Treatment" - self.ACCEPTED_FACILITY_LEVEL = '1a' self.number_of_occurrences = 0 + self.ACCEPTED_FACILITY_LEVEL = "1a" if (self.facility_level == "1a") else "2" @property def EXPECTED_APPT_FOOTPRINT(self): @@ -2197,6 +2106,18 @@ def apply(self, person_id, squeeze_factor): item_codes=self.module.item_codes_for_consumables_required[treatment_regimen] ) + # if require MDR treatment, and not currently at level 2, refer to level 2 + if (treatment_regimen == "tb_mdrtx") and (self.facility_level != "2"): + self.sim.modules["HealthSystem"].schedule_hsi_event( + hsi_event=HSI_Tb_StartTreatment( + person_id=person_id, module=self.module, facility_level="2" + ), + topen=self.sim.date + DateOffset(days=1), + tclose=None, + priority=0, + ) + return self.sim.modules["HealthSystem"].get_blank_appt_footprint() + if treatment_available: # start person on tb treatment - update properties df.at[person_id, "tb_on_treatment"] = True @@ -2208,16 +2129,15 @@ def apply(self, person_id, squeeze_factor): df.at[person_id, "tb_date_treated_mdr"] = now # schedule first follow-up appointment - follow_up_date = self.sim.date + DateOffset(months=1) logger.debug( key="message", data=f"HSI_Tb_StartTreatment: scheduling first follow-up " - f"for person {person_id} on {follow_up_date}", + f"for person {person_id}", ) self.sim.modules["HealthSystem"].schedule_hsi_event( HSI_Tb_FollowUp(person_id=person_id, module=self.module), - topen=follow_up_date, + topen=self.sim.date + DateOffset(months=1), tclose=None, priority=0, ) @@ -2225,17 +2145,19 @@ def apply(self, person_id, squeeze_factor): # if treatment not available, return for treatment start in 1 week # cap repeated visits at 5 else: - if self.number_of_occurrences <= 5: + + if ( + self.number_of_occurrences + <= self.module.parameters["tb_healthseekingbehaviour_cap"] + ): + self.sim.modules["HealthSystem"].schedule_hsi_event( - hsi_event=self, + self, topen=self.sim.date + DateOffset(weeks=1), tclose=None, priority=0, ) - def post_apply_hook(self): - self.number_of_occurrences += 1 - def select_treatment(self, person_id): """ helper function to select appropriate treatment and check whether @@ -2279,16 +2201,6 @@ def select_treatment(self, person_id): # treatment for reinfection ds-tb: child treatment_regimen = "tb_retx_child" - # -------- SHINE Trial shorter paediatric regimen -------- # - # shorter treatment for child with minimal tb - if (self.module.parameters["scenario"] == 5) \ - & (self.sim.date >= self.module.parameters["scenario_start_date"]) \ - & (person["age_years"] <= 16) \ - & ~(person["tb_smear"]) \ - & ~person["tb_ever_treated"] \ - & ~person["tb_diagnosed_mdr"]: - treatment_regimen = "tb_tx_child_shorter" - return treatment_regimen @@ -2354,11 +2266,6 @@ def apply(self, person_id, squeeze_factor): sputum_fup = follow_up_times["mdr_sputum"].dropna() treatment_length = p["mdr_treatment_length"] - # if person on shorter paediatric regimen - elif person["tb_treatment_regimen"] == "tb_tx_child_shorter": - sputum_fup = follow_up_times["shine_sputum"].dropna() - treatment_length = p["shine_treatment_length"] - # check schedule for sputum test and perform if necessary if months_since_tx in sputum_fup: ACTUAL_APPT_FOOTPRINT = self.make_appt_footprint( @@ -2489,8 +2396,11 @@ def apply(self, person_id, squeeze_factor): ) else: - # Reschedule this HSI to occur again, up to a 3 times in total - if self.number_of_occurrences < 3: + # Reschedule this HSI to occur again, up to a 5 times in total + if ( + self.number_of_occurrences + <= self.module.parameters["tb_healthseekingbehaviour_cap"] + ): self.sim.modules["HealthSystem"].schedule_hsi_event( self, topen=self.sim.date + pd.DateOffset(days=1), @@ -2499,6 +2409,39 @@ def apply(self, person_id, squeeze_factor): ) +class HSI_Tb_EndOfLifeCare(HSI_Event, IndividualScopeEventMixin): + """ + this is a hospital stay for terminally-ill patients with TB + it does not affect disability weight or probability of death + no consumables are logged but health system capacity (HR) is allocated + there are no consequences if hospital bed is not available as person has scheduled death + already within 2 weeks + """ + + def __init__(self, module, person_id, beddays=8): + super().__init__(module, person_id=person_id) + assert isinstance(module, Tb) + + self.TREATMENT_ID = "Tb_PalliativeCare" + self.EXPECTED_APPT_FOOTPRINT = self.make_appt_footprint({}) + self.ACCEPTED_FACILITY_LEVEL = "2" + + self.beddays = beddays + self.BEDDAYS_FOOTPRINT = self.make_beddays_footprint({"general_bed": self.beddays}) + + def apply(self, person_id, squeeze_factor): + df = self.sim.population.props + hs = self.sim.modules["HealthSystem"] + + if not df.at[person_id, "is_alive"]: + return hs.get_blank_appt_footprint() + + logger.debug( + key="message", + data=f"HSI_Tb_EndOfLifeCare: inpatient admission for {person_id}", + ) + + class Tb_DecisionToContinueIPT(Event, IndividualScopeEventMixin): """Helper event that is used to 'decide' if someone on IPT should continue or end This event is scheduled by 'HSI_Tb_Start_or_Continue_Ipt' after 6 months @@ -2541,11 +2484,13 @@ def apply(self, person_id): # --------------------------------------------------------------------------- -class TbDeathEvent(Event, IndividualScopeEventMixin): +class TbDecideDeathEvent(Event, IndividualScopeEventMixin): """ - The scheduled death for a tb case - check whether this death should occur using a linear model + The scheduled hospitalisation and subsequent death for a tb case + check whether death should occur using a linear model will depend on treatment status, smear status and age + then schedule a hospital stay prior to that death + hospital stay will not affect outcomes """ def __init__(self, module, person_id, cause): @@ -2554,6 +2499,7 @@ def __init__(self, module, person_id, cause): def apply(self, person_id): df = self.sim.population.props + p = self.module.parameters if not df.at[person_id, "is_alive"]: return @@ -2563,26 +2509,67 @@ def apply(self, person_id): logger.debug( key="message", - data=f"TbDeathEvent: checking whether death should occur for person {person_id}", + data=f"TbDecideDeathEvent: checking whether death should occur for person {person_id}", ) # use linear model to determine whether this person will die: rng = self.module.rng - result = self.module.lm["death_rate"].predict(df.loc[[person_id]], rng=rng) + will_die = self.module.lm["death_rate"].predict(df.loc[[person_id]], rng=rng) - if result: - logger.debug( - key="message", - data=f"TbDeathEvent: cause this death for person {person_id}", + if will_die: + # schedule hospital stay for this person + # schedule hospital stay + beddays = self.module.rng.randint( + low=p['length_of_inpatient_stay_if_terminal'][0], + high=p['length_of_inpatient_stay_if_terminal'][1]) + + self.sim.modules["HealthSystem"].schedule_hsi_event( + hsi_event=HSI_Tb_EndOfLifeCare( + person_id=person_id, module=self.sim.modules["Tb"], beddays=beddays + ), + priority=0, + topen=self.sim.date, + tclose=None, ) - self.sim.modules["Demography"].do_death( - individual_id=person_id, - cause=self.cause, - originating_module=self.module, + # schedule death for this person after hospital stay + self.sim.schedule_event( + event=TbDeathEvent(person_id=person_id, module=self.module), + date=self.sim.date + pd.DateOffset(days=beddays), ) +class TbDeathEvent(Event, IndividualScopeEventMixin): + """ + The scheduled death for a tb case + check whether this death should occur using a linear model + will depend on treatment status, smear status and age + """ + + def __init__(self, module, person_id): + super().__init__(module, person_id=person_id) + + def apply(self, person_id): + df = self.sim.population.props + + if not df.at[person_id, "is_alive"]: + return + + if not df.at[person_id, "tb_inf"] == "active": + return + + logger.debug( + key="message", + data=f"TbDeathEvent: cause this death for person {person_id}", + ) + + self.sim.modules["Demography"].do_death( + individual_id=person_id, + cause="TB", + originating_module=self.module, + ) + + # --------------------------------------------------------------------------- # Logging # --------------------------------------------------------------------------- @@ -2776,9 +2763,9 @@ def apply(self, population): # this will give ipt among whole population - not just eligible pop if new_tb_ipt: - ipt_coverage = new_tb_ipt / len(df[df.is_alive]) + current_ipt_coverage = new_tb_ipt / len(df[df.is_alive]) else: - ipt_coverage = 0 + current_ipt_coverage = 0 logger.info( key="tb_treatment", @@ -2787,7 +2774,7 @@ def apply(self, population): "tbNewDiagnosis": new_tb_diagnosis, "tbPropDiagnosed": prop_dx, "tbTreatmentCoverage": tx_coverage, - "tbIptCoverage": ipt_coverage, + "tbIptCoverage": current_ipt_coverage, }, ) diff --git a/tests/test_healthsystem.py b/tests/test_healthsystem.py index 78efbfca1c..801150fdcc 100644 --- a/tests/test_healthsystem.py +++ b/tests/test_healthsystem.py @@ -1537,8 +1537,12 @@ def get_set_of_treatment_ids_that_run(service_availability) -> Set[str]: get_set_of_treatment_ids_that_run(service_availability=["Hiv_Test_*"]) - generic_first_appts # Allow all `Hiv` things (but nothing else) - assert set({'Hiv_Test', 'Hiv_Treatment', 'Hiv_Prevention_Circumcision'}) == \ - get_set_of_treatment_ids_that_run(service_availability=["Hiv_*"]) - generic_first_appts + hiv_hsi_events = {'Hiv_Test', 'Hiv_Treatment', 'Hiv_Prevention_Circumcision', 'Hiv_Prevention_Infant', + 'Hiv_Prevention_Prep', 'Hiv_PalliativeCare'} + returned_treatment_ids = get_set_of_treatment_ids_that_run(service_availability=["Hiv_*"]) + + assert returned_treatment_ids.intersection( + hiv_hsi_events), "None of the expected treatment IDs are found in the returned set" # Allow all except `Hiv_Test` everything_except_hiv_test = everything - set({'Hiv_Test'}) diff --git a/tests/test_hiv.py b/tests/test_hiv.py index 59746f21ff..47ef0d2083 100644 --- a/tests/test_hiv.py +++ b/tests/test_hiv.py @@ -30,6 +30,7 @@ from tlo.methods.healthseekingbehaviour import HealthSeekingBehaviourPoll from tlo.methods.healthsystem import HealthSystemScheduler from tlo.methods.hiv import ( + HivAidsDeathEvent, HivAidsOnsetEvent, HSI_Hiv_StartOrContinueTreatment, HSI_Hiv_TestAndRefer, @@ -136,18 +137,18 @@ def test_initialisation(seed): for idx in before_aids_idx: events_for_this_person = sim.find_events_for_person(idx) - assert 1 == len(events_for_this_person) - next_event_date, next_event_obj = events_for_this_person[0] - assert isinstance(next_event_obj, hiv.HivAidsOnsetEvent) - assert next_event_date >= sim.date + assert len(events_for_this_person) > 0 + assert len([event for _ , event in events_for_this_person if isinstance(event, HivAidsOnsetEvent)]) + assert all(date_of_event >= sim.date for date_of_event, _ in + events_for_this_person), "Not all dates in the event list are after the current date" - # check that everyone who is infected and has got AIDS event get a future AIDS death event but nothing else + # check that everyone who is infected and has got AIDS event get a future AIDS death event for idx in aids: events_for_this_person = sim.find_events_for_person(idx) - assert 1 == len(events_for_this_person) - next_event_date, next_event_obj = events_for_this_person[0] - assert isinstance(next_event_obj, hiv.HivAidsDeathEvent) - assert next_event_date >= sim.date + assert len(events_for_this_person) > 0 + assert len([event for _ , event in events_for_this_person if isinstance(event, HivAidsDeathEvent)]) + assert all(date >= sim.date for date, _ in + events_for_this_person), "Not all dates in the event list are after the current date" def test_generation_of_new_infection(seed): diff --git a/tests/test_hiv_tb_scenarios.py b/tests/test_hiv_tb_scenarios.py deleted file mode 100644 index e872eab63f..0000000000 --- a/tests/test_hiv_tb_scenarios.py +++ /dev/null @@ -1,270 +0,0 @@ -""" Tests for setting up the HIV and TB scenarios used for projections """ - -import os -from pathlib import Path - -import pandas as pd -import pytest - -from tlo import Date, Simulation -from tlo.methods import ( - demography, - enhanced_lifestyle, - epi, - healthburden, - healthseekingbehaviour, - healthsystem, - hiv, - simplified_births, - symptommanager, - tb, -) - -try: - resourcefilepath = Path(os.path.dirname(__file__)) / '../resources' -except NameError: - # running interactively - resourcefilepath = 'resources' - - -def get_sim(seed): - """ - register all necessary modules for the tests to run - """ - - start_date = Date(2010, 1, 1) - sim = Simulation(start_date=start_date, seed=seed) - - # Register the appropriate modules - sim.register( - demography.Demography(resourcefilepath=resourcefilepath), - simplified_births.SimplifiedBirths(resourcefilepath=resourcefilepath), - enhanced_lifestyle.Lifestyle(resourcefilepath=resourcefilepath), - healthsystem.HealthSystem( - resourcefilepath=resourcefilepath, - service_availability=["*"], # all treatment allowed - mode_appt_constraints=0, # mode of constraints to do with officer numbers and time - cons_availability="all", # mode for consumable constraints (if ignored, all consumables available) - ignore_priority=True, # do not use the priority information in HSI event to schedule - capabilities_coefficient=1.0, # multiplier for the capabilities of health officers - disable=False, # disables the healthsystem (no constraints and no logging) and every HSI runs - disable_and_reject_all=False, # disable healthsystem and no HSI runs - ), - symptommanager.SymptomManager(resourcefilepath=resourcefilepath), - healthseekingbehaviour.HealthSeekingBehaviour(resourcefilepath=resourcefilepath), - healthburden.HealthBurden(resourcefilepath=resourcefilepath), - epi.Epi(resourcefilepath=resourcefilepath), - hiv.Hiv(resourcefilepath=resourcefilepath), - tb.Tb(resourcefilepath=resourcefilepath), - ) - - return sim - - -@pytest.mark.slow -def test_scenario_ipt_expansion(seed): - """ test scenario IPT expansion is set up correctly - should be expanded age eligibility in scenario 3 - otherwise only ages <5 are eligible - """ - - popsize = 100 - - sim = get_sim(seed=seed) - - # stop PLHIV getting IPT for purpose of tests - sim.modules['Tb'].parameters['ipt_coverage'].coverage_plhiv = 0 - # set coverage of IPT for TB contacts to 100% - sim.modules['Tb'].parameters['ipt_coverage'].coverage_paediatric = 100 - - # Make the population - sim.make_initial_population(n=popsize) - # simulate for 0 days, just get everything set up (dxtests etc) - sim.simulate(end_date=sim.date + pd.DateOffset(days=0)) - df = sim.population.props - - # check default scenario is set to 0 - assert sim.modules['Tb'].parameters['scenario'] == 0 - - # ipt eligibility should be limited to ages <=5 years - # assign all population into one district - so all are eligible as contacts - df.loc[df.is_alive, 'district_of_residence'] = 'Blantyre' - - # assign active TB to person 0 - person_id = 0 - - # assign person_id 0 active tb - df.at[person_id, 'tb_inf'] = 'active' - df.at[person_id, 'tb_strain'] = 'ds' - df.at[person_id, 'tb_date_active'] = sim.date - df.at[person_id, 'tb_smear'] = True - df.at[person_id, 'age_exact_years'] = 20 - df.at[person_id, 'age_years'] = 20 - - # assign symptoms - symptom_list = {"fever", "respiratory_symptoms", "fatigue", "night_sweats"} - sim.modules["SymptomManager"].change_symptom( - person_id=person_id, - symptom_string=symptom_list, - add_or_remove="+", - disease_module=sim.modules['Tb'], - duration_in_days=None, - ) - - # run diagnosis (HSI_Tb_ScreeningAndRefer) for person 0 - assert "tb_sputum_test_smear_positive" in sim.modules["HealthSystem"].dx_manager.dx_tests - screening_appt = tb.HSI_Tb_ScreeningAndRefer(person_id=person_id, - module=sim.modules['Tb']) - screening_appt.apply(person_id=person_id, squeeze_factor=0.0) - - assert pd.notnull(df.at[person_id, 'tb_date_tested']) - assert df.at[person_id, 'tb_diagnosed'] - - # check ages of those scheduled for HSI_Tb_Start_or_Continue_Ipt - list_of_events = list() - - for ev_tuple in sim.modules['HealthSystem'].HSI_EVENT_QUEUE: - date = ev_tuple.topen # this is the 'topen' value - event = ev_tuple.hsi_event - if isinstance(event, tb.HSI_Tb_Start_or_Continue_Ipt): - list_of_events.append((date, event, event.target)) - - idx_of_ipt_candidates = [x[2] for x in list_of_events] - ages_of_ipt_candidates = df.loc[idx_of_ipt_candidates, "age_exact_years"] - assert (ages_of_ipt_candidates < 6).all() - - -@pytest.mark.slow -def test_check_tb_test_under_each_scenario(seed): - """ test correct test is scheduled under each scenario - """ - - popsize = 10 - - sim = get_sim(seed=seed) - - # Make the population - sim.make_initial_population(n=popsize) - - sim.modules['Tb'].parameters["prop_presumptive_mdr_has_xpert"] = 1.0 # xpert always available - sim.modules['Tb'].parameters["sens_xpert"] = 1.0 # increase sensitivity of xpert testing - - # ------------------------- scenario 0 ------------------------- # - sim.modules['Tb'].parameters['scenario'] = 0 - - # simulate for 0 days, just get everything set up (dxtests etc) - sim.simulate(end_date=sim.date + pd.DateOffset(days=0)) - - df = sim.population.props - - # assign person_id active tb - hiv_neg_person = 0 - hiv_pos_person = 1 - both_people = [hiv_neg_person, hiv_pos_person] - - df.loc[both_people, 'tb_inf'] = 'active' - df.loc[both_people, 'tb_strain'] = 'ds' - df.loc[both_people, 'tb_date_active'] = sim.date - df.loc[both_people, 'tb_smear'] = True - df.loc[both_people, 'age_exact_years'] = 20 - df.loc[both_people, 'age_years'] = 20 - - # set HIV status - df.at[hiv_neg_person, 'hv_inf'] = False - df.at[hiv_pos_person, 'hv_inf'] = True - df.at[hiv_neg_person, 'hv_diagnosed'] = False # this is used for tb test selection - df.at[hiv_pos_person, 'hv_diagnosed'] = True - - # assign symptoms - symptom_list = {"fever", "respiratory_symptoms", "fatigue", "night_sweats"} - sim.modules["SymptomManager"].change_symptom( - person_id=both_people, - symptom_string=symptom_list, - add_or_remove="+", - disease_module=sim.modules['Tb'], - duration_in_days=None, - ) - - # select test for each person under baseline scenario - standard guidelines - assert "sputum" == sim.modules["Tb"].select_tb_test(hiv_neg_person) - assert "xpert" == sim.modules["Tb"].select_tb_test(hiv_pos_person) - - # screen and test hiv_neg_person - screening_appt = tb.HSI_Tb_ScreeningAndRefer(person_id=hiv_neg_person, - module=sim.modules['Tb']) - screening_appt.apply(person_id=hiv_neg_person, squeeze_factor=0.0) - - assert pd.notnull(df.at[hiv_neg_person, 'tb_date_tested']) - assert df.at[hiv_neg_person, 'tb_diagnosed'] - assert not df.at[hiv_neg_person, 'tb_diagnosed_mdr'] - - # screen and test hiv_pos_person - screening_appt = tb.HSI_Tb_ScreeningAndRefer(person_id=hiv_pos_person, - module=sim.modules['Tb']) - screening_appt.apply(person_id=hiv_pos_person, squeeze_factor=0.0) - - assert pd.notnull(df.at[hiv_pos_person, 'tb_date_tested']) - assert df.at[hiv_pos_person, 'tb_diagnosed'] - assert not df.at[hiv_pos_person, 'tb_diagnosed_mdr'] - - # apply scenario change, re-test, should be same - scenario_change_event = tb.ScenarioSetupEvent(module=sim.modules['Tb']) - scenario_change_event.apply(sim.population) - - # select test for each person under baseline scenario - standard guidelines - # this person should still qualify for sputum as they have not been treated - assert "sputum" == sim.modules["Tb"].select_tb_test(hiv_neg_person) - assert "xpert" == sim.modules["Tb"].select_tb_test(hiv_pos_person) - - # ------------------------- scenario 1 ------------------------- # - sim = get_sim(seed=seed) - - # Make the population - sim.make_initial_population(n=popsize) - - sim.modules['Tb'].parameters["prop_presumptive_mdr_has_xpert"] = 1.0 # xpert always available - sim.modules['Tb'].parameters["sens_xpert"] = 1.0 # increase sensitivity of xpert testing - sim.modules['Tb'].parameters['scenario'] = 1 - - # simulate for 0 days, just get everything set up (dxtests etc) - sim.simulate(end_date=sim.date + pd.DateOffset(days=0)) - - df = sim.population.props - - # assign person_id active tb - hiv_neg_person = 0 - hiv_pos_person = 1 - both_people = [hiv_neg_person, hiv_pos_person] - - df.loc[both_people, 'tb_inf'] = 'active' - df.loc[both_people, 'tb_strain'] = 'ds' - df.loc[both_people, 'tb_date_active'] = sim.date - df.loc[both_people, 'tb_smear'] = True - df.loc[both_people, 'age_exact_years'] = 20 - df.loc[both_people, 'age_years'] = 20 - # set HIV status - df.at[hiv_neg_person, 'hv_inf'] = False - df.at[hiv_pos_person, 'hv_inf'] = True - df.at[hiv_pos_person, 'hv_diagnosed'] = True - - # assign symptoms - symptom_list = {"fever", "respiratory_symptoms", "fatigue", "night_sweats"} - sim.modules["SymptomManager"].change_symptom( - person_id=both_people, - symptom_string=symptom_list, - add_or_remove="+", - disease_module=sim.modules['Tb'], - duration_in_days=None, - ) - - # select test for each person under scenario 1, should be standard at first - assert "sputum" == sim.modules["Tb"].select_tb_test(hiv_neg_person) - assert "xpert" == sim.modules["Tb"].select_tb_test(hiv_pos_person) - - # apply scenario change, re-test, should be xpert for all - scenario_change_event = tb.ScenarioSetupEvent(module=sim.modules['Tb']) - scenario_change_event.apply(sim.population) - - # select test for each person under changed guidelines - assert "xpert" == sim.modules["Tb"].select_tb_test(hiv_neg_person) - assert "xpert" == sim.modules["Tb"].select_tb_test(hiv_pos_person) diff --git a/tests/test_malaria.py b/tests/test_malaria.py index a1cb08a327..4ac2d377db 100644 --- a/tests/test_malaria.py +++ b/tests/test_malaria.py @@ -126,7 +126,7 @@ def test_sims(sim): assert not df.at[person, "ma_inf_type"] == "none" # if on treatment, must have treatment start date - for person in df.index[(df.ma_tx == 'uncomplicated')]: + for person in df.index[df.ma_tx.isin(["uncomplicated", "complicated"])]: assert not pd.isnull(df.at[person, "ma_date_tx"]) # remove scheduled rdt testing and disable health system, should be no rdts and no treatment @@ -209,7 +209,7 @@ def test_schedule_rdt_for_all(sim): df = sim.population.props # check no treatment unless infected - for person in df.index[(df.ma_tx == 'uncomplicated')]: + for person in df.index[df.ma_tx.isin(["uncomplicated", "complicated"])]: assert not pd.isnull(df.at[person, "ma_date_infected"]) # check clinical counter is working @@ -244,7 +244,7 @@ def test_dx_algorithm_for_malaria_outcomes_clinical( person_id: int = 0, ): """ - Create a person with clinical malaria and check if the functions in + Create a person with clinical malaria and check if the functions in dx_algorithm_child return the correct diagnosis. """ # Set up the simulation: @@ -388,7 +388,7 @@ def test_severe_malaria_deaths_perfect_treatment(sim): treatment_appt = malaria.HSI_Malaria_Treatment_Complicated(person_id=person_id, module=sim.modules['Malaria']) treatment_appt.apply(person_id=person_id, squeeze_factor=0.0) - assert df.at[person_id, 'ma_tx'] == 'complicated' + assert df.at[person_id, "ma_tx"] != "none" assert df.at[person_id, "ma_date_tx"] == sim.date assert df.at[person_id, "ma_tx_counter"] > 0 @@ -423,7 +423,7 @@ def test_severe_malaria_deaths_treatment_failure(sim): treatment_appt = malaria.HSI_Malaria_Treatment_Complicated(person_id=person_id, module=sim.modules['Malaria']) treatment_appt.apply(person_id=person_id, squeeze_factor=0.0) - assert df.at[person_id, 'ma_tx'] == 'complicated' + assert df.at[person_id, 'ma_tx'] != 'none' assert df.at[person_id, "ma_date_tx"] == sim.date assert df.at[person_id, "ma_tx_counter"] > 0 @@ -442,7 +442,7 @@ def test_severe_malaria_deaths_treatment_failure(sim): person_id = 1 df.loc[person_id, ["ma_is_infected", "ma_inf_type"]] = (True, "severe") - assert not df.at[person_id, 'ma_tx'] == 'complicated' + assert df.at[person_id, "ma_tx"] == "none" assert df.at[person_id, "ma_date_tx"] is pd.NaT assert df.at[person_id, "ma_tx_counter"] == 0 @@ -634,7 +634,7 @@ def test_individual_testing_and_treatment(sim): tx_appt.apply(person_id=person_id, squeeze_factor=0.0) assert df.at[person_id, "ma_tx_counter"] == 1 - assert df.at[person_id, "ma_tx"] == 'complicated' + assert df.at[person_id, "ma_tx"] != "none" def test_population_testing_and_treatment(sim): diff --git a/tests/test_tb.py b/tests/test_tb.py index 5c2943b310..0434c70069 100644 --- a/tests/test_tb.py +++ b/tests/test_tb.py @@ -431,8 +431,8 @@ def get_appt_footprints(_consumables_availability): def test_treatment_failure(seed): """ - test treatment failure occurs and - retreatment properties / follow-up occurs correctly + test treatment failure occurs and properties set correctly + treatment failure will schedule referral for xpert test at level 2 """ popsize = 10 @@ -504,26 +504,30 @@ def test_treatment_failure(seed): # check referral for screening/testing again # screen and test person_id screening_appt = tb.HSI_Tb_ScreeningAndRefer(person_id=person_id, - module=sim.modules['Tb']) + module=sim.modules['Tb'], + facility_level="1a") test = screening_appt.apply(person_id=person_id, squeeze_factor=0.0) - # should schedule xpert - if available - # check that the event returned a footprint for an xpert test + # should schedule a referral for xpert testing at facility level 2 + # check that the event returned a footprint Over5OPD assert test == screening_appt.make_appt_footprint({'Over5OPD': 1}) - # start treatment - tx_start = tb.HSI_Tb_StartTreatment(person_id=person_id, - module=sim.modules['Tb']) - tx_start.apply(person_id=person_id, squeeze_factor=0.0) - - # clinical monitoring - # check tb.HSI_Tb_FollowUp scheduled - followup_event = [ + # check tb.HSI_Tb_ScreeningAndRefer scheduled + followup_test = [ ev for ev in sim.modules['HealthSystem'].find_events_for_person(person_id) if - isinstance(ev[1], tb.HSI_Tb_FollowUp) + isinstance(ev[1], tb.HSI_Tb_ScreeningAndRefer) ][-1] - assert followup_event[0] > sim.date + assert followup_test[0] > sim.date + + # schedule follow-up test at level 2 to get xpert + screening_appt = tb.HSI_Tb_ScreeningAndRefer(person_id=person_id, + module=sim.modules['Tb'], + facility_level="2") + test = screening_appt.apply(person_id=person_id, squeeze_factor=0.0) + + # assert now should be diagnosed as active TB again + assert df.at[person_id, 'tb_diagnosed'] def test_children_referrals(seed): @@ -783,7 +787,8 @@ def test_mdr(seed): # next screening should pick up case as retreatment / mdr screening_appt = tb.HSI_Tb_ScreeningAndRefer(person_id=person_id, - module=sim.modules['Tb']) + module=sim.modules['Tb'], + facility_level='2') screening_appt.apply(person_id=person_id, squeeze_factor=0.0) assert df.at[person_id, 'tb_diagnosed_mdr'] @@ -791,7 +796,8 @@ def test_mdr(seed): # schedule mdr treatment start # this calls clinical_monitoring which should schedule all follow-up appts tx_start = tb.HSI_Tb_StartTreatment(person_id=person_id, - module=sim.modules['Tb']) + module=sim.modules['Tb'], + facility_level='2') tx_start.apply(person_id=person_id, squeeze_factor=0.0) # check treatment appropriate for mdr-tb @@ -1124,13 +1130,24 @@ def test_hsi_scheduling(seed): hsi_event = tb.HSI_Tb_ScreeningAndRefer(person_id=person_id, module=sim.modules['Tb']) hsi_event.run(squeeze_factor=0) + # person is HIV+ and will be referred for xpert - only available at level 2 + # Check person_id has a further testing event scheduled + date_event, event = [ + ev for ev in sim.modules['HealthSystem'].find_events_for_person(person_id) if + isinstance(ev[1], tb.HSI_Tb_ScreeningAndRefer) + ][0] + assert date_event >= sim.date + + # run testing event for xpert + hsi_event = tb.HSI_Tb_ScreeningAndRefer(person_id=person_id, + module=sim.modules['Tb'], + facility_level='2') + hsi_event.run(squeeze_factor=0) + + # person should now have treatment event scheduled # Check person_id has a treatment event scheduled date_event, event = [ ev for ev in sim.modules['HealthSystem'].find_events_for_person(person_id) if isinstance(ev[1], tb.HSI_Tb_StartTreatment) ][0] assert date_event == sim.date - - # check this is the only event scheduled - tmp = sim.modules['HealthSystem'].find_events_for_person(person_id) - assert len(tmp) == 1