Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stopping PrEP usage #216

Open
wants to merge 14 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/hivpy/column_names.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,9 @@
FIRST_LEN_START_DATE = "first_len_start_date" # None | date: start date of first ever injectable Len PrEP usage
FIRST_VR_START_DATE = "first_vr_start_date" # None | date: start date of first ever vaginal ring PrEP usage
LAST_PREP_START_DATE = "last_prep_start_date" # None | date: start date of most recent PrEP usage
LAST_PREP_STOP_DATE = "last_prep_stop_date" # None | date: stop date of most recent PrEP usage
PREP_JUST_STARTED = "prep_just_started" # Bool: True if PrEP usage began this time step
LAST_PREP_STOP_DATE = "last_prep_stop_date" # None | date: stop date of most recent PrEP usage
PREP_PAUSED = "prep_paused" # Bool: True if PrEP usage is paused this time step due to ineligibility
CONT_ON_PREP = "cont_on_prep" # None | timedelta: total length of continuous usage of current PrEP based on user intention (breaks due to ineligibility do not count against continuity)
CONT_ACTIVE_ON_PREP = "cont_active_on_prep" # None | timedelta: actual total length of continuous usage of current PrEP
CUMULATIVE_PREP_ORAL = "cumulative_prep_oral" # None | timedelta: total length of cumulative oral PrEP usage
Expand Down
11 changes: 6 additions & 5 deletions src/hivpy/data/prep.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,22 @@ prob_suspect_risk_prep: 0.5
# prep preference beta
prep_oral_pref_beta:
Value: [1.1, 1.3, 1.5]
prep_willing_threshold: 0.2
# effect of low vl on prep willingness
vl_prevalence_prep_threshold:
Value: [0.005, 0.01]

# starting prep
# prep-related testing
rate_test_onprep_any: 1
prep_willing_threshold: 0.2
prob_test_prep_start:
Value: [0.25, 0.50, 0.75]

# starting prep
prob_base_prep_start:
Value: [0.10, 0.30]
prob_prep_restart:
Value: [0.05, 0.10, 0.20]
prob_base_prep_stop:
Value: [0.05, 0.15, 0.30]
prob_base_prep_stop_nonuniform:
Value: [0.05, 0.15, 0.30]
Probability: [0.8, 0.1, 0.1]
prob_prep_restart:
Value: [0.05, 0.10, 0.20]
45 changes: 37 additions & 8 deletions src/hivpy/prep.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,13 @@ def __init__(self, **kwargs):
self.prep_cab_pref_beta = self.prep_oral_pref_beta + 0.3
self.prep_len_pref_beta = self.prep_cab_pref_beta
self.prep_vr_pref_beta = self.prep_oral_pref_beta - 0.1
self.prep_willing_threshold = self.p_data.prep_willing_threshold

self.vl_prevalence_affects_prep = rng.choice([True, False], p=[1/3, 2/3])
self.vl_prevalence_prep_threshold = self.p_data.vl_prevalence_prep_threshold.sample()

self.rate_test_onprep_any = self.p_data.rate_test_onprep_any
self.prep_willing_threshold = self.p_data.prep_willing_threshold
self.prob_test_prep_start = self.p_data.prob_test_prep_start.sample()

# probability of starting prep in people who are eligible, willing,
# and tested for HIV according to base rate of testing
self.prob_base_prep_start = self.p_data.prob_base_prep_start.sample()
Expand All @@ -60,12 +61,12 @@ def __init__(self, **kwargs):
self.prob_cab_prep_start = self.prob_base_prep_start
self.prob_len_prep_start = self.prob_base_prep_start
self.prob_vr_prep_start = self.prob_base_prep_start
self.prob_prep_restart = self.p_data.prob_prep_restart.sample()
# FIXME: stop probabilities dependent on time step length
self.prob_oral_prep_stop = self.p_data.prob_base_prep_stop.sample()
self.prob_cab_prep_stop = self.p_data.prob_base_prep_stop.sample()
self.prob_len_prep_stop = self.prob_cab_prep_stop
self.prob_vr_prep_stop = self.p_data.prob_base_prep_stop_nonuniform.sample()
self.prob_prep_restart = self.p_data.prob_prep_restart.sample()

def init_prep_variables(self, pop: Population):
pop.init_variable(col.PREP_ORAL_PREF, 0)
Expand Down Expand Up @@ -95,8 +96,9 @@ def init_prep_variables(self, pop: Population):
pop.init_variable(col.FIRST_LEN_START_DATE, None)
pop.init_variable(col.FIRST_VR_START_DATE, None)
pop.init_variable(col.LAST_PREP_START_DATE, None)
pop.init_variable(col.LAST_PREP_STOP_DATE, None)
pop.init_variable(col.PREP_JUST_STARTED, False)
pop.init_variable(col.LAST_PREP_STOP_DATE, None)
pop.init_variable(col.PREP_PAUSED, False)
pop.init_variable(col.CONT_ON_PREP, timedelta(months=0))
pop.init_variable(col.CONT_ACTIVE_ON_PREP, timedelta(months=0))
pop.init_variable(col.CUMULATIVE_PREP_ORAL, timedelta(months=0))
Expand Down Expand Up @@ -734,8 +736,8 @@ def restart_prep(self, pop: Population, time_step):

if len(eligible) > 0:
# starting prep outcomes
prep_types = pop.transform_group([col.FAVOURED_PREP_TYPE], self.calc_restarting_prep,
sub_pop=eligible, dropna=True)
prep_types = pop.transform_group([col.FAVOURED_PREP_TYPE, col.PREP_PAUSED],
self.calc_restarting_prep, sub_pop=eligible, dropna=True)
# people who are restarting prep
restarting_prep_pop = pop.apply_bool_mask(prep_types.notnull(), eligible)

Expand All @@ -752,19 +754,44 @@ def restart_prep(self, pop: Population, time_step):
self.set_all_prep_cumulative(pop, restarting_prep_pop, time_step)
# unset stop date
pop.set_present_variable(col.LAST_PREP_STOP_DATE, None, restarting_prep_pop)
# unpause prep
pop.set_present_variable(col.PREP_PAUSED, False, restarting_prep_pop)

def calc_restarting_prep(self, favoured_prep, size):
def calc_restarting_prep(self, favoured_prep, prep_paused, size):
"""
Returns PrEP types for people restarting PrEP.
Individual preferences and availability are taken into account.
"""
# outcomes
r = rng.uniform(size=size)
restarting = r < self.prob_prep_restart
# those who stopped due to ineligibility are guaranteed to restart
if prep_paused:
restarting = r < 1
else:
restarting = r < self.prob_prep_restart
prep = [favoured_prep if r else None for r in restarting]

return prep

def stop_prep(self, pop: Population):
"""
Update PrEP usage for people stopping PrEP due to lack of eligibility or positive test.
"""
# people who are using prep and are now ineligible
ineligible = pop.get_sub_pop(AND(OR(COND(col.HIV_DIAGNOSED, op.eq, True),
COND(col.PREP_ELIGIBLE, op.eq, False)),
COND(col.EVER_PREP, op.eq, True),
COND(col.LAST_PREP_STOP_DATE, op.eq, None),
COND(col.LAST_TEST_DATE, op.eq, pop.date)))

if len(ineligible) > 0:
# reset active continuous use
pop.set_present_variable(col.CONT_ACTIVE_ON_PREP, timedelta(months=0), ineligible)
# set stop date
pop.set_present_variable(col.LAST_PREP_STOP_DATE, pop.date, ineligible)
# pause prep
pop.set_present_variable(col.PREP_PAUSED, True, ineligible)

def prep_usage(self, pop: Population, time_step):
"""
Update PrEP usage for people starting, continuing, switching, restarting, and stopping PrEP.
Expand All @@ -775,3 +802,5 @@ def prep_usage(self, pop: Population, time_step):
self.continue_prep(pop, time_step)
# restarting prep
self.restart_prep(pop, time_step)
# stopping prep
self.stop_prep(pop)
6 changes: 3 additions & 3 deletions src/hivpy/prep_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ def __init__(self, filename):
self.prob_suspect_risk_prep = self.data["prob_suspect_risk_prep"]

self.prep_oral_pref_beta = self._get_discrete_dist("prep_oral_pref_beta")
self.prep_willing_threshold = self.data["prep_willing_threshold"]
self.vl_prevalence_prep_threshold = self._get_discrete_dist("vl_prevalence_prep_threshold")

self.rate_test_onprep_any = self.data["rate_test_onprep_any"]
self.prep_willing_threshold = self.data["prep_willing_threshold"]
self.prob_test_prep_start = self._get_discrete_dist("prob_test_prep_start")

self.prob_base_prep_start = self._get_discrete_dist("prob_base_prep_start")
self.prob_prep_restart = self._get_discrete_dist("prob_prep_restart")
self.prob_base_prep_stop = self._get_discrete_dist("prob_base_prep_stop")
self.prob_base_prep_stop_nonuniform = self._get_discrete_dist("prob_base_prep_stop_nonuniform")
self.prob_prep_restart = self._get_discrete_dist("prob_prep_restart")

except KeyError as ke:
print(ke.args)
Expand Down
35 changes: 34 additions & 1 deletion src/tests/test_prep.py
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,7 @@ def test_restarting_prep():
pop.data[col.PREP_ELIGIBLE] = True
pop.data[col.EVER_PREP] = True
pop.data[col.LAST_PREP_STOP_DATE] = pop.date - time_step
pop.data[col.PREP_PAUSED] = False
pop.data[col.PREP_JUST_STARTED] = False
pop.data[col.CONT_ON_PREP] = timedelta(months=0)
pop.data[col.CONT_ACTIVE_ON_PREP] = timedelta(months=0)
Expand All @@ -858,7 +859,7 @@ def test_restarting_prep():
pop.prep.prob_prep_restart = 0.5

pop.prep.restart_prep(pop, time_step)
# expecting 90% of people to restart prep
# expecting 50% of people to restart prep
no_on_prep = sum(pop.data[col.LAST_PREP_STOP_DATE].isnull())
mean = N * pop.prep.prob_prep_restart
stdev = sqrt(mean * (1 - pop.prep.prob_prep_restart))
Expand All @@ -868,3 +869,35 @@ def test_restarting_prep():
(pop.data[col.LAST_PREP_STOP_DATE].isnull()))
assert all((pop.data[col.CONT_ACTIVE_ON_PREP] == time_step) ==
(pop.data[col.LAST_PREP_STOP_DATE].isnull()))

pop.data[col.LAST_PREP_STOP_DATE] = pop.date - time_step
pop.data[col.PREP_PAUSED] = True
pop.prep.restart_prep(pop, time_step)
# expecting everyone to restart prep
assert sum(pop.data[col.LAST_PREP_STOP_DATE].isnull()) == N


def test_stopping_prep():
N = 100
pop = Population(size=N, start_date=date(5000, 1, 1))
pop.prep.date_prep_intro = [date(2000), date(3000), date(4000), date(5000)]
pop.data[col.HIV_DIAGNOSED] = False
pop.data[col.PREP_ELIGIBLE] = False
pop.data[col.EVER_PREP] = True
pop.data[col.LAST_PREP_STOP_DATE] = None
pop.data[col.PREP_PAUSED] = False
pop.data[col.LAST_TEST_DATE] = pop.date

pop.prep.stop_prep(pop)
# expecting everyone to stop prep due to ineligibility
assert sum(pop.data[col.LAST_PREP_STOP_DATE] == pop.date) == N
assert sum(pop.data[col.PREP_PAUSED]) == N

pop.data[col.HIV_DIAGNOSED] = True
pop.data[col.PREP_ELIGIBLE] = True
pop.data[col.LAST_PREP_STOP_DATE] = None
pop.data[col.PREP_PAUSED] = False
pop.prep.stop_prep(pop)
# expecting everyone to stop prep due to positive diagnosis
assert sum(pop.data[col.LAST_PREP_STOP_DATE] == pop.date) == N
assert sum(pop.data[col.PREP_PAUSED]) == N
25 changes: 13 additions & 12 deletions tutorials/intervention.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## Intervention Tutorial

Intervention is an optional part of the simulation which is triggered at the _year of intervention_; this allows the simulation to fork at this point into two independent simulations which differ in some properties. The changes to the properties of the simulation are determined by the _intervention function_, what can modify properties of the population or any of the simulation modules. After the year on intervention, one simulation will continue with the intervention function applied, and the other will continue with no changes. The intervention function may be applied once at the year of intervention, or every timestep following that point.
Intervention is an optional part of the simulation which is triggered at the _year of intervention_; this allows the simulation to fork at this point into two independent simulations which differ in some properties. The changes to the properties of the simulation are determined by the _intervention function_, what can modify properties of the population or any of the simulation modules. After the year on intervention, one simulation will continue with the intervention function applied, and the other will continue with no changes. The intervention function may be applied once at the year of intervention, or every timestep following that point.

The most relevant files are listed below:

Expand All @@ -13,10 +13,10 @@ If there are any testing-related variables you would like to change before runni

To make use of the intervention-related option there are two steps that need to be followed:

1) to add/modify the relevent intervention-related variables (intervention year, intervention option etc.) at the configuration file that is used to launch the simulation,
2) to ensure that the relevent intervention option is implemented in the intervention function in `simulation.py`.
1) to add/modify the relevant intervention-related variables (intervention year, intervention option etc.) at the configuration file that is used to launch the simulation,
2) to ensure that the relevant intervention option is implemented in the intervention function in `simulation.py`.

These steps are described in more detail below:
These steps are described in more detail below:

### Modifying the configuration file

Expand All @@ -34,25 +34,26 @@ run_model hivpy_intervention.yaml

### Modifying the simulation module

If an intervention year is set in the configuration file the `intervention` function is called in the simulation module. A synopsis of the process is: if the intervention year exists the simulation runs until the intervention year; a deep copy of the population object is created (this includes all of the modules that it owns such as sexual behaviour, HIV status, etc.); the simulation advances in two seperate outcomes with and without the intervention being implemented.
If an intervention year is set in the configuration file the `intervention` function is called in the simulation module. A synopsis of the process is: if the intervention year exists the simulation runs until the intervention year; a deep copy of the population object is created (this includes all of the modules that it owns such as sexual behaviour, HIV status, etc.); the simulation advances in two separate outcomes with and without the intervention being implemented.

The 'intervention' function takes as input the 'intervention_option' provided as a numeric value in the configuration file and updates the population / modules accordingly.
The 'intervention' function takes as input the 'intervention_option' provided as a numeric value in the configuration file and updates the population / modules accordingly.

#### Adding an intervention option in the simulation

To add a new intervention option into the simulation, the 'intervention' function in the simulation module needs to be modified. To do so a condition with the number of the option should be added and the corresponding population sub-module should be accessed.
To add a new intervention option into the simulation, the 'intervention' function in the simulation module needs to be modified. To do so a condition with the number of the option should be added and the corresponding population sub-module should be accessed.

**Options with negative numbers (-1, -2) have been reserved for the purposes of testing and providing example code in the intervention function.**

For example, option no. `-1` changes the starting date of the sex worker program setting as starting date the intervention date:
For example, option no. `-1` changes the starting date of the sex worker program setting as starting date the intervention date:
```bash
if option == -1:
pop.sexual_behaviour.sw_program_start_date = pop.date - self.simulation_config.time_step

```
- `if` statements can be more complex if desired, e.g. `if option in [1, 2, 3, 4]:` can be used to define code which applies to all the options in that list.
- Option code can modify any of the modules or the population itself through the population object (`pop`); in this case we are changing the `sw_program_start_date` by accessing the sexual behaviour module (`sexual_behaviour`).
- Alternative options can be added in a similar way.
- Alternative options can be added in a similar way.

#### Recurrent intervention

#### Recurrent intervention
If the `repeat_intervention` is set to True in the configuration file, the intervention is set to repeat for every timestep after the intervention year. This can be useful if there is reason to believe that changes made in the option code may be overridden by other parts of the simulation.
If the `repeat_intervention` is set to True in the configuration file, the intervention is set to repeat for every timestep after the intervention year. This can be useful if there is reason to believe that changes made in the option code may be overridden by other parts of the simulation.
2 changes: 1 addition & 1 deletion tutorials/post_processing.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Each time a HIVpy model run is completed, some basic post-processing is run at t
### Usage

```bash
python src/hivpy/post_processing.py <config> <output_dir> [-hi hipvy_input] [-si sas_input] [-eec]
python src/hivpy/post_processing.py <config> <output_dir> [-hi hipvy_input] [-si sas_input] [-eec early_epidemic_comparison]
```

- `config` - Path to configuration file containing names of expected graph output columns.
Expand Down
Loading
Loading