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

Year of intervention #202

Merged
merged 9 commits into from
Oct 2, 2024
Merged
16 changes: 16 additions & 0 deletions hivpy_intervention.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
EXPERIMENT:
population: 1000
start_year: 1989
end_year: 2021
time_interval_days: 90
intervention_year: 2000
intervention_option: 0
repeat_intervention: True
simulation_output_dir: output
graph_outputs: []

LOGGING:
log_directory: log
logfile_prefix: hivpy
log_file_level: DEBUG
console_log_level: WARNING
6 changes: 6 additions & 0 deletions src/hivpy/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ class SimulationConfig:
output_dir: Path
graph_outputs: list
time_step: timedelta = field(default_factory=lambda: timedelta(days=90))
intervention_date: date = None
intervention_option: int = 0
recurrent_intervention: bool = False

def _validate(self):
"""
Expand All @@ -63,6 +66,9 @@ def _validate(self):
try:
assert self.stop_date >= self.start_date + self.time_step
assert self.time_step > timedelta(days=0)
if self.intervention_date:
assert self.intervention_date >= self.start_date + self.time_step
assert self.intervention_date <= self.stop_date - self.time_step
except AssertionError:
raise SimulationException("Invalid simulation configuration.")

Expand Down
13 changes: 12 additions & 1 deletion src/hivpy/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,18 @@ def create_simulation(experiment_param):
output_dir = Path(experiment_param['simulation_output_dir'])
if not output_dir.exists():
output_dir.mkdir()
return SimulationConfig(population_size, start_date, end_date, output_dir, graph_outputs, interval)
if 'intervention_year' in experiment_param.keys():
intervention_date = date(int(experiment_param['intervention_year']), 1, 1)
intervention_option = int(experiment_param['intervention_option'])
recurrent_intervention = int(experiment_param['repeat_intervention'])
simconfiguration = SimulationConfig(population_size, start_date, end_date, output_dir,
graph_outputs, interval, intervention_date,
intervention_option, recurrent_intervention)
else:
simconfiguration = SimulationConfig(population_size, start_date, end_date, output_dir,
graph_outputs, interval)
return simconfiguration

except ValueError as err:
print('Error parsing the experiment parameters {}'.format(err))
except KeyError as kerr:
Expand Down
57 changes: 55 additions & 2 deletions src/hivpy/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import logging
import os
from copy import deepcopy
from datetime import datetime

import pandas as pd
Expand Down Expand Up @@ -32,23 +33,68 @@ def __init__(self, simulation_config):
"simulation_output_" + str(datetime.now().strftime("%Y%m%d-%H%M%S")))
self.output_path = self.output_dir / (
"simulation_output_" + str(datetime.now().strftime("%Y%m%d-%H%M%S")) + ".csv")
self.output_path_intervention = self.output_dir / (
"simulation_output_intervention_i" + str(datetime.now().strftime("%Y%m%d-%H%M%S")) + ".csv")

def _initialise_population(self):
self.population = Population(self.simulation_config.population_size,
self.simulation_config.start_date)

def intervention(self, pop, option):
# Negative intervention options have been reserved for demonstration / testing
if option == -1:
pop.sexual_behaviour.sw_program_start_date = pop.date - self.simulation_config.time_step
elif option == -2 and pop.date == datetime(2002, 1, 1):
pop.circumcision.policy_intervention_year = pop.date
return pop

def run(self):

# Start the simulation
date = self.simulation_config.start_date
assert date == self.population.date
time_step = self.simulation_config.time_step
while date <= self.simulation_config.stop_date:

if self.simulation_config.intervention_date:
end_date = self.simulation_config.intervention_date
interv_option = self.simulation_config.intervention_option
message = "Reached intervention year"
else:
end_date = self.simulation_config.stop_date
message = "Finished"

while date <= end_date:
logging.info("Timestep %s\n", date)
# Advance the population
self.population = self.population.evolve(time_step)
self.output.update_summary_stats(date, self.population, time_step)
date = date + time_step
logging.info("finished")
logging.info(message)

if self.simulation_config.intervention_date:
# make deep copy
self.modified_population = deepcopy(self.population)
self.intervention_output = deepcopy(self.output)
# call intervention function
self.modified_population = self.intervention(self.modified_population, interv_option)

while date <= self.simulation_config.stop_date:
logging.info("Timestep %s\n", date)

# intervention
self.modified_population = self.modified_population.evolve(time_step)
self.intervention_output.update_summary_stats(date, self.modified_population, time_step)
# repeat intervention according to option number
if self.simulation_config.recurrent_intervention:
self.modified_population = self.intervention(self.modified_population, interv_option)

# no intervention
self.population = self.population.evolve(time_step)
self.output.update_summary_stats(date, self.population, time_step)

date = date + time_step
logging.info("Finished")

# Store results
if not os.path.exists(self.output_dir):
os.makedirs(os.path.join(self.output_dir, "graph_outputs"))
Expand All @@ -57,3 +103,10 @@ def run(self):
self.output.output_stats["Date"], format="(%Y, %m, %d)")
graph_output(os.path.join(self.output_dir, "graph_outputs"), self.output.output_stats,
self.simulation_config.graph_outputs)

if self.simulation_config.intervention_date:
self.intervention_output.write_output(self.output_path_intervention)
self.intervention_output.output_stats["Date"] = pd.to_datetime(
self.intervention_output.output_stats["Date"], format="(%Y, %m, %d)")
graph_output(os.path.join(self.output_dir, "graph_outputs"), self.intervention_output.output_stats,
self.simulation_config.graph_outputs)
11 changes: 11 additions & 0 deletions src/tests/test_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,14 @@ def test_dummy_workflow(tmp_path, sample_experiment_params):
sample_experiment_params["LOGGING"]["log_directory"] = str(d)
dummy_config = create_experiment(sample_experiment_params)
run_experiment(dummy_config)


def test_intervention_year():
"""
Assert that the hivpy_intervention.yaml file is used and runs with intervention year
Needs refinement - To revisit this
"""
with open("hivpy_intervention.yaml", 'r') as sample_file:
hivpy_experiment_params = yaml.safe_load(sample_file)
hivpy_config = create_experiment(hivpy_experiment_params)
run_experiment(hivpy_config)
18 changes: 18 additions & 0 deletions src/tests/test_population.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,21 @@ def test_unions():
# union
union = pop.get_sub_pop_union(males, over25, under10)
assert all(expectation == union)


def test_population_deep_copy():
"""
Assert that a deep copy of the population is generated for the intervention
"""
from copy import deepcopy

size = 1000
pop = Population(size=size, start_date=date(1989, 1, 1))
pop_intervention = deepcopy(pop)
pop_intervention.set_present_variable(col.TEST_MARK, True)
pop_for_testing = pop.get_variable(col.TEST_MARK)
modified_pop_for_testing = pop_intervention.get_variable(col.TEST_MARK)

# assert pop_intervention is not a shallow copy
assert sum(pop_for_testing) == 0
assert sum(modified_pop_for_testing) == 1000
64 changes: 64 additions & 0 deletions src/tests/test_simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,67 @@ def test_death_occurs(tmp_path):
# ...and that there is at least one death overall!
# FIXME This is not guaranteed at the moment because of the values used
# assert results.num_alive[-1] < results.num_alive[0]


def test_error_intervention_before_start(tmp_path):
"""
Ensure that we throw an error if the intervention date is before the start.
"""
start = date(1989, 1)
end = date(1995, 1)
intervention = start - timedelta(days=365)
with pytest.raises(SimulationException):
SimulationConfig(start_date=start, stop_date=end, output_dir=tmp_path,
graph_outputs=[], intervention_date=intervention, population_size=100)


def test_error_intervention_after_end(tmp_path):
"""
Ensure that we throw an error if the intervention date is after the end.
"""
start = date(1989, 1)
end = date(1995, 1)
intervention = end + timedelta(days=365)
with pytest.raises(SimulationException):
SimulationConfig(start_date=start, stop_date=end, output_dir=tmp_path,
graph_outputs=[], intervention_date=intervention, population_size=100)


def test_intervention_option(tmp_path):
"""
Assert that the option number is implemented
In this case for the sexual worker program start date
"""
size = 1000
start = date(1989, 1)
step = timedelta(days=90)
end = date(1995, 1)
intervention = date(1992, 1)
option = -1
config = SimulationConfig(size, start, end, tmp_path, [], step, intervention, option)
simulation_handler = SimulationHandler(config)

simulation_handler.run()

modified_date = simulation_handler.modified_population.sexual_behaviour.sw_program_start_date
assert modified_date == intervention


def test_recurrent_intervention(tmp_path):
"""
Assert that the intervention is being updated when required
And with a different option number and date
"""
size = 1000
start = date(1989, 1)
step = timedelta(days=90)
end = date(2005, 1)
intervention = date(2000, 1)
option = -2
repeat_interv = True
config = SimulationConfig(size, start, end, tmp_path, [], step, intervention, option, repeat_interv)
simulation_handler = SimulationHandler(config)

simulation_handler.run()

assert simulation_handler.modified_population.circumcision.policy_intervention_year == date(2002, 1, 1)
58 changes: 58 additions & 0 deletions tutorials/intervention.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
## 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.

The most relevant files are listed below:

- `src/hivpy/simulation.py` - The simulation module, which contains the intervention code
- `src/tests/test_simulation.py` - Tests for the simulation
- `src/hivpy/hivpy_intervention.yaml` - Sample file for running with intervention-related variables
- `src/tests/test_population.py` - Test for understanding how the population class is modified

If there are any testing-related variables you would like to change before running your simulation, please change them in `hivpy_intervention.yaml` and `simulation.py`.

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`.

These steps are described in more detail below:

### Modifying the configuration file

The configuration file contains the intervention-related parameters that should be initialised when launching the simulation. These parameters are:
- `intervention_year`: The year were the intervention is set to take place (date)
- `intervention_option`: The option to implement at the intervention year (integer)
- `repeat_intervention`: Whether the intervention will repeat every time step after the year of intervention (True/False). If false, the intervention function will only be called once at the start of the intervention year.

For an example of how these parameters can be used please refer to the sample file `hivpy_intervention.yaml`

To run the simulations using the sample file, the `run_model` command can be run in the terminal:
```bash
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.

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.

**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:
```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.

#### 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.
Loading