From b0d030fd63c34c74af17e4779b3e1d13af0d4350 Mon Sep 17 00:00:00 2001 From: Matthew Ghosh Date: Wed, 19 Jun 2024 23:25:51 +0100 Subject: [PATCH 1/6] Made the required changes for serial intervals and started some tests --- pyEpiabm/pyEpiabm/core/person.py | 49 ++++++- pyEpiabm/pyEpiabm/routine/simulation.py | 85 +++++++++-- .../pyEpiabm/sweep/host_progression_sweep.py | 7 + pyEpiabm/pyEpiabm/sweep/household_sweep.py | 6 + pyEpiabm/pyEpiabm/sweep/place_sweep.py | 13 ++ pyEpiabm/pyEpiabm/sweep/spatial_sweep.py | 6 + .../tests/test_unit/test_core/test_person.py | 10 ++ .../test_unit/test_routine/test_simulation.py | 136 +++++++++++++++++- 8 files changed, 300 insertions(+), 12 deletions(-) diff --git a/pyEpiabm/pyEpiabm/core/person.py b/pyEpiabm/pyEpiabm/core/person.py index 870d40ff..1a964ebf 100644 --- a/pyEpiabm/pyEpiabm/core/person.py +++ b/pyEpiabm/pyEpiabm/core/person.py @@ -54,6 +54,8 @@ def __init__(self, microcell, age_group=None): self.secondary_infections_counts = [] self.time_of_recovery = None self.num_times_infected = 0 + self.exposure_period = None + self.serial_interval_dict = {} self.care_home_resident = False self.key_worker = False self.date_positive = None @@ -283,10 +285,55 @@ def increment_secondary_infections(self): """Increments the number of secondary infections the given person has for this specific infection period (i.e. if the given person has been infected multiple times, then we only increment the current secondary - infection count) + infection count). """ try: self.secondary_infections_counts[-1] += 1 except IndexError: raise RuntimeError("Cannot call increment_secondary_infections " "while secondary_infections_counts is empty") + + def set_exposure_period(self, exposure_period: float): + """Sets the exposure period (we define here as the time between a + primary case infection and a secondary case exposure, with the current + `Person` being the secondary case). We store this to be added to the + latent period of the infection to give a serial interval. + + Parameters + ---------- + exposure_period : float + The time between the infector's time of infection and the time + of exposure to the current Person + """ + self.exposure_period = exposure_period + + def store_serial_interval(self, latent_period: float): + """Adds this `latent_period` to the current `exposure_period` to give + a `serial_interval`, which will be stored in the + `serial_interval_dict`. The serial interval is the time between a + primary case infection and a secondary case infection. + + Parameters + ---------- + latent_period : float + The period between this `Person`'s time of exposure and this + `Person`'s time of infection + """ + # This method has been called erroneously if the exposure period is + # None + if self.exposure_period is None: + raise RuntimeError("Cannot call store_serial_interval while the" + " exposure_period is None") + + serial_interval = self.exposure_period + latent_period + # The reference day is the day the primary case was first infected + # This is what we will store in the dictionary + reference_day = self.infection_start_times[-1] - serial_interval + try: + (self.serial_interval_dict[reference_day] + .append(serial_interval)) + except KeyError: + self.serial_interval_dict[reference_day] = [serial_interval] + + # Reset the exposure period for the next infection + self.exposure_period = None diff --git a/pyEpiabm/pyEpiabm/routine/simulation.py b/pyEpiabm/pyEpiabm/routine/simulation.py index df725386..dbdf1bbb 100644 --- a/pyEpiabm/pyEpiabm/routine/simulation.py +++ b/pyEpiabm/pyEpiabm/routine/simulation.py @@ -65,6 +65,8 @@ def configure(self, a csv file containing infectiousness (viral load) values * `secondary_infections_output`: Boolean to determine whether we \ need a csv file containing secondary infections and R_t values + * `serial_interval_output`: Boolean to determine whether we \ + need a csv file containing serial interval data * `compress`: Boolean to determine whether we compress \ the infection history csv files @@ -87,12 +89,12 @@ def configure(self, inf_history_params : dict This is short for 'infection history file parameters' and we will use the abbreviation 'ih' to refer to infection history throughout - this class. If `status_output`, `infectiousness_output` and - `secondary_infections_output` are all False, then no infection - history csv files are produced (or if the dictionary is None). - These files contain the infection status, infectiousness and - secondary infection counts of each person every time step - respectively. The EpiOS tool + this class. If `status_output`, `infectiousness_output`, + `secondary_infections_output` and `serial_interval_output` are all + False, then no infection history csv files are produced (or if the + dictionary is None). These files contain the infection status, + infectiousness and secondary infection counts of each person every + time step respectively. The EpiOS tool (https://github.com/SABS-R3-Epidemiology/EpiOS) samples data from these files to mimic real life epidemic sampling techniques. These files can be compressed when 'compress' is True, reducing the size @@ -154,9 +156,11 @@ def configure(self, self.status_output = False self.infectiousness_output = False self.secondary_infections_output = False + self.serial_interval_output = False self.ih_status_writer = None self.ih_infectiousness_writer = None self.secondary_infections_writer = None + self.serial_interval_writer = None self.compress = False if inf_history_params: @@ -168,19 +172,28 @@ def configure(self, .get("infectiousness_output") self.secondary_infections_output = inf_history_params\ .get("secondary_infections_output") + self.serial_interval_output = inf_history_params \ + .get("serial_interval_output") self.compress = inf_history_params.get("compress", False) person_ids = [] person_ids += [person.id for cell in population.cells for person in cell.persons] self.ih_output_titles = ["time"] + person_ids self.Rt_output_titles = ["time"] + person_ids + ["R_t"] + ts = 1 / Parameters.instance().time_steps_per_day + times = np.arange(self.sim_params["simulation_start_time"], + self.sim_params["simulation_end_time"] + ts, + ts).tolist() + self.si_output_titles = times ih_folder = os.path.join(os.getcwd(), inf_history_params["output_dir"]) if not (self.status_output or self.infectiousness_output - or self.secondary_infections_output): - logging.warning("status_output, infectiousness_output and " - + "secondary_infections_output are False. " + or self.secondary_infections_output + or self.serial_interval_output): + logging.warning("status_output, infectiousness_output, " + + "secondary_infections_output and " + + "serial_interval_output are False. " + "No infection history csvs will be created.") if self.status_output: @@ -219,6 +232,18 @@ def configure(self, self.Rt_output_titles ) + if self.serial_interval_output: + + file_name = "serial_intervals.csv" + logging.info( + f"Set serial interval location to " + f"{os.path.join(ih_folder, file_name)}") + + self.serial_interval_writer = _CsvDictWriter( + ih_folder, file_name, + self.si_output_titles + ) + @log_exceptions() def run_sweeps(self): """Iteration step of the simulation. First the initialisation sweeps @@ -264,6 +289,8 @@ def run_sweeps(self): logging.info(f"Final time {t} days reached") if self.secondary_infections_writer: self.write_to_Rt_file(times) + if self.serial_interval_writer: + self.write_to_serial_interval_file(times) def write_to_file(self, time): """Records the count number of a given list of infection statuses @@ -404,6 +431,46 @@ def write_to_Rt_file(self, times: np.array): # Write each time step in dictionary form self.secondary_infections_writer.write(dict_row) + def write_to_serial_interval_file(self, times: np.array): + """Records the intervals between an infector and an infectee getting + infected to provide an overall serial interval for each time-step of + the epidemic. This can be used as a histogram of values for each + time step. + + Parameters + ---------- + times : np.array + An array of all time steps of the simulation + """ + # Initialise the dataframe + all_times = np.hstack((np.array(self + .sim_params["simulation_start_time"]), + times)) + data_dict = {time: [] for time in all_times} + for cell in self.population.cells: + for person in cell.persons: + # For every time the person was infected, add their list of + # serial intervals to the timepoint at which their infector + # became infected + for t_inf, intervals in person.serial_interval_dict.items(): + data_dict[t_inf] += intervals + + # Here we will fill out the rest of the dataframe with NaN values, + # as all lists will have different lengths + max_list_length = max([len(intervals) + for intervals in data_dict.values()]) + for t in data_dict.keys(): + data_dict[t] += ([np.nan] * (max_list_length - len(data_dict[t]))) + + # Change to dataframe to get the data in a list of dicts format + df = pd.DataFrame(data_dict) + + # The below is a list of dictionaries for each time step + list_of_dicts = df.to_dict(orient='records') + for dict_row in list_of_dicts: + # Write each time step in dictionary form + self.serial_interval_writer.write(dict_row) + def add_writer(self, writer: AbstractReporter): self.writers.append(writer) diff --git a/pyEpiabm/pyEpiabm/sweep/host_progression_sweep.py b/pyEpiabm/pyEpiabm/sweep/host_progression_sweep.py index 9d317838..f9b59401 100644 --- a/pyEpiabm/pyEpiabm/sweep/host_progression_sweep.py +++ b/pyEpiabm/pyEpiabm/sweep/host_progression_sweep.py @@ -324,6 +324,13 @@ def update_time_status_change(self, person: Person, time: float): person.time_of_status_change = time + transition_time + # Finally, if the person is Exposed, we can store their latency period + # as the transition_time. This can be used for calculating the serial + # interval + if person.infection_status == InfectionStatus.Exposed: + latent_period = transition_time + person.store_serial_interval(latent_period) + def _updates_infectiousness(self, person: Person, time: float): """Updates infectiousness. Scales using the initial infectiousness if the person is in an infectious state. Updates the infectiousness to diff --git a/pyEpiabm/pyEpiabm/sweep/household_sweep.py b/pyEpiabm/pyEpiabm/sweep/household_sweep.py index 7b04d07a..4d53b8e8 100644 --- a/pyEpiabm/pyEpiabm/sweep/household_sweep.py +++ b/pyEpiabm/pyEpiabm/sweep/household_sweep.py @@ -56,3 +56,9 @@ def __call__(self, time: float): # Increment the infector's # secondary_infections_count infector.increment_secondary_infections() + # Set the time between infector's infection time and + # the infectee's exposure time (current time) to be + # the exposure period of the infectee + inf_to_exposed = (time - + infector.infection_start_times[-1]) + infectee.set_exposure_period(inf_to_exposed) diff --git a/pyEpiabm/pyEpiabm/sweep/place_sweep.py b/pyEpiabm/pyEpiabm/sweep/place_sweep.py index fe31b803..61c82648 100644 --- a/pyEpiabm/pyEpiabm/sweep/place_sweep.py +++ b/pyEpiabm/pyEpiabm/sweep/place_sweep.py @@ -57,6 +57,12 @@ def __call__(self, time: float): # Increment the infector's # secondary_infections_count infector.increment_secondary_infections() + # Set the time between infector's infection time + # and the infectee's exposure time (current time) + # to be the exposure period of the infectee + inf_to_exposed = \ + (time - infector.infection_start_times[-1]) + infectee.set_exposure_period(inf_to_exposed) # Otherwise number of infectees is binomially # distributed. Not sure if covidsim considers only @@ -97,3 +103,10 @@ def __call__(self, time: float): # Increment the infector's # secondary_infections_count infector.increment_secondary_infections() + # Set the time between infector's infection + # time and the infectee's exposure time + # (current time) to be the exposure period of + # the infectee + inf_to_exposed = \ + (time - infector.infection_start_times[-1]) + infectee.set_exposure_period(inf_to_exposed) diff --git a/pyEpiabm/pyEpiabm/sweep/spatial_sweep.py b/pyEpiabm/pyEpiabm/sweep/spatial_sweep.py index e76d9139..d231b0ab 100644 --- a/pyEpiabm/pyEpiabm/sweep/spatial_sweep.py +++ b/pyEpiabm/pyEpiabm/sweep/spatial_sweep.py @@ -251,6 +251,12 @@ def do_infection_event(self, infector: Person, infectee: Person, infectee.microcell.cell.enqueue_person(infectee) # Increment the infector's secondary_infections_count infector.increment_secondary_infections() + # Set the time between infector's infection time and + # the infectee's exposure time (current time) to be + # the exposure period of the infectee + inf_to_exposed = (time - + infector.infection_start_times[-1]) + infectee.set_exposure_period(inf_to_exposed) def bind_population(self, population): super().bind_population(population) diff --git a/pyEpiabm/pyEpiabm/tests/test_unit/test_core/test_person.py b/pyEpiabm/pyEpiabm/tests/test_unit/test_core/test_person.py index 01844af2..41223459 100644 --- a/pyEpiabm/pyEpiabm/tests/test_unit/test_core/test_person.py +++ b/pyEpiabm/pyEpiabm/tests/test_unit/test_core/test_person.py @@ -149,6 +149,16 @@ def test_increment_secondary_infections(self): self.assertListEqual(self.person.secondary_infections_counts, [2, 5, 2]) + def test_add_serial_interval(self): + self.person.infection_start_times.append(1.0) + self.person.add_serial_interval(2.0) + self.person.infection_start_times.append(4.0) + self.person.add_serial_interval(1.0) + self.person.add_serial_interval(1.0) + self.person.add_serial_interval(3.0) + self.assertDictEqual(self.person.serial_interval_dict, + {1.0: [2.0], 4.0: [1.0, 1.0, 3.0]}) + if __name__ == '__main__': unittest.main() diff --git a/pyEpiabm/pyEpiabm/tests/test_unit/test_routine/test_simulation.py b/pyEpiabm/pyEpiabm/tests/test_unit/test_routine/test_simulation.py index 5c90927d..605ad0ad 100644 --- a/pyEpiabm/pyEpiabm/tests/test_unit/test_routine/test_simulation.py +++ b/pyEpiabm/pyEpiabm/tests/test_unit/test_routine/test_simulation.py @@ -36,7 +36,8 @@ def setUpClass(cls) -> None: cls.inf_history_params = {"output_dir": cls.mock_output_dir, "status_output": False, "infectiousness_output": False, - "secondary_infections_output": False} + "secondary_infections_output": False, + "serial_interval_output": False} cls.spatial_file_params = dict(cls.file_params) cls.spatial_file_params["age_stratified"] = True cls.spatial_file_params["spatial_output"] = True @@ -73,9 +74,11 @@ def test_configure(self, mock_mkdir): self.assertEqual(test_sim.status_output, False) self.assertEqual(test_sim.infectiousness_output, False) self.assertEqual(test_sim.secondary_infections_output, False) + self.assertEqual(test_sim.serial_interval_output, False) self.assertEqual(test_sim.ih_status_writer, None) self.assertEqual(test_sim.ih_infectiousness_writer, None) self.assertEqual(test_sim.secondary_infections_writer, None) + self.assertEqual(test_sim.serial_interval_writer, None) self.assertEqual(test_sim.include_waning, True) self.assertEqual(test_sim.compress, False) @@ -83,6 +86,7 @@ def test_configure(self, mock_mkdir): del test_sim.ih_status_writer del test_sim.ih_infectiousness_writer del test_sim.secondary_infections_writer + del test_sim.serial_interval_writer mo.assert_called_with(filename, 'w') @patch('os.makedirs') @@ -92,6 +96,7 @@ def test_configure_ih_status_infectiousness_false(self, mock_warning, self.inf_history_params["infectiousness_output"] = False self.inf_history_params["status_output"] = False self.inf_history_params["secondary_infections_output"] = False + self.inf_history_params["serial_interval_output"] = False mo = mock_open() with patch('pyEpiabm.output._csv_dict_writer.open', mo): test_sim = pe.routine.Simulation() @@ -99,8 +104,9 @@ def test_configure_ih_status_infectiousness_false(self, mock_warning, self.sweeps, self.sim_params, self.file_params, self.inf_history_params) mock_warning.assert_called_once_with("status_output, " - "infectiousness_output and " + "infectiousness_output, " "secondary_infections_output " + "and serial_interval_output " "are False. No infection " "history csvs will be " "created.") @@ -111,6 +117,7 @@ def test_configure_ih_status(self, mock_mkdir): self.inf_history_params["infectiousness_output"] = False self.inf_history_params["status_output"] = True self.inf_history_params["secondary_infections_output"] = False + self.inf_history_params["serial_interval_output"] = False with patch('pyEpiabm.output._csv_dict_writer.open', mo): filename = os.path.join(os.getcwd(), self.inf_history_params["output_dir"], @@ -127,11 +134,13 @@ def test_configure_ih_status(self, mock_mkdir): # infectiousness_output is False self.assertEqual(test_sim.ih_infectiousness_writer, None) self.assertEqual(test_sim.secondary_infections_writer, None) + self.assertEqual(test_sim.serial_interval_writer, None) del test_sim.writer del test_sim.ih_status_writer del test_sim.ih_infectiousness_writer del test_sim.secondary_infections_writer + del test_sim.serial_interval_writer mo.assert_called_with(filename, 'w') @patch('os.makedirs') @@ -140,6 +149,7 @@ def test_configure_ih_infectiousness(self, mock_mkdir): self.inf_history_params["infectiousness_output"] = True self.inf_history_params["status_output"] = False self.inf_history_params["secondary_infections_output"] = False + self.inf_history_params["serial_interval_output"] = False with patch('pyEpiabm.output._csv_dict_writer.open', mo): filename = os.path.join(os.getcwd(), self.inf_history_params["output_dir"], @@ -156,11 +166,13 @@ def test_configure_ih_infectiousness(self, mock_mkdir): # infectiousness_output is False self.assertEqual(test_sim.ih_status_writer, None) self.assertEqual(test_sim.secondary_infections_writer, None) + self.assertEqual(test_sim.serial_interval_writer, None) del test_sim.writer del test_sim.ih_status_writer del test_sim.ih_infectiousness_writer del test_sim.secondary_infections_writer + del test_sim.serial_interval_writer mo.assert_called_with(filename, 'w') @patch('os.makedirs') @@ -169,6 +181,7 @@ def test_configure_secondary_infections(self, mock_mkdir): self.inf_history_params["infectiousness_output"] = False self.inf_history_params["status_output"] = False self.inf_history_params["secondary_infections_output"] = True + self.inf_history_params["serial_interval_output"] = False with patch('pyEpiabm.output._csv_dict_writer.open', mo): filename = os.path.join(os.getcwd(), self.inf_history_params["output_dir"], @@ -186,11 +199,46 @@ def test_configure_secondary_infections(self, mock_mkdir): # infectiousness_output is False self.assertEqual(test_sim.ih_status_writer, None) self.assertEqual(test_sim.ih_infectiousness_writer, None) + self.assertEqual(test_sim.serial_interval_writer, None) del test_sim.writer del test_sim.ih_status_writer del test_sim.ih_infectiousness_writer del test_sim.secondary_infections_writer + del test_sim.serial_interval_writer + mo.assert_called_with(filename, 'w') + + @patch('os.makedirs') + def test_configure_serial_interval(self, mock_mkdir): + mo = mock_open() + self.inf_history_params["infectiousness_output"] = False + self.inf_history_params["status_output"] = False + self.inf_history_params["secondary_infections_output"] = False + self.inf_history_params["serial_interval_output"] = True + with patch('pyEpiabm.output._csv_dict_writer.open', mo): + filename = os.path.join(os.getcwd(), + self.inf_history_params["output_dir"], + "serial_intervals.csv") + test_sim = pe.routine.Simulation() + + # Test that the output titles are correct + test_sim.configure(self.test_population, self.initial_sweeps, + self.sweeps, self.sim_params, self.file_params, + self.inf_history_params) + + self.assertEqual(test_sim.si_output_titles, + [0.0, 1.0]) + # Test that the ih_status_writer is None, as + # infectiousness_output is False + self.assertEqual(test_sim.ih_status_writer, None) + self.assertEqual(test_sim.ih_infectiousness_writer, None) + self.assertEqual(test_sim.secondary_infections_writer, None) + + del test_sim.writer + del test_sim.ih_status_writer + del test_sim.ih_infectiousness_writer + del test_sim.secondary_infections_writer + del test_sim.serial_interval_writer mo.assert_called_with(filename, 'w') @patch('logging.exception') @@ -256,6 +304,7 @@ def test_run_sweeps_ih_status(self, mock_mkdir, patch_write): self.inf_history_params["infectiousness_output"] = False self.inf_history_params["status_output"] = True self.inf_history_params["secondary_infections_output"] = False + self.inf_history_params["serial_interval_output"] = False with patch('pyEpiabm.output._csv_dict_writer.open', mo): time_write = self.sim_params["simulation_end_time"] test_sim = pe.routine.Simulation() @@ -273,6 +322,7 @@ def test_run_sweeps_ih_infectiousness(self, mock_mkdir, patch_write): self.inf_history_params["infectiousness_output"] = True self.inf_history_params["status_output"] = False self.inf_history_params["secondary_infections_output"] = False + self.inf_history_params["serial_interval_output"] = False with patch('pyEpiabm.output._csv_dict_writer.open', mo): time_write = self.sim_params["simulation_end_time"] test_sim = pe.routine.Simulation() @@ -291,6 +341,24 @@ def test_run_sweeps_secondary_infections(self, mock_mkdir, patch_write): self.inf_history_params["infectiousness_output"] = False self.inf_history_params["status_output"] = False self.inf_history_params["secondary_infections_output"] = True + self.inf_history_params["serial_interval_output"] = False + with patch('pyEpiabm.output._csv_dict_writer.open', mo): + test_sim = pe.routine.Simulation() + test_sim.configure(self.test_population, self.initial_sweeps, + self.sweeps, self.sim_params, self.file_params, + self.inf_history_params) + test_sim.run_sweeps() + patch_write.assert_called_with(np.array([1])) + + @patch('pyEpiabm.routine.simulation.tqdm', notqdm) + @patch('pyEpiabm.routine.Simulation.write_to_serial_interval_file') + @patch('os.makedirs') + def test_run_sweeps_serial_interval(self, mock_mkdir, patch_write): + mo = mock_open() + self.inf_history_params["infectiousness_output"] = False + self.inf_history_params["status_output"] = False + self.inf_history_params["secondary_infections_output"] = False + self.inf_history_params["serial_interval_output"] = True with patch('pyEpiabm.output._csv_dict_writer.open', mo): test_sim = pe.routine.Simulation() test_sim.configure(self.test_population, self.initial_sweeps, @@ -468,6 +536,7 @@ def test_write_to_ih_file_status_no_infectiousness(self, mock_mkdir, self.inf_history_params['status_output'] = True self.inf_history_params['infectiousness_output'] = False self.inf_history_params['secondary_infections_output'] = False + self.inf_history_params['serial_interval_output'] = False with patch('pyEpiabm.output._csv_dict_writer.open', mo): test_sim = pe.routine.Simulation() test_sim.configure(self.test_population, self.initial_sweeps, @@ -497,6 +566,7 @@ def test_write_to_ih_file_no_status_infectiousness(self, mock_mkdir, self.inf_history_params['status_output'] = False self.inf_history_params['infectiousness_output'] = True self.inf_history_params['secondary_infections_output'] = False + self.inf_history_params['serial_interval_output'] = False with patch('pyEpiabm.output._csv_dict_writer.open', mo): test_sim = pe.routine.Simulation() test_sim.configure(self.test_population, self.initial_sweeps, @@ -526,6 +596,7 @@ def test_write_to_ih_file_status_infectiousness(self, mock_mkdir, time=1): self.inf_history_params['status_output'] = True self.inf_history_params['infectiousness_output'] = True self.inf_history_params['secondary_infections_output'] = False + self.inf_history_params['serial_interval_output'] = False with patch('pyEpiabm.output._csv_dict_writer.open', mo): test_sim = pe.routine.Simulation() test_sim.configure(self.test_population, self.initial_sweeps, @@ -564,6 +635,7 @@ def test_write_to_Rt_file(self, mock_mkdir, time=1): self.inf_history_params['status_output'] = False self.inf_history_params['infectiousness_output'] = False self.inf_history_params['secondary_infections_output'] = True + self.inf_history_params['serial_interval_output'] = False with patch('pyEpiabm.output._csv_dict_writer.open', mo): test_sim = pe.routine.Simulation() test_sim.configure(self.rt_test_population, self.initial_sweeps, @@ -616,6 +688,66 @@ def test_write_to_Rt_file(self, mock_mkdir, time=1): mock_mkdir.assert_called_with( os.path.join(os.getcwd(), self.inf_history_params["output_dir"])) + @patch('os.makedirs') + def test_write_to_serial_interval_file(self, mock_mkdir, time=1): + + if os.path.exists(self.inf_history_params["output_dir"]): + os.rmdir(self.inf_history_params["output_dir"]) + + mo = mock_open() + self.inf_history_params['status_output'] = False + self.inf_history_params['infectiousness_output'] = False + self.inf_history_params['secondary_infections_output'] = False + self.inf_history_params['serial_interval_output'] = True + with patch('pyEpiabm.output._csv_dict_writer.open', mo): + test_sim = pe.routine.Simulation() + test_sim.configure(self.rt_test_population, self.initial_sweeps, + self.sweeps, self.sim_params, self.file_params, + self.inf_history_params) + person1 = self.rt_test_population.cells[0].persons[0] + person1.num_times_infected = 2 + person1.infection_start_times = [0.0, 2.0] + person1.serial_interval_dict = {0.0: [6.0, 3.0], 2.0: [3.0]} + person2 = self.rt_test_population.cells[0].persons[1] + person2.num_times_infected = 1 + person2.infection_start_times = [1.0] + person2.serial_interval_dict = {1.0: [4.0]} + person3 = self.rt_test_population.cells[0].persons[2] + person3.num_times_infected = 1 + person3.infection_start_times = [1.0] + person3.serial_interval_dict = {1.0: [7.0, 5.0]} + dict_0 = {0.0: 6.0, 1.0: 4.0, 2.0: 3.0} + dict_1 = {0.0: 3.0, 1.0: 7.0, 2.0: np.nan} + dict_2 = {0.0: np.nan, 1.0: 5.0, 2.0: np.nan} + + with patch('pyEpiabm.output._csv_dict_writer' + '._CsvDictWriter.write') as mock_write: + test_sim.write_to_serial_interval_file(np.array([1.0, 2.0])) + calls = mock_write.call_args_list + # Need to use np.testing for the NaNs + # Need to test keys and values separately in case we are using + # python 3.7 (for which np.testing.assert_equal will not work) + major, minor = sys.version_info[0], sys.version_info[1] + if major >= 4 or (major == 3 and minor >= 8): + actual_dict_0 = calls[0].args[0] + for key in dict_0: + self.assertTrue(key in actual_dict_0) + np.testing.assert_array_equal(dict_0[key], + actual_dict_0[key]) + actual_dict_1 = calls[1].args[0] + for key in dict_1: + self.assertTrue(key in actual_dict_1) + np.testing.assert_array_equal(dict_1[key], + actual_dict_1[key]) + actual_dict_2 = calls[2].args[0] + for key in dict_2: + self.assertTrue(key in actual_dict_2) + np.testing.assert_array_equal(dict_2[key], + actual_dict_2[key]) + self.assertEqual(mock_write.call_count, 3) + mock_mkdir.assert_called_with( + os.path.join(os.getcwd(), self.inf_history_params["output_dir"])) + @patch('os.makedirs') def test_compress_no_compression(self, mock_mkdir): mo = mock_open() From 2873068b7c4462cb90b0d855d1c73b102125e95b Mon Sep 17 00:00:00 2001 From: Matthew Ghosh Date: Thu, 20 Jun 2024 15:42:04 +0100 Subject: [PATCH 2/6] Finished all tests --- pyEpiabm/pyEpiabm/core/person.py | 5 ++- .../tests/test_unit/test_core/test_person.py | 41 +++++++++++++++---- .../test_unit/test_routine/test_simulation.py | 18 ++++---- .../test_sweep/test_host_progression_sweep.py | 26 ++++++++++++ .../test_sweep/test_household_sweep.py | 4 ++ .../test_unit/test_sweep/test_place_sweep.py | 6 +++ .../test_sweep/test_spatial_sweep.py | 13 ++++++ 7 files changed, 94 insertions(+), 19 deletions(-) diff --git a/pyEpiabm/pyEpiabm/core/person.py b/pyEpiabm/pyEpiabm/core/person.py index 1a964ebf..6548a1af 100644 --- a/pyEpiabm/pyEpiabm/core/person.py +++ b/pyEpiabm/pyEpiabm/core/person.py @@ -311,7 +311,8 @@ def store_serial_interval(self, latent_period: float): """Adds this `latent_period` to the current `exposure_period` to give a `serial_interval`, which will be stored in the `serial_interval_dict`. The serial interval is the time between a - primary case infection and a secondary case infection. + primary case infection and a secondary case infection. This method + is called immediately after a person becomes exposed. Parameters ---------- @@ -328,7 +329,7 @@ def store_serial_interval(self, latent_period: float): serial_interval = self.exposure_period + latent_period # The reference day is the day the primary case was first infected # This is what we will store in the dictionary - reference_day = self.infection_start_times[-1] - serial_interval + reference_day = self.time_of_status_change - serial_interval try: (self.serial_interval_dict[reference_day] .append(serial_interval)) diff --git a/pyEpiabm/pyEpiabm/tests/test_unit/test_core/test_person.py b/pyEpiabm/pyEpiabm/tests/test_unit/test_core/test_person.py index 41223459..46a9e896 100644 --- a/pyEpiabm/pyEpiabm/tests/test_unit/test_core/test_person.py +++ b/pyEpiabm/pyEpiabm/tests/test_unit/test_core/test_person.py @@ -149,15 +149,40 @@ def test_increment_secondary_infections(self): self.assertListEqual(self.person.secondary_infections_counts, [2, 5, 2]) - def test_add_serial_interval(self): - self.person.infection_start_times.append(1.0) - self.person.add_serial_interval(2.0) - self.person.infection_start_times.append(4.0) - self.person.add_serial_interval(1.0) - self.person.add_serial_interval(1.0) - self.person.add_serial_interval(3.0) + def test_set_exposure_period(self): + self.person.set_exposure_period(exposure_period=5) + self.assertEqual(self.person.exposure_period, 5) + + def test_store_serial_interval_erroneous(self): + with self.assertRaises(RuntimeError) as ve_1: + self.person.store_serial_interval(4.0) + self.assertEqual(str(ve_1.exception), + "Cannot call store_serial_interval while the" + " exposure_period is None") + # Infect once with an exposure period for successful method call + self.person.set_exposure_period(1.0) + self.person.time_of_status_change = 2.0 + self.person.store_serial_interval(latent_period=1.0) + # Do not add the exposure period for failure + self.person.time_of_status_change = 19.0 + with self.assertRaises(RuntimeError) as ve_2: + self.person.store_serial_interval(latent_period=4.0) + self.assertEqual(str(ve_2.exception), + "Cannot call store_serial_interval while the" + " exposure_period is None") + + def test_store_serial_interval(self): + self.person.set_exposure_period(1.0) + self.person.time_of_status_change = 2.0 + self.person.store_serial_interval(latent_period=1.0) + self.person.set_exposure_period(2.0) + self.person.time_of_status_change = 6.0 + self.person.store_serial_interval(latent_period=4.0) + self.person.set_exposure_period(5.0) + self.person.time_of_status_change = 19.0 + self.person.store_serial_interval(latent_period=4.0) self.assertDictEqual(self.person.serial_interval_dict, - {1.0: [2.0], 4.0: [1.0, 1.0, 3.0]}) + {0.0: [2.0, 6.0], 10.0: [9.0]}) if __name__ == '__main__': diff --git a/pyEpiabm/pyEpiabm/tests/test_unit/test_routine/test_simulation.py b/pyEpiabm/pyEpiabm/tests/test_unit/test_routine/test_simulation.py index 605ad0ad..dce32821 100644 --- a/pyEpiabm/pyEpiabm/tests/test_unit/test_routine/test_simulation.py +++ b/pyEpiabm/pyEpiabm/tests/test_unit/test_routine/test_simulation.py @@ -706,19 +706,19 @@ def test_write_to_serial_interval_file(self, mock_mkdir, time=1): self.inf_history_params) person1 = self.rt_test_population.cells[0].persons[0] person1.num_times_infected = 2 - person1.infection_start_times = [0.0, 2.0] - person1.serial_interval_dict = {0.0: [6.0, 3.0], 2.0: [3.0]} + person1.infection_start_times = [2.0, 10.0] + person1.serial_interval_dict = {0.0: [2.0], 1.0: [9.0]} person2 = self.rt_test_population.cells[0].persons[1] person2.num_times_infected = 1 - person2.infection_start_times = [1.0] - person2.serial_interval_dict = {1.0: [4.0]} + person2.infection_start_times = [5.0] + person2.serial_interval_dict = {2.0: [3.0]} person3 = self.rt_test_population.cells[0].persons[2] person3.num_times_infected = 1 - person3.infection_start_times = [1.0] - person3.serial_interval_dict = {1.0: [7.0, 5.0]} - dict_0 = {0.0: 6.0, 1.0: 4.0, 2.0: 3.0} - dict_1 = {0.0: 3.0, 1.0: 7.0, 2.0: np.nan} - dict_2 = {0.0: np.nan, 1.0: 5.0, 2.0: np.nan} + person3.infection_start_times = [3.0, 11.0, 21.0] + person3.serial_interval_dict = {0.0: [3.0, 11.0], 2.0: [19.0]} + dict_0 = {0.0: 2.0, 1.0: 9.0, 2.0: 3.0} + dict_1 = {0.0: 3.0, 1.0: np.nan, 2.0: 19.0} + dict_2 = {0.0: 11.0, 1.0: np.nan, 2.0: np.nan} with patch('pyEpiabm.output._csv_dict_writer' '._CsvDictWriter.write') as mock_write: diff --git a/pyEpiabm/pyEpiabm/tests/test_unit/test_sweep/test_host_progression_sweep.py b/pyEpiabm/pyEpiabm/tests/test_unit/test_sweep/test_host_progression_sweep.py index 72929ae8..bc374d9d 100644 --- a/pyEpiabm/pyEpiabm/tests/test_unit/test_sweep/test_host_progression_sweep.py +++ b/pyEpiabm/pyEpiabm/tests/test_unit/test_sweep/test_host_progression_sweep.py @@ -376,6 +376,32 @@ def test_update_time_status_change_no_age(self, current_time=100.0): else: self.assertLessEqual(current_time, time_of_status_change) + def test_update_time_status_change_serial_interval(self): + """Tests that an exposed person has their latency period stored and + added to their exposure_period to give a serial interval (to be stored) + This occurs at the end of the update_time_status_change method + """ + test_sweep = pe.sweep.HostProgressionSweep() + # 3 days between their infector becoming infected and now (the time + # of the infection event) + self.person1.set_exposure_period(3.0) + self.person1.update_status(InfectionStatus.Exposed) + self.person1.next_infection_status = InfectionStatus.InfectMild + current_time = 5.0 + test_sweep.update_time_status_change(self.person1, current_time) + time_of_status_change = self.person1.time_of_status_change + # The time_of_status_change to InfectMild minus the current_time + # gives the length of time that the person is latent + latent_period = time_of_status_change - current_time + # The reference time is the time in which their infector became + # infected + reference_time = 5.0 - 3.0 + # The serial_interval_dict records their serial_interval (defined as + # the time between the infector having Infect status and the current + # person having Infect status) at the reference time + self.assertDictEqual(self.person1.serial_interval_dict, + {reference_time: [3.0 + latent_period]}) + def test_update_time_status_change_waning_immunity(self): """Tests that the time to status change of a Recovered person with waning immunity is equal to the output of the InverseCDF method. This diff --git a/pyEpiabm/pyEpiabm/tests/test_unit/test_sweep/test_household_sweep.py b/pyEpiabm/pyEpiabm/tests/test_unit/test_sweep/test_household_sweep.py index ed95ed9d..c25f1f70 100644 --- a/pyEpiabm/pyEpiabm/tests/test_unit/test_sweep/test_household_sweep.py +++ b/pyEpiabm/pyEpiabm/tests/test_unit/test_sweep/test_household_sweep.py @@ -64,18 +64,21 @@ def test__call__(self, mock_force): # Add one susceptible to the population, with the mocked infectiousness # ensuring they are added to the infected queue. self.person.infection_status = pe.property.InfectionStatus.InfectMild + self.person.infection_start_times = [0.0] test_queue = Queue() new_person = pe.Person(self.microcell) new_person.household = self.house self.house.persons.append(new_person) self.house.susceptible_persons.append(new_person) self.pop.cells[0].persons.append(new_person) + self.assertEqual(new_person.exposure_period, None) test_queue.put(new_person) self.test_sweep.bind_population(self.pop) self.test_sweep(self.time) self.assertEqual(self.cell.person_queue.qsize(), 1) self.assertListEqual(self.person.secondary_infections_counts, [1]) + self.assertEqual(new_person.exposure_period, 1.0) # Change the additional person to recovered, and assert the queue # is empty. @@ -87,6 +90,7 @@ def test__call__(self, mock_force): self.test_sweep(self.time) self.assertTrue(self.cell.person_queue.empty()) self.assertListEqual(self.person.secondary_infections_counts, [1]) + self.assertEqual(new_person.exposure_period, 1.0) def test_no_households(self): pop_nh = pe.Population() # Population without households diff --git a/pyEpiabm/pyEpiabm/tests/test_unit/test_sweep/test_place_sweep.py b/pyEpiabm/pyEpiabm/tests/test_unit/test_sweep/test_place_sweep.py index c1555e5e..69db6b3e 100644 --- a/pyEpiabm/pyEpiabm/tests/test_unit/test_sweep/test_place_sweep.py +++ b/pyEpiabm/pyEpiabm/tests/test_unit/test_sweep/test_place_sweep.py @@ -60,13 +60,16 @@ def test__call__(self, mock_inf, mock_force): # ensuring they are added to the infected queue and the infector's # secondary_infections_counts are incremented. self.person1.update_status(pe.property.InfectionStatus.InfectMild) + self.person1.infection_start_times = [0.0] self.person1.secondary_infections_counts = [0] + self.assertEqual(self.new_person.exposure_period, None) self.place.add_person(self.new_person) self.cell.person_queue = Queue() self.test_sweep.bind_population(self.pop) self.test_sweep(time) self.assertEqual(self.cell.person_queue.qsize(), 1) self.assertListEqual(self.person1.secondary_infections_counts, [1]) + self.assertEqual(self.new_person.exposure_period, 1.0) # Change the additional person to recovered, and assert the queue # is empty. @@ -76,6 +79,7 @@ def test__call__(self, mock_inf, mock_force): self.test_sweep(time) self.assertTrue(self.cell.person_queue.empty()) self.assertListEqual(self.person1.secondary_infections_counts, [1]) + self.assertEqual(self.new_person.exposure_period, 1.0) # Now test when binomial dist is activated. mock_inf.return_value = 1 @@ -86,6 +90,7 @@ def test__call__(self, mock_inf, mock_force): self.test_sweep(time) self.assertTrue(self.cell.person_queue.empty()) self.assertListEqual(self.person1.secondary_infections_counts, [1]) + self.assertEqual(self.new_person.exposure_period, 1.0) # Change the additional person to susceptible. self.new_person.update_status(pe.property.InfectionStatus.Susceptible) @@ -95,6 +100,7 @@ def test__call__(self, mock_inf, mock_force): self.test_sweep(time) self.assertEqual(self.cell.person_queue.qsize(), 1) self.assertListEqual(self.person1.secondary_infections_counts, [2]) + self.assertEqual(self.new_person.exposure_period, 1.0) if __name__ == '__main__': diff --git a/pyEpiabm/pyEpiabm/tests/test_unit/test_sweep/test_spatial_sweep.py b/pyEpiabm/pyEpiabm/tests/test_unit/test_sweep/test_spatial_sweep.py index 875000e4..849b39a6 100644 --- a/pyEpiabm/pyEpiabm/tests/test_unit/test_sweep/test_spatial_sweep.py +++ b/pyEpiabm/pyEpiabm/tests/test_unit/test_sweep/test_spatial_sweep.py @@ -255,9 +255,12 @@ def test__call__(self, mock_inf, mock_foi, mock_poisson, mock_inf_list, # Assert a basic population test_pop = self.pop test_sweep.bind_population(test_pop) + self.infector.infection_start_times = [0.0] + self.assertEqual(self.infectee.exposure_period, None) test_sweep(time) self.assertTrue(self.cell_inf.person_queue.empty()) self.assertListEqual(self.infector.secondary_infections_counts, [1]) + self.assertEqual(self.infectee.exposure_period, 1.0) mock_inf.assert_called_once_with(self.cell_inf, time) mock_foi.assert_called_once_with(self.cell_inf, self.cell_susc, @@ -266,26 +269,31 @@ def test__call__(self, mock_inf, mock_foi, mock_poisson, mock_inf_list, # Change infector's status to infected self.infector.update_status(InfectionStatus.InfectMild) + self.infector.infection_start_times = [0.0] test_sweep(time) self.assertEqual(self.cell_inf.person_queue.qsize(), 0) self.assertListEqual(self.infector.secondary_infections_counts, [2]) + self.assertEqual(self.infectee.exposure_period, 1.0) Parameters.instance().do_CovidSim = True self.cell_susc.person_queue = Queue() test_sweep(time) self.assertEqual(self.cell_susc.person_queue.qsize(), 1) self.assertListEqual(self.infector.secondary_infections_counts, [3]) + self.assertEqual(self.infectee.exposure_period, 1.0) # Check when we have an infector but no infectees self.infectee.update_status(InfectionStatus.Recovered) self.cell_susc.person_queue = Queue() test_sweep(time) self.assertEqual(self.cell_susc.person_queue.qsize(), 0) self.assertListEqual(self.infector.secondary_infections_counts, [3]) + self.assertEqual(self.infectee.exposure_period, 1.0) # Test parameters break-out clause Parameters.instance().infection_radius = 0 test_sweep(time) self.assertEqual(self.cell_susc.person_queue.qsize(), 0) self.assertListEqual(self.infector.secondary_infections_counts, [3]) + self.assertEqual(self.infectee.exposure_period, 1.0) mock_inf_list.assert_called_with(self.cell_inf, [self.cell_susc], time) mock_list_covid.assert_called_with(self.infector, [self.cell_susc], @@ -323,17 +331,22 @@ def test_do_infection_event(self, mock_random): fake_infectee = microcell_susc.persons[1] fake_infectee.update_status(InfectionStatus.Recovered) actual_infectee = microcell_susc.persons[0] + self.infector.infection_start_times = [0.0] self.assertTrue(cell_susc.person_queue.empty()) test_sweep.do_infection_event(self.infector, fake_infectee, 1) self.assertFalse(mock_random.called) # Should have already returned self.assertTrue(cell_susc.person_queue.empty()) self.assertListEqual(self.infector.secondary_infections_counts, [0]) + self.assertEqual(fake_infectee.exposure_period, None) + self.assertEqual(actual_infectee.exposure_period, None) test_sweep.do_infection_event(self.infector, actual_infectee, 1) mock_random.assert_called_once() self.assertEqual(cell_susc.person_queue.qsize(), 1) self.assertListEqual(self.infector.secondary_infections_counts, [1]) + self.assertEqual(fake_infectee.exposure_period, None) + self.assertEqual(actual_infectee.exposure_period, 1.0) if __name__ == '__main__': From a39388d8fa3dba725fc9dab8dbb12836860d5fe1 Mon Sep 17 00:00:00 2001 From: Matthew Ghosh Date: Thu, 20 Jun 2024 15:53:25 +0100 Subject: [PATCH 3/6] Fixed host progression sweep tests --- .../test_unit/test_sweep/test_host_progression_sweep.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyEpiabm/pyEpiabm/tests/test_unit/test_sweep/test_host_progression_sweep.py b/pyEpiabm/pyEpiabm/tests/test_unit/test_sweep/test_host_progression_sweep.py index bc374d9d..63ad6bdc 100644 --- a/pyEpiabm/pyEpiabm/tests/test_unit/test_sweep/test_host_progression_sweep.py +++ b/pyEpiabm/pyEpiabm/tests/test_unit/test_sweep/test_host_progression_sweep.py @@ -364,6 +364,8 @@ def test_update_time_status_change_no_age(self, current_time=100.0): continue # Method should not be used to infect people test_sweep.update_next_infection_status(person) + if person.infection_status.name == 'Exposed': + person.set_exposure_period(2.0) test_sweep.update_time_status_change(person, current_time) time_of_status_change = person.time_of_status_change if person.infection_status.name in ['Recovered', 'Dead', @@ -682,6 +684,7 @@ def test_call_main(self, mock_next_time, mock_param, mock_param.return_value.infectiousness_prof = self.mock_inf_prog # First check that people progress through the # infection stages correctly. + self.person2.set_exposure_period(1.0) self.person2.update_status(pe.property.InfectionStatus.Exposed) self.person2.time_of_status_change = 1.0 self.person2.next_infection_status = \ @@ -696,6 +699,7 @@ def test_call_main(self, mock_next_time, mock_param, # Tests population bound successfully. self.assertEqual(test_sweep._population.cells[0].persons[1]. infection_status, pe.property.InfectionStatus.Exposed) + self.person1.set_exposure_period(1.0) test_sweep(1.0) self.assertEqual(self.person2.infection_status, pe.property.InfectionStatus.InfectMild) @@ -823,6 +827,7 @@ def test_multiple_transitions_in_one_time_step(self, mock_param, mock_param.return_value.rate_multiplier_params = self.multipliers mock_next_time.return_value = 0.0 self.person1.time_of_status_change = 1.0 + self.person1.set_exposure_period(1.0) self.person1.update_status(InfectionStatus.Susceptible) self.person1.next_infection_status = InfectionStatus.Exposed test_sweep = pe.sweep.HostProgressionSweep() From b7a25de8a684ebab13a98c81426c74b4dbc10834 Mon Sep 17 00:00:00 2001 From: kcgallagher Date: Thu, 20 Jun 2024 16:51:42 +0100 Subject: [PATCH 4/6] Temporary triple sweep changes --- pyEpiabm/pyEpiabm/sweep/__init__.py | 2 + pyEpiabm/pyEpiabm/sweep/triple_sweep.py | 35 +++++ .../simulation_outputs/output.csv | 122 +++++++++--------- .../triple_simulation_flow.py | 75 +++++++++++ .../triple_simulation_outputs/output.csv | 22 ++++ .../triple_simulation_flow_SIR_plot.png | Bin 0 -> 27159 bytes 6 files changed, 195 insertions(+), 61 deletions(-) create mode 100644 pyEpiabm/pyEpiabm/sweep/triple_sweep.py create mode 100644 python_examples/basic_infection_history_simulation/triple_simulation_flow.py create mode 100644 python_examples/basic_infection_history_simulation/triple_simulation_outputs/output.csv create mode 100644 python_examples/basic_infection_history_simulation/triple_simulation_outputs/triple_simulation_flow_SIR_plot.png diff --git a/pyEpiabm/pyEpiabm/sweep/__init__.py b/pyEpiabm/pyEpiabm/sweep/__init__.py index 1092b531..d9becbad 100644 --- a/pyEpiabm/pyEpiabm/sweep/__init__.py +++ b/pyEpiabm/pyEpiabm/sweep/__init__.py @@ -21,3 +21,5 @@ from .travel_sweep import TravelSweep from .transition_matrices import StateTransitionMatrix, TransitionTimeMatrix from .initial_vaccine_sweep import InitialVaccineQueue + +from .triple_sweep import TripleSweep \ No newline at end of file diff --git a/pyEpiabm/pyEpiabm/sweep/triple_sweep.py b/pyEpiabm/pyEpiabm/sweep/triple_sweep.py new file mode 100644 index 00000000..59d7e52f --- /dev/null +++ b/pyEpiabm/pyEpiabm/sweep/triple_sweep.py @@ -0,0 +1,35 @@ +# +# Infection always causes three subsequent infections. Always. +# + +import random + +from pyEpiabm.core import Person + +from .abstract_sweep import AbstractSweep + + +class TripleSweep(AbstractSweep): + + def __call__(self, time: float): + """Given a population structure, loops over infected members + and considers whether they infected household members based + on individual, and spatial infectiousness and susceptibility. + + Parameters + ---------- + time : float + Simulation time + + """ + # Infects three people at first opportunity, then never again + for cell in self._population.cells: + infectious_persons = filter(Person.is_infectious, cell.persons) + for infector in infectious_persons: + while infector.secondary_infections_counts[-1] < 3: + infectee = random.choice(infector.household.susceptible_persons) + cell.enqueue_person(infectee) + infector.increment_secondary_infections() + inf_to_exposed = (time - + infector.infection_start_times[-1]) + infectee.set_exposure_period(inf_to_exposed) diff --git a/python_examples/basic_infection_history_simulation/simulation_outputs/output.csv b/python_examples/basic_infection_history_simulation/simulation_outputs/output.csv index 0929fc32..4ee88e60 100644 --- a/python_examples/basic_infection_history_simulation/simulation_outputs/output.csv +++ b/python_examples/basic_infection_history_simulation/simulation_outputs/output.csv @@ -1,62 +1,62 @@ time,InfectionStatus.Susceptible,InfectionStatus.Exposed,InfectionStatus.InfectASympt,InfectionStatus.InfectMild,InfectionStatus.InfectGP,InfectionStatus.InfectHosp,InfectionStatus.InfectICU,InfectionStatus.InfectICURecov,InfectionStatus.Recovered,InfectionStatus.Dead,InfectionStatus.Vaccinated -0,90,0,0,10,0,0,0,0,0,0,0 -1.0,90,0,0,8,0,0,0,0,2,0,0 -2.0,75,17,0,8,0,0,0,0,0,0,0 -3.0,67,21,2,9,1,0,0,0,0,0,0 -4.0,66,20,3,7,2,0,0,0,2,0,0 -5.0,68,18,5,7,2,0,0,0,0,0,0 -6.0,66,14,8,10,1,0,0,0,1,0,0 -7.0,66,14,8,8,0,0,0,0,4,0,0 -8.0,67,15,10,6,0,0,0,0,2,0,0 -9.0,65,16,11,6,0,0,0,0,2,0,0 -10.0,66,16,12,4,0,0,0,0,2,0,0 -11.0,66,14,13,4,2,0,0,0,1,0,0 -12.0,66,13,15,4,2,0,0,0,0,0,0 -13.0,63,14,14,5,2,0,0,0,2,0,0 -14.0,65,12,13,6,3,0,0,0,1,0,0 -15.0,63,12,14,5,5,0,0,0,1,0,0 -16.0,62,13,13,4,6,0,0,0,2,0,0 -17.0,64,10,13,5,4,0,0,0,4,0,0 -18.0,68,6,12,4,7,0,0,0,3,0,0 -19.0,67,6,11,6,7,1,0,0,2,0,0 -20.0,65,9,11,6,7,1,0,0,1,0,0 -21.0,63,10,12,7,6,1,0,0,1,0,0 -22.0,64,8,12,7,5,1,0,0,3,0,0 -23.0,67,4,11,10,6,1,0,0,1,0,0 -24.0,67,5,8,8,5,2,0,0,5,0,0 -25.0,70,6,9,8,2,2,0,0,3,0,0 -26.0,72,5,10,9,2,2,0,0,0,0,0 -27.0,72,4,11,7,2,2,0,0,2,0,0 -28.0,70,4,10,6,5,2,0,0,3,0,0 -29.0,71,5,10,5,6,1,0,0,2,0,0 -30.0,69,8,9,5,6,1,0,0,2,0,0 -31.0,68,9,7,6,6,1,0,0,3,0,0 -32.0,70,6,8,6,5,1,0,0,4,0,0 -33.0,74,4,9,6,5,1,0,0,1,0,0 -34.0,74,3,7,6,3,2,0,0,5,0,0 -35.0,77,4,7,7,3,2,0,0,0,0,0 -36.0,72,7,6,8,4,2,0,0,1,0,0 -37.0,72,6,7,9,4,1,0,0,1,0,0 -38.0,73,5,6,9,5,1,0,0,1,0,0 -39.0,72,7,6,7,4,0,0,0,3,1,0 -40.0,74,7,6,8,2,0,0,0,2,1,0 -41.0,75,7,5,7,3,0,0,0,2,1,0 -42.0,77,5,5,7,3,0,0,0,2,1,0 -43.0,78,5,5,7,3,0,0,0,1,1,0 -44.0,77,5,6,6,3,0,0,0,2,1,0 -45.0,76,8,6,5,3,0,0,0,1,1,0 -46.0,75,8,7,4,2,0,0,0,3,1,0 -47.0,78,5,8,6,2,0,0,0,0,1,0 -48.0,76,6,8,5,3,0,0,0,1,1,0 -49.0,77,2,8,6,4,0,0,0,2,1,0 -50.0,78,3,8,6,3,0,0,0,1,1,0 -51.0,79,2,8,5,3,0,0,0,2,1,0 -52.0,80,2,6,2,4,0,0,0,5,1,0 -53.0,85,2,6,0,3,0,0,0,3,1,0 -54.0,87,2,6,1,3,0,0,0,0,1,0 -55.0,84,5,6,1,3,0,0,0,0,1,0 -56.0,83,6,6,1,3,0,0,0,0,1,0 -57.0,83,5,5,2,3,0,0,0,1,1,0 -58.0,84,5,3,2,2,0,0,0,3,1,0 -59.0,87,3,3,4,2,0,0,0,0,1,0 -60.0,87,2,4,3,2,0,0,0,1,1,0 +0,99,0,0,1,0,0,0,0,0,0,0 +1.0,99,0,0,1,0,0,0,0,0,0,0 +2.0,99,0,0,1,0,0,0,0,0,0,0 +3.0,99,0,0,1,0,0,0,0,0,0,0 +4.0,99,0,0,1,0,0,0,0,0,0,0 +5.0,99,0,0,0,0,0,0,0,1,0,0 +6.0,99,0,0,0,0,0,0,0,1,0,0 +7.0,99,0,0,0,0,0,0,0,1,0,0 +8.0,99,0,0,0,0,0,0,0,1,0,0 +9.0,99,0,0,0,0,0,0,0,1,0,0 +10.0,99,0,0,0,0,0,0,0,1,0,0 +11.0,99,0,0,0,0,0,0,0,1,0,0 +12.0,99,0,0,0,0,0,0,0,1,0,0 +13.0,99,0,0,0,0,0,0,0,1,0,0 +14.0,99,0,0,0,0,0,0,0,1,0,0 +15.0,99,0,0,0,0,0,0,0,1,0,0 +16.0,99,0,0,0,0,0,0,0,1,0,0 +17.0,99,0,0,0,0,0,0,0,1,0,0 +18.0,99,0,0,0,0,0,0,0,1,0,0 +19.0,99,0,0,0,0,0,0,0,1,0,0 +20.0,99,0,0,0,0,0,0,0,1,0,0 +21.0,99,0,0,0,0,0,0,0,1,0,0 +22.0,99,0,0,0,0,0,0,0,1,0,0 +23.0,99,0,0,0,0,0,0,0,1,0,0 +24.0,99,0,0,0,0,0,0,0,1,0,0 +25.0,99,0,0,0,0,0,0,0,1,0,0 +26.0,99,0,0,0,0,0,0,0,1,0,0 +27.0,99,0,0,0,0,0,0,0,1,0,0 +28.0,99,0,0,0,0,0,0,0,1,0,0 +29.0,99,0,0,0,0,0,0,0,1,0,0 +30.0,99,0,0,0,0,0,0,0,1,0,0 +31.0,99,0,0,0,0,0,0,0,1,0,0 +32.0,99,0,0,0,0,0,0,0,1,0,0 +33.0,99,0,0,0,0,0,0,0,1,0,0 +34.0,99,0,0,0,0,0,0,0,1,0,0 +35.0,99,0,0,0,0,0,0,0,1,0,0 +36.0,99,0,0,0,0,0,0,0,1,0,0 +37.0,99,0,0,0,0,0,0,0,1,0,0 +38.0,99,0,0,0,0,0,0,0,1,0,0 +39.0,99,0,0,0,0,0,0,0,1,0,0 +40.0,99,0,0,0,0,0,0,0,1,0,0 +41.0,99,0,0,0,0,0,0,0,1,0,0 +42.0,99,0,0,0,0,0,0,0,1,0,0 +43.0,99,0,0,0,0,0,0,0,1,0,0 +44.0,99,0,0,0,0,0,0,0,1,0,0 +45.0,99,0,0,0,0,0,0,0,1,0,0 +46.0,99,0,0,0,0,0,0,0,1,0,0 +47.0,99,0,0,0,0,0,0,0,1,0,0 +48.0,99,0,0,0,0,0,0,0,1,0,0 +49.0,99,0,0,0,0,0,0,0,1,0,0 +50.0,99,0,0,0,0,0,0,0,1,0,0 +51.0,99,0,0,0,0,0,0,0,1,0,0 +52.0,99,0,0,0,0,0,0,0,1,0,0 +53.0,99,0,0,0,0,0,0,0,1,0,0 +54.0,99,0,0,0,0,0,0,0,1,0,0 +55.0,99,0,0,0,0,0,0,0,1,0,0 +56.0,99,0,0,0,0,0,0,0,1,0,0 +57.0,99,0,0,0,0,0,0,0,1,0,0 +58.0,99,0,0,0,0,0,0,0,1,0,0 +59.0,99,0,0,0,0,0,0,0,1,0,0 +60.0,99,0,0,0,0,0,0,0,1,0,0 diff --git a/python_examples/basic_infection_history_simulation/triple_simulation_flow.py b/python_examples/basic_infection_history_simulation/triple_simulation_flow.py new file mode 100644 index 00000000..3cc31ecb --- /dev/null +++ b/python_examples/basic_infection_history_simulation/triple_simulation_flow.py @@ -0,0 +1,75 @@ +# +# Example simulation script with data output and visualisation +# + +import os +import logging +import pandas as pd +import matplotlib.pyplot as plt + +import pyEpiabm as pe + +# Setup output for logging file +logging.basicConfig(filename='sim.log', filemode='w+', level=logging.DEBUG, + format=('%(asctime)s - %(name)s' + + '- %(levelname)s - %(message)s')) + +# Set config file for Parameters +pe.Parameters.set_file(os.path.join(os.path.dirname(__file__), + "simple_parameters.json")) + +# Method to set the seed at the start of the simulation, for reproducibility + +pe.routine.Simulation.set_random_seed(seed=42) + +# Pop_params are used to configure the population structure being used in this +# simulation. + +pop_params = {"population_size": 1000, "cell_number": 1, + "microcell_number": 1, "household_number": 1, + "place_number": 2} + +# Create a population based on the parameters given. +population = pe.routine.ToyPopulationFactory().make_pop(pop_params) + +# sim_ and file_params give details for the running of the simulations and +# where output should be written to. +sim_params = {"simulation_start_time": 0, "simulation_end_time": 20, + "initial_infected_number": 1} + +file_params = {"output_file": "output.csv", + "output_dir": os.path.join(os.path.dirname(__file__), + "triple_simulation_outputs"), + "spatial_output": False, + "age_stratified": False} + +# Create a simulation object, configure it with the parameters given, then +# run the simulation. +sim = pe.routine.Simulation() +sim.configure( + population, + [pe.sweep.InitialInfectedSweep()], + [pe.sweep.TripleSweep(), pe.sweep.QueueSweep(), + pe.sweep.HostProgressionSweep()], + sim_params, + file_params) +sim.run_sweeps() + +# Need to close the writer object at the end of each simulation. +del sim.writer +del sim + +# Creation of a plot of results (without logging matplotlib info) +logging.getLogger("matplotlib").setLevel(logging.WARNING) +filename = os.path.join(os.path.dirname(__file__), "triple_simulation_outputs", + "output.csv") +df = pd.read_csv(filename) +df.plot(x="time", y=["InfectionStatus.Susceptible", + "InfectionStatus.InfectMild", + "InfectionStatus.Recovered"]) +plt.savefig( + os.path.join(os.path.dirname(__file__), + "triple_simulation_outputs", + "triple_simulation_flow_SIR_plot.png") +) +# Default file format is .png, but can be changed to .pdf, .svg, etc. diff --git a/python_examples/basic_infection_history_simulation/triple_simulation_outputs/output.csv b/python_examples/basic_infection_history_simulation/triple_simulation_outputs/output.csv new file mode 100644 index 00000000..f06181c2 --- /dev/null +++ b/python_examples/basic_infection_history_simulation/triple_simulation_outputs/output.csv @@ -0,0 +1,22 @@ +time,InfectionStatus.Susceptible,InfectionStatus.Exposed,InfectionStatus.InfectASympt,InfectionStatus.InfectMild,InfectionStatus.InfectGP,InfectionStatus.InfectHosp,InfectionStatus.InfectICU,InfectionStatus.InfectICURecov,InfectionStatus.Recovered,InfectionStatus.Dead,InfectionStatus.Vaccinated +0,999,0,0,1,0,0,0,0,0,0,0 +1.0,996,3,0,1,0,0,0,0,0,0,0 +2.0,996,2,1,1,0,0,0,0,0,0,0 +3.0,993,4,1,1,1,0,0,0,0,0,0 +4.0,990,7,1,1,1,0,0,0,0,0,0 +5.0,990,6,1,1,1,0,0,0,1,0,0 +6.0,987,8,1,2,1,0,0,0,1,0,0 +7.0,984,7,4,2,1,0,0,0,2,0,0 +8.0,972,18,4,2,2,0,0,0,2,0,0 +9.0,969,18,5,3,3,0,0,0,2,0,0 +10.0,960,23,6,5,4,0,0,0,2,0,0 +11.0,948,33,7,4,4,0,0,0,4,0,0 +12.0,942,31,10,5,6,1,0,0,5,0,0 +13.0,918,49,14,5,7,1,0,0,6,0,0 +14.0,900,60,14,8,9,1,0,0,8,0,0 +15.0,882,65,18,15,9,1,0,0,10,0,0 +16.0,843,95,20,16,8,1,0,0,17,0,0 +17.0,816,105,23,22,13,0,0,0,21,0,0 +18.0,766,139,24,31,16,0,0,0,24,0,0 +19.0,721,147,39,39,22,0,0,0,32,0,0 +20.0,618,224,49,48,24,2,0,0,35,0,0 diff --git a/python_examples/basic_infection_history_simulation/triple_simulation_outputs/triple_simulation_flow_SIR_plot.png b/python_examples/basic_infection_history_simulation/triple_simulation_outputs/triple_simulation_flow_SIR_plot.png new file mode 100644 index 0000000000000000000000000000000000000000..9b7d18bf4f401db6dc82f82b8e38a409a0babe08 GIT binary patch literal 27159 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 literal 0 HcmV?d00001 From 939c9a637bc6c9086b48285fd1198f79fb20eb47 Mon Sep 17 00:00:00 2001 From: Matthew Ghosh Date: Tue, 26 Nov 2024 10:02:19 +0000 Subject: [PATCH 5/6] Deleted the triple_sweep.py file (used for testing purposes) --- pyEpiabm/pyEpiabm/sweep/__init__.py | 2 -- pyEpiabm/pyEpiabm/sweep/triple_sweep.py | 35 ------------------------- 2 files changed, 37 deletions(-) delete mode 100644 pyEpiabm/pyEpiabm/sweep/triple_sweep.py diff --git a/pyEpiabm/pyEpiabm/sweep/__init__.py b/pyEpiabm/pyEpiabm/sweep/__init__.py index d9becbad..1092b531 100644 --- a/pyEpiabm/pyEpiabm/sweep/__init__.py +++ b/pyEpiabm/pyEpiabm/sweep/__init__.py @@ -21,5 +21,3 @@ from .travel_sweep import TravelSweep from .transition_matrices import StateTransitionMatrix, TransitionTimeMatrix from .initial_vaccine_sweep import InitialVaccineQueue - -from .triple_sweep import TripleSweep \ No newline at end of file diff --git a/pyEpiabm/pyEpiabm/sweep/triple_sweep.py b/pyEpiabm/pyEpiabm/sweep/triple_sweep.py deleted file mode 100644 index 59d7e52f..00000000 --- a/pyEpiabm/pyEpiabm/sweep/triple_sweep.py +++ /dev/null @@ -1,35 +0,0 @@ -# -# Infection always causes three subsequent infections. Always. -# - -import random - -from pyEpiabm.core import Person - -from .abstract_sweep import AbstractSweep - - -class TripleSweep(AbstractSweep): - - def __call__(self, time: float): - """Given a population structure, loops over infected members - and considers whether they infected household members based - on individual, and spatial infectiousness and susceptibility. - - Parameters - ---------- - time : float - Simulation time - - """ - # Infects three people at first opportunity, then never again - for cell in self._population.cells: - infectious_persons = filter(Person.is_infectious, cell.persons) - for infector in infectious_persons: - while infector.secondary_infections_counts[-1] < 3: - infectee = random.choice(infector.household.susceptible_persons) - cell.enqueue_person(infectee) - infector.increment_secondary_infections() - inf_to_exposed = (time - - infector.infection_start_times[-1]) - infectee.set_exposure_period(inf_to_exposed) From 37029ed0c4a0332d6a85d700b298d7680cc5a7fe Mon Sep 17 00:00:00 2001 From: Matthew Ghosh Date: Tue, 26 Nov 2024 10:07:03 +0000 Subject: [PATCH 6/6] Fixed test_simulation.py --- .../tests/test_unit/test_routine/test_simulation.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyEpiabm/pyEpiabm/tests/test_unit/test_routine/test_simulation.py b/pyEpiabm/pyEpiabm/tests/test_unit/test_routine/test_simulation.py index dce32821..d53d5f1f 100644 --- a/pyEpiabm/pyEpiabm/tests/test_unit/test_routine/test_simulation.py +++ b/pyEpiabm/pyEpiabm/tests/test_unit/test_routine/test_simulation.py @@ -87,7 +87,7 @@ def test_configure(self, mock_mkdir): del test_sim.ih_infectiousness_writer del test_sim.secondary_infections_writer del test_sim.serial_interval_writer - mo.assert_called_with(filename, 'w') + mo.assert_called_with(filename, 'w', newline='') @patch('os.makedirs') @patch('logging.warning') @@ -141,7 +141,7 @@ def test_configure_ih_status(self, mock_mkdir): del test_sim.ih_infectiousness_writer del test_sim.secondary_infections_writer del test_sim.serial_interval_writer - mo.assert_called_with(filename, 'w') + mo.assert_called_with(filename, 'w', newline='') @patch('os.makedirs') def test_configure_ih_infectiousness(self, mock_mkdir): @@ -173,7 +173,7 @@ def test_configure_ih_infectiousness(self, mock_mkdir): del test_sim.ih_infectiousness_writer del test_sim.secondary_infections_writer del test_sim.serial_interval_writer - mo.assert_called_with(filename, 'w') + mo.assert_called_with(filename, 'w', newline='') @patch('os.makedirs') def test_configure_secondary_infections(self, mock_mkdir): @@ -206,7 +206,7 @@ def test_configure_secondary_infections(self, mock_mkdir): del test_sim.ih_infectiousness_writer del test_sim.secondary_infections_writer del test_sim.serial_interval_writer - mo.assert_called_with(filename, 'w') + mo.assert_called_with(filename, 'w', newline='') @patch('os.makedirs') def test_configure_serial_interval(self, mock_mkdir): @@ -239,7 +239,7 @@ def test_configure_serial_interval(self, mock_mkdir): del test_sim.ih_infectiousness_writer del test_sim.secondary_infections_writer del test_sim.serial_interval_writer - mo.assert_called_with(filename, 'w') + mo.assert_called_with(filename, 'w', newline='') @patch('logging.exception') @patch('os.path.join')