From 3d481fc4df4ddc5951d1f7473f4f6d175ea35e5f Mon Sep 17 00:00:00 2001 From: Martyn Date: Thu, 8 Jul 2021 14:31:54 +0100 Subject: [PATCH 01/70] Initial commit of queueing process module --- .../queueing_processes.py | 412 ++++++++++++++++++ 1 file changed, 412 insertions(+) create mode 100644 household_contact_tracing/queueing_processes.py diff --git a/household_contact_tracing/queueing_processes.py b/household_contact_tracing/queueing_processes.py new file mode 100644 index 0000000..37c1612 --- /dev/null +++ b/household_contact_tracing/queueing_processes.py @@ -0,0 +1,412 @@ +''' +Contains queueing process objects that are used to model testing delays and probability of +not being able to get testing when there are constrained testing resources. +''' +# TODO consider moving away from dataframes for the applicants. Allocating all the memory beforehand could be restrictive,# +# Also not sure if dataframes are the fastest if we are constantly writing to them + +import pandas as pd +import numpy as np +import numpy.random as npr +from typing import Callable, List + +class Queue: + + def __init__( + self, + days_to_simulate: int, + capacity: List[int] + ): + """Creates a queueing process object that stores the current and previous states of the queue. + Also contains methods for accessing the data in the queue, and changing the data in the queue. + + Args: + days_to_simulate (int): The total number of days that will be simulated + capacity (list): The swabbing capacity (integer values) at each timepoint + max_time_in_queue (int): Maximum days from symptom onset to ineligibility for swabbing + verbose (bool, optional): If true prints some outputs. Defaults to False. + """ + + self.days_to_simulate = days_to_simulate + self.capacity = capacity + + # default values + self.time = 0 + + # initialise a dataframe that stores summaries of the queue at each timepoint + self.create_queue_df() + self.create_applicants_df() + + + def create_queue_df(self): + """ + Queue df stores summaries of the overall status of the queue at each timepoint. It is updated as the calculations progresses + """ + + # create a dataframe to store information about the overall queueing process + self.queue_df = pd.DataFrame({ + 'time': list(range(self.days_to_simulate)), + 'capacity': self.capacity + }) + + # create some empty columns for storing results + self.queue_df['new_applicants'] = '' + self.queue_df['spillover_to_next_day'] = '' + self.queue_df['total_applications_today'] = '' + self.queue_df['capacity_exceeded'] = '' + self.queue_df['capacity_exceeded_by'] = '' + self.queue_df['number_swabbed_today'] = '' + self.queue_df['number_left_queue_not_tested'] = '' + + + def create_applicants_df(self): + """Applicants df store information about everyone who has applied for a test at each timepoint. It is updated as the calculation progresses. + """ + + self.applicant_df = pd.DataFrame() + + # create empty columns for applicants + self.applicant_df['id'] = '' + self.applicant_df['swabbed'] = '' + self.applicant_df['waiting_to_be_swabbed'] = '' + self.applicant_df['left_queue_not_swabbed'] = '' + self.applicant_df['time_symptom_onset'] = '' + self.applicant_df['time_joined_queue'] = '' + self.applicant_df['time_swabbed'] = '' + self.applicant_df['time_received_result'] = '' + self.applicant_df['time_will_leave_queue'] = '' + + def add_new_applicants( + self, + ids: list, + symptom_onset_times: list, + max_time_in_queue: int + ): + """ + Adds new applicants to the queue. + """ + + queue_leaving_times = np.array(symptom_onset_times) + max_time_in_queue + + new_applicant_df = pd.DataFrame( + { + 'id': ids, + 'symptom_onset': symptom_onset_times, + 'time_will_leave_queue': queue_leaving_times + } + ) + + # initialise other columns with default values + new_applicant_df['swabbed'] = False + new_applicant_df['waiting_to_be_swabbed'] = True # default value, initially the queue is empty + new_applicant_df['left_queue_not_swabbed'] = '' + new_applicant_df['time_symptom_onset'] = '' + new_applicant_df['time_joined_queue'] = '' + new_applicant_df['time_swabbed'] = '' + new_applicant_df['time_received_result'] = '' + new_applicant_df['time_will_leave_queue'] = '' + + self.applicant_df.append(new_applicant_df, ignore_index = True) + + def swab_applicants(self, + to_be_swabbed: list, + test_processing_delays: list): + """For a list of applicants who were successful in getting thorugh the queue, update their variables associated with swabbing + Args: + to_be_swabbed (list): A list of integers, referring the rows of the applicant_dataframe that will get processed + """ + + # The columns that will be updated + columns_to_update = [ + 'waiting_to_be_swabbed', + 'time_swabbed', + 'left_queue_not_swabbed', + 'swabbed' + ] + + # record an attribtue of which individuals were swabbed today for use later + self.todays_swabbed_index = to_be_swabbed + + # update the above status to show they have been swabbed + self.applicant_df.loc[to_be_swabbed, columns_to_update] = [False, self.time, False, True] + + # work out when they receive their result, and update the data + self.applicant_df.loc[to_be_swabbed, 'time_received_result'] = self.time + test_processing_delays + + # update the queue_df table with the number of individuals processed today + self.queue_df.loc[self.queue_df.time == self.time, ['number_swabbed_today']] = len(to_be_swabbed) + + + def update_queue_leaver_status(self): + """These individuals have been in the queue too long. They are no longer trying/able to get a swab. + """ + + # These people will leave the queue today + self.leavers = (self.applicant_df.time_will_leave_queue <= self.time) & (self.applicant_df.waiting_to_be_swabbed == True) + + + # record the number of people who carry over to the next day + if self.todays_capacity > len(self.current_applicants): + spillover_to_next_day = 0 + else: + spillover_to_next_day = len(self.current_applicants) - sum(self.leavers) - self.todays_capacity + + # + self.queue_df.loc[self.time, ['spillover_to_next_day', 'number_left_queue_not_tested']] = [spillover_to_next_day, sum(self.leavers)] + + # Set their waiting to be swabbed status to False + self.applicant_df.loc[self.leavers, ['waiting_to_be_swabbed', 'left_queue_not_swabbed']] = [False, True] + + + @property + def current_applicants(self) -> int: + """Gets the number of individuals waiting to be swabbed. + + Returns: + int: The number of individuals waiting to be swabbed + """ + return list(self.applicant_df[self.applicant_df.waiting_to_be_swabbed].index) + + + @property + def todays_capacity(self) -> int: + """Gets the number of swabs that can be performed today. + + Returns: + int: The number of swabs that can be performed today + """ + return int(self.queue_df[self.queue_df.time == self.time].capacity) + + + @property + def number_swabs_performed_today(self) -> int: + """ + Gets the number of swabs that have been formed today. This will be the number of new applicants + or the swabbing capacity. + + Returns: + int: The number of swabs that have been performed today + """ + + return sum(self.applicant_df.time_swabbed == self.time) + + +# controller layout +class QueueController: + + def __init__(self): + """ + Queue controllers control how the queue is run. In some + cases you may want to run the queue on it's own, or you may want + the queue to be interacting with a branching process model in various ways. + """ + + self.completed = False + + def process_todays_new_demand(self): + pass + + def process_queue(self): + pass + + def simulate_one_day(self): + pass + + +class SimpleQueue(QueueController): + + def __init__( + self, + days_to_simulate: int, + demand: List[int], + capacity: List[int], + max_time_in_queue: int, + test_processing_delay_dist: Callable, + symptom_onset_delay_dist: Callable + ): + """A simple queueing process object that does not interact with a branching process model. + + The test demand and capacity are pre-determined, and the model works out what happens to the queue. + + Args: + days_to_simulate (int): Number of simulation steps to be performed + demand (List[int]): The number of new test seekers at each time step. + capacity (List[int]): The swabbing capacity of the queue at each time step. + max_time_in_queue (int): How long since symptom onset that an individual can remain in the queue + they become ineligible for testing + test_processing_delay_dist (Callable): A callable that returns integer test processing delays + symptom_onset_delay_dist (Callable): A callable the returns integer delays of the time from symptom onset to booking a test. + """ + + # initialise the queue + self.queue = Queue( + days_to_simulate = days_to_simulate, + capacity = capacity + ) + + # set parameters + self.demand = demand + self.test_processing_delay_dist = test_processing_delay_dist + self.symptom_onset_delay_dist = symptom_onset_delay_dist + self.max_time_in_queue = max_time_in_queue + + # ease of acccess stuff + self.queue_df = self.queue.queue_df + self.applicant_df = self.queue.applicant_df + self.time = self.queue.time + + + def add_new_test_seekers(self): + """ + Adds new test seekers to the queue. + + For this model, the new test seekers at each time point are defined a priori. + """ + + self.queue.add_new_applicants( + ids = self.demand[self.time], + symptom_onset_times = [ + self.symptom_onset_delay_dist() for _ in range(self.demand[self.time]) + ], + max_time_in_queue = self.max_time_in_queue + ) + + def process_queue(self): + """ + Performs swabbing of individuals up to capacity, and updates the dataframes that store the calculations + """ + + # Note: this method is set up so that it can be called multiple times in one day + # in case new applicants are added multiple times in a day. This is sometimes useful + + number_applicants = len(self.queue.current_applicants) + + # update queue_df with the number of applicants today + self.queue.queue_df.loc[self.queue_df.time == self.time, ['total_applications_today']] = [number_applicants] + + # how much swabbing capacity do we have remaining? The method + remaining_swabbing_capacity = self.queue.todays_capacity - self.queue.number_swabs_performed_today + + # is todays remaining capacity exceeded? + if number_applicants <= remaining_swabbing_capacity: + # if capacity not exceeded, then everyone gets swabbed + + test_delays = [ + self.test_processing_delay_dist() for _ in range(number_applicants) + ] + + self.queue.swab_applicants( + to_be_swabbed = self.queue.current_applicants, + test_processing_delays = test_delays) + + else: + # Then swabbing capacity is being exceeded. We swab up to capacity. + # We must select who gets swabbed, at the moment there is only one method + # implemented that does this, that picks a subset without replacement + + successful_applicants = npr.choice( + a = self.queue.current_applicants, + size = remaining_swabbing_capacity, + replace = False + ) + + test_delays = [ + self.test_processing_delay_dist() for _ in range(remaining_swabbing_capacity) + ] + + self.queue.swab_applicants( + to_be_swabbed = successful_applicants, + test_processing_delays = test_delays) + + def update_queue_leaver_status(self): + """These individuals have been in the queue too long. They are no longer trying/able to get a swab. + """ + + # These people will leave the queue today + self.leavers = (self.applicant_df.time_will_leave_queue <= self.time) & (self.applicant_df.waiting_to_be_swabbed == True) + + self.queue_df.loc[self.time, 'number_left_queue_not_tested'] = [sum(self.leavers)] + + # Set their waiting to be swabbed status to False + self.applicant_df.loc[self.leavers, ['waiting_to_be_swabbed', 'left_queue_not_swabbed']] = [False, True] + + # work out who will come back the next day + # not left and not swabbed + returners_index = self.applicant_df.waiting_to_be_swabbed == True + + self.queue_df.loc[self.time, 'spillover_to_next_day'] = [sum(returners_index)] + + def simulate_one_day(self, verbose: bool = True): + """ + Simulates one day of the queue. + """ + + # steps required to simulate one day + self.add_new_test_seekers() + self.update_queue_leaver_status() + self.process_queue() + + # make a nice little status update + if verbose: + print(f'Model time {self.time}, progress: {round((self.time + 1) / self.queue.days_to_simulate * 100)}%', end = '\r') + +class QueueBranchingProcessController(): + + def __init__( + self, + queue: Queue): + + self.queue = queue + + def get_todays_queue_output(self): + """ + Provides outputs from the queueing process that can be passed to a + branching process model. + + Returns: + dict: output dict, with the ids and number of swabbed individuals + """ + + swabbed_individuals = self.queue.applicant_df.loc[self.queue.todays_swabbed_index] + + output = { + 'leaving_the_queue_node_ids': self.queue.todays_leavers, + 'swabbed_individuals': swabbed_individuals + } + + return output + +class QueueAnalyzer(): + + def __init__( + self, + queue: Queue): + + self.queue = queue + self.applicant_df = queue.applicant_df + self.queue_df = queue.queue_df + + def get_prob_getting_tested(self, time_entered_queue: int): + """ + Returns the probability of getting tested if you join the queue on a specified day + + Args: + time_entered_queue (int): The day of interest + """ + valid_individuals = (self.queue.applicant_df.time_entered_queue == time_entered_queue) & (self.applicant_df.waiting_to_be_swabbed == False) + left_queue_not_swabbed = self.applicant_df[valid_individuals].left_queue_not_swabbed + return 1 - left_queue_not_swabbed.mean() + + def get_delays_for(self, time_entered_queue: int, delay_from_column: str, delay_to_column: str): + """ + Return a list of the delays between two timepoints who joined on a specified day + + Args: + time (int): The day on which the applicants joined the queue + delay_from_column (str): The earliest timepoint + delay_to_column (str): The latest timepoint + """ + day_index = (self.applicant_df.time_entered_queue == time_entered_queue) & (self.applicant_df.swabbed == True) + delay_from_column = self.applicant_df.loc[day_index, delay_from_column] + delay_to_column = self.applicant_df.loc[day_index, delay_to_column] + return delay_to_column - delay_from_column From e78a267ee5c173edeca2b8dc52dbea25c843e0d0 Mon Sep 17 00:00:00 2001 From: Martyn Date: Thu, 8 Jul 2021 16:15:58 +0100 Subject: [PATCH 02/70] Initial testing fixes --- .../queueing_processes.py | 40 +++--- test/test_queueing_processes.py | 126 ++++++++++++++++++ 2 files changed, 151 insertions(+), 15 deletions(-) create mode 100644 test/test_queueing_processes.py diff --git a/household_contact_tracing/queueing_processes.py b/household_contact_tracing/queueing_processes.py index 37c1612..29ff0b9 100644 --- a/household_contact_tracing/queueing_processes.py +++ b/household_contact_tracing/queueing_processes.py @@ -104,9 +104,8 @@ def add_new_applicants( new_applicant_df['time_joined_queue'] = '' new_applicant_df['time_swabbed'] = '' new_applicant_df['time_received_result'] = '' - new_applicant_df['time_will_leave_queue'] = '' - self.applicant_df.append(new_applicant_df, ignore_index = True) + self.applicant_df = self.applicant_df.append(new_applicant_df, ignore_index = True) def swab_applicants(self, to_be_swabbed: list, @@ -131,7 +130,7 @@ def swab_applicants(self, self.applicant_df.loc[to_be_swabbed, columns_to_update] = [False, self.time, False, True] # work out when they receive their result, and update the data - self.applicant_df.loc[to_be_swabbed, 'time_received_result'] = self.time + test_processing_delays + self.applicant_df.loc[to_be_swabbed, 'time_received_result'] = self.time + np.array(test_processing_delays) # update the queue_df table with the number of individuals processed today self.queue_df.loc[self.queue_df.time == self.time, ['number_swabbed_today']] = len(to_be_swabbed) @@ -159,11 +158,11 @@ def update_queue_leaver_status(self): @property - def current_applicants(self) -> int: - """Gets the number of individuals waiting to be swabbed. + def current_applicants(self) -> list: + """Gets the indexes of individuals waiting to be swabbed. Returns: - int: The number of individuals waiting to be swabbed + list: The indexes of individuals waiting to be swabbed """ return list(self.applicant_df[self.applicant_df.waiting_to_be_swabbed].index) @@ -213,7 +212,7 @@ def simulate_one_day(self): pass -class SimpleQueue(QueueController): +class DeterministicQueue(QueueController): def __init__( self, @@ -249,10 +248,9 @@ def __init__( self.test_processing_delay_dist = test_processing_delay_dist self.symptom_onset_delay_dist = symptom_onset_delay_dist self.max_time_in_queue = max_time_in_queue + self.days_to_simulate = days_to_simulate # ease of acccess stuff - self.queue_df = self.queue.queue_df - self.applicant_df = self.queue.applicant_df self.time = self.queue.time @@ -282,7 +280,7 @@ def process_queue(self): number_applicants = len(self.queue.current_applicants) # update queue_df with the number of applicants today - self.queue.queue_df.loc[self.queue_df.time == self.time, ['total_applications_today']] = [number_applicants] + self.queue.queue_df.loc[self.queue.queue_df.time == self.time, ['total_applications_today']] = [number_applicants] # how much swabbing capacity do we have remaining? The method remaining_swabbing_capacity = self.queue.todays_capacity - self.queue.number_swabs_performed_today @@ -323,18 +321,18 @@ def update_queue_leaver_status(self): """ # These people will leave the queue today - self.leavers = (self.applicant_df.time_will_leave_queue <= self.time) & (self.applicant_df.waiting_to_be_swabbed == True) + self.leavers = (self.queue.applicant_df.time_will_leave_queue <= self.time) & (self.queue.applicant_df.waiting_to_be_swabbed == True) - self.queue_df.loc[self.time, 'number_left_queue_not_tested'] = [sum(self.leavers)] + self.queue.queue_df.loc[self.time, 'number_left_queue_not_tested'] = [sum(self.leavers)] # Set their waiting to be swabbed status to False - self.applicant_df.loc[self.leavers, ['waiting_to_be_swabbed', 'left_queue_not_swabbed']] = [False, True] + self.queue.applicant_df.loc[self.leavers, ['waiting_to_be_swabbed', 'left_queue_not_swabbed']] = [False, True] # work out who will come back the next day # not left and not swabbed - returners_index = self.applicant_df.waiting_to_be_swabbed == True + returners_index = self.queue.applicant_df.waiting_to_be_swabbed == True - self.queue_df.loc[self.time, 'spillover_to_next_day'] = [sum(returners_index)] + self.queue.queue_df.loc[self.time, 'spillover_to_next_day'] = [sum(returners_index)] def simulate_one_day(self, verbose: bool = True): """ @@ -346,10 +344,22 @@ def simulate_one_day(self, verbose: bool = True): self.update_queue_leaver_status() self.process_queue() + self.time + # make a nice little status update if verbose: print(f'Model time {self.time}, progress: {round((self.time + 1) / self.queue.days_to_simulate * 100)}%', end = '\r') + def run_simulation(self, verbose: bool = True): + """Runs the queueing process model. + """ + + while self.time < self.days_to_simulate: + + self.simulate_one_day(verbose) + + self.time += 1 + class QueueBranchingProcessController(): def __init__( diff --git a/test/test_queueing_processes.py b/test/test_queueing_processes.py new file mode 100644 index 0000000..7fc3b10 --- /dev/null +++ b/test/test_queueing_processes.py @@ -0,0 +1,126 @@ +import pandas as pd +import pytest +from household_contact_tracing.queueing_processes import Queue, DeterministicQueue + + +# Testing the Queue object + +@pytest.fixture +def simple_Queue(): + """ + Creates a Queue object with some easy parameters for testing. + """ + + my_queue = Queue(days_to_simulate=10, capacity=[10]*10) + + return my_queue + + +@pytest.fixture +def empty_applicant_df_fixture(): + """ + Loads a fixture of an empty, correctly initialised applicant_df + """ + return pd.read_pickle('./test/fixtures/queueing_processes/empty_applicant_df.pickle') + + +def test_Queue_init_applicant_df(simple_queue, empty_applicant_df_fixture): + return pd.testing.assert_frame_equal(empty_applicant_df_fixture, simple_queue.applicant_df) + + +@pytest.fixture +def empty_queue_df_fixture(): + """ + Loads a fixture of an empty, correctly initialised applicant_df + """ + return pd.read_pickle('./test/fixtures/queueing_processes/empty_queue_df.pickle') + + +def test_Queue_init_queue_df(simple_queue, empty_queue_df_fixture): + return pd.testing.assert_frame_equal(empty_queue_df_fixture, simple_queue.queue_df) + + +@pytest.fixture +def Queue_new_applicants_fixture(): + """ + Load a fixture of an applicant df where several applicants have been added + """ + return pd.read_pickle('./test/fixtures/queueing_processes/Queue_new_applicants_fixture.pickle') + + +def test_Queue_new_applicants(simple_queue, Queue_new_applicants_fixture): + simple_queue.add_new_applicants(['A', 'B', 'C'], [1,2,3], 6) + + return pd.testing.assert_frame_equal(Queue_new_applicants_fixture, simple_queue.applicant_df) + + +@pytest.fixture +def Queue_swab_applicants_fixture(): + """Loads a fixture where some applicants have been swabbed. + """ + return pd.read_pickle('./test/fixtures/queueing_processes/Queue_swab_applicants.pickle') + +def test_Queue_swab_applicants(simple_queue, Queue_swab_applicants_fixture): + """Adds some applicants, swabs some of the and checks the applicant df + """ + simple_queue.add_new_applicants(['A', 'B', 'C'], [1,2,3], 6) + simple_queue.swab_applicants([1, 2], [1,2]) + + return pd.testing.assert_frame_equal(Queue_swab_applicants_fixture, simple_queue.applicant_df) + + +def test_Queue_current_applicants(simple_queue): + """Checks that the waiting to be swabbed indexes are returned. + + Add 3 people to the queue, swab 2 + """ + simple_queue.add_new_applicants(['A', 'B', 'C'], [1,2,3], 6) + simple_queue.swab_applicants([1, 2], [1,2]) + assert simple_queue.current_applicants == [0] + + +def test_Queue_todays_capacity(): + """Checks that the queue returns the right value for todays capacity. + """ + queue = Queue(days_to_simulate=10, capacity=list(range(10))) + + queue.time = 4 + + assert queue.todays_capacity == 4 + + +@pytest.fixture +def DeterministicQueue_add_new_test_seekers_fixture(): + return pd.read_pickle('./test/fixtures/queueing_processes/DeterministicQueue_add_new_test_seekers.pickle') + + +def test_DeterministicQueue_add_new_test_seekers(DeterministicQueue_add_new_test_seekers_fixture): + """ + Checks that the add_new_test_seekers method correctly modifies the dataframe + by adding test seekers based upon the demand + """ + def test_processing_delay_dist(): + return 1 + + def symptom_onset_delay_dist(): + return 2 + + my_det_queue = DeterministicQueue( + days_to_simulate = 10, + demand = [10]*10, + capacity = [10]*10, + max_time_in_queue = 10, + test_processing_delay_dist = test_processing_delay_dist, + symptom_onset_delay_dist = symptom_onset_delay_dist + ) + + my_det_queue.add_new_test_seekers() + + return pd.testing.assert_frame_equal(DeterministicQueue_add_new_test_seekers_fixture, my_det_queue.queue.applicant_df) + + +def test_process_queue_excess_capacity(): + pass + +def test_process_queue_excess_demand(): + pass From 62e1c59f978989e0a150f1897d7a308c337f508f Mon Sep 17 00:00:00 2001 From: Martyn Date: Thu, 8 Jul 2021 16:16:55 +0100 Subject: [PATCH 03/70] adding test fixtures --- ...eterministicQueue_add_new_test_seekers.pickle | Bin 0 -> 1266 bytes .../Queue_new_applicants_fixture.pickle | Bin 0 -> 1411 bytes .../Queue_swab_applicants.pickle | Bin 0 -> 1409 bytes .../queueing_processes/empty_applicant_df.pickle | Bin 0 -> 1629 bytes .../queueing_processes/empty_queue_df.pickle | Bin 0 -> 1936 bytes 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/fixtures/queueing_processes/DeterministicQueue_add_new_test_seekers.pickle create mode 100644 test/fixtures/queueing_processes/Queue_new_applicants_fixture.pickle create mode 100644 test/fixtures/queueing_processes/Queue_swab_applicants.pickle create mode 100644 test/fixtures/queueing_processes/empty_applicant_df.pickle create mode 100644 test/fixtures/queueing_processes/empty_queue_df.pickle diff --git a/test/fixtures/queueing_processes/DeterministicQueue_add_new_test_seekers.pickle b/test/fixtures/queueing_processes/DeterministicQueue_add_new_test_seekers.pickle new file mode 100644 index 0000000000000000000000000000000000000000..1971f3f3c2f2879a6a7bdd0a29051507c7d20c8a GIT binary patch literal 1266 zcmcIkQBM;=5N@$Ws1Snzkrxsl)FOI;#0MWtFk(#PLN&aT&0cTXJ>1)?w|mqklIR0S zlYL|Sjs6Us-Sa9~ebe3KZfg7Sbd<&-sY_u^hUY(k3suhTyj84bfhw zw7kg$hP6}Ntw;C;53n89!`3N^72Ax>XaEi}^X4gz&&$(mw*C=2ozC8OTK*C47>vd) zhnXa%d-ae<5sIu2rHq0GgLcU) zkFrETT!JX6(y(&0>^~__NRG)U2TcT!f3F@UQZfnr7%KGQRS)DOvJK@b+bPb70etG+A8fqP#nf!ONUrZzk`#H4L#lMOIzIRQwh`#XX75zAuLQ7zUgqKWD=6zg`T zcZJ|*zY$z>1n<5r|H#_&fAvPXb~>hcAJbh|(6BYV$CGtFS+<5^|9|grVS2mLd1e>I zP3i@)G@VYJ_;$&NfV*_JsuKSdF#i+K7Wc$`@j$f2hIlA8#Ut_9xvOq~6ST8i!S<72 zyX=2X4U3{>XCv3(8i_v&$<7{gS-6uOJ4X|O(IHKNxu{5}M-_=pEt9Tmvyhk%>+aVm zTrP>&6GtKqYosr=P-qH(CJpr^4I$U})j(n?814tbA>GK0p;6-kWIf6IUR>o;W<$;5 SVnF`50Va6_QscQUqTmO1l}{*V3#I=e?pE1+D{ySdr#yP5rFc0QNC?Yjkg>A4P#n^by@BxPPJr4a)? z^OP$3%-YAW`UMW4rpqL1r=VBH;bE*;8dD*?h{m+dQVII%mPi^OUghs#>9t+tFHI7g zY@bOlpc2DXNzA(BMT|Zq@GIIK+BR?#lL5UDXHln@rx|5Jg)~iR595)PHIi7WG;1iN zYu5rnPvf>Mt-BivK?I|FY{;ISjDSG`U5-s< z>)8=Xz-RUHdDTd1+&1OdG0(9Q@U^;jrJ|`qC3RVe< zPY5-Bo8Pg?|DWTII5ob?@A7;6{w0~C^d4L?y%E71C$Iz1Q)}M(LvKAFClt>=@T#5) z%)F3*{euGOsXZz(v$CNpIB*Cq;!vpsS%^nIM$?rdY?xE)7w{xIrqeQ`?05d0|KLB- zSDNtFt+#}Q9>>15UM&8E_nr05_|+3H%OsWrM;{IfoQh@;4JRatn4)$v?P5+;DV%*~ fi=-uJ+nS41h@>k?GwWc+R52oGz=VXp%mVKZ`t}qZ literal 0 HcmV?d00001 diff --git a/test/fixtures/queueing_processes/Queue_swab_applicants.pickle b/test/fixtures/queueing_processes/Queue_swab_applicants.pickle new file mode 100644 index 0000000000000000000000000000000000000000..cdf3446460afbe885ed2a03b166aa3346c042ed5 GIT binary patch literal 1409 zcma)6O>fgc5Ve!IO#>AwYH53^!~qCjRx5GffYj0=1Xu*M5+_ux*2yMo@JH~j6CzOw z4lS{?w+;u8_>cSpm|Zt%LJQPU?D6b-p4m6E^QrQ+=UC=a3tbwwsBoJ}%G`EJBL-^j z36=Dzv5(>QXE=bms*tFYf?At|2eD*nOhe&DG^QPv3Q(7~!=(A{W$_M%Uf)Ol$~3XX zJ}}|>RA9IoiCLdKkI{z&en~ka+XQZ5GN6{?Eb0!5G@~q(0Zmgnz<4BO%_J5w&6*PF z$}vDtv$!n-IR?)N^kx^Ptn5#k0>S0)j` zq?rV@V4QE0AZD$CgqtdWqd<%175B-49)eOPPymc?-2EU7NyuoAjo9qdz*s(w(uhQXHk9o5j=GDxTFXz-1%MJimgi%oomX+D5swKKusN zqO6Z9^jAXZgiz-<`7M+D|2fW>Q|Gt&9e$VJyC8F%-u(-vHzv4~1a<&wX5HO*;BFMd zgyQK3UQ;u^o)!YIdr%-X)1zUgS2k4@#|^l3-G|lXgVScn)=LQoo&z7eU@ou zzwu}MJO6>c@|3q>yk#u(B=)uOV)3WEZ;W@&tDSgRCa@$p_;5_%Ow?m&G#^RCBsFts nA9JEo;^fm?B<+xPjJZt1FzE}@%DR{_m5hYcXQ6CrpM+`NJdyf0VKqtm$kp z&0e_p$gY01yS8r{G#;nc+&EhvNe!uFLisV1Yz(Qg=HfGv3_rXo*4EMc+sMCGCWi0{ zlpipK?Uty7DSassLju2Gn)BE-4)HM7^kf!KX2mh%Oz4QEDVt$Gn!+%VN~hUSBi%Gz zp*4#*mMb~3q_$_Ddx8CCw``wyq_byv&oo0ln?O#fzkX~F4~p!9@jlzZV6gccANL|p zwG9sQ8R}7p`>H7m0>G#@Ws#2LnCgTEKu_?i0!EsC%pe0Q6MdSvqN5n=X7NNPF~viJ zwx;XA_emrnEJ!$1Q8;h3?3OquL_1iNf$C3FZ zei@bIUB1j$axM7vxf1!}CeDTw{-CG^ob-I5M{4`0rRVnfKhjQ`zl-S|i^*@XGkX3* z{S(6Ns)RdL3H_>s)vAQMRS9cV3HPcJ?w=*Vt~ITP{`w<-y)ZO}4rKX~X$84sDU1I- z-V)Q=VIs>jPfQyvk75#StsP_$nvTS2`!8iC%{tK~s3EgyP zoh+2lHx6AOjS~9Sp$*b3q3;~pB&`w}IJ89;ODK2fB59XU>CiUm9HWpa=Moex5dA*4 v*tz%97&LR9yN$CltF7wXu?tt25Xn^0Fq@zfOam1x0HN%G$^!onaQtNN literal 0 HcmV?d00001 diff --git a/test/fixtures/queueing_processes/empty_queue_df.pickle b/test/fixtures/queueing_processes/empty_queue_df.pickle new file mode 100644 index 0000000000000000000000000000000000000000..7c3e80a198c8f7332d9b7cb7a9672a1f3573edfd GIT binary patch literal 1936 zcmeHI&1)1f6rb6h*)Oe9XlV;l@Sv_=TMHgMh=LWNMpx;ENC@Q8>(nC5h_m6I{m zs{52k^33`t`sOFSulvfw<58~F^6BLv7j+Lbn^%4$_cCfIjaLu!@zG?qsoW2GI2=Cz z37`E#kEITdO%9%njXa%@WF(9woPNnfPO>bCL&7DbKQFR4Nz!eaW0^u;yAnqx%te_> zl2l7&3=zWuJ2hKvhv{xeX+$Hf=FVX_fL-Pww*a;lJ0u8bRPAu3gcG_Y@%w@n6!TPK zNri;XD1@&(Zf;y#&)tiIs3n-hW03!!AkZX!YVea@F zTW8O;@VgB_2~rY@6DItcaA*2#rX1D Date: Thu, 8 Jul 2021 16:17:57 +0100 Subject: [PATCH 04/70] initial code to make queueing process fixtures --- .../queueing_processes/update_fixtures.ipynb | 530 ++++++++++++++++++ 1 file changed, 530 insertions(+) create mode 100644 test/fixtures/queueing_processes/update_fixtures.ipynb diff --git a/test/fixtures/queueing_processes/update_fixtures.ipynb b/test/fixtures/queueing_processes/update_fixtures.ipynb new file mode 100644 index 0000000..140297d --- /dev/null +++ b/test/fixtures/queueing_processes/update_fixtures.ipynb @@ -0,0 +1,530 @@ +{ + "metadata": { + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.10" + }, + "orig_nbformat": 4, + "kernelspec": { + "name": "python3", + "display_name": "Python 3.7.10 64-bit (conda)" + }, + "interpreter": { + "hash": "7492ded05b103f4219da6ddb5c402bdc2a09c917dea74c75989a286e893175e7" + } + }, + "nbformat": 4, + "nbformat_minor": 2, + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from household_contact_tracing.queueing_processes import Queue, DeterministicQueue\n", + "import pickle" + ] + }, + { + "source": [ + "This notebook is used to create the fixtures for test_queueing_processes.py." + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "source": [ + "## Queue" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "my_queue = Queue(days_to_simulate=10, capacity=[10]*10)" + ] + }, + { + "source": [ + "### Initialised applicant df" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "Empty DataFrame\n", + "Columns: [id, swabbed, waiting_to_be_swabbed, left_queue_not_swabbed, time_symptom_onset, time_joined_queue, time_swabbed, time_received_result, time_will_leave_queue]\n", + "Index: []" + ], + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
idswabbedwaiting_to_be_swabbedleft_queue_not_swabbedtime_symptom_onsettime_joined_queuetime_swabbedtime_received_resulttime_will_leave_queue
\n
" + }, + "metadata": {}, + "execution_count": 3 + } + ], + "source": [ + "my_queue.applicant_df" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "#my_queue.applicant_df.to_pickle('./test/fixtures/queueing_processes/empty_applicant_df.pickle')" + ] + }, + { + "source": [ + "### Initialised queue_df" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " time capacity new_applicants spillover_to_next_day \\\n", + "0 0 10 \n", + "1 1 10 \n", + "2 2 10 \n", + "3 3 10 \n", + "4 4 10 \n", + "5 5 10 \n", + "6 6 10 \n", + "7 7 10 \n", + "8 8 10 \n", + "9 9 10 \n", + "\n", + " total_applications_today capacity_exceeded capacity_exceeded_by \\\n", + "0 \n", + "1 \n", + "2 \n", + "3 \n", + "4 \n", + "5 \n", + "6 \n", + "7 \n", + "8 \n", + "9 \n", + "\n", + " number_swabbed_today number_left_queue_not_tested \n", + "0 \n", + "1 \n", + "2 \n", + "3 \n", + "4 \n", + "5 \n", + "6 \n", + "7 \n", + "8 \n", + "9 " + ], + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
timecapacitynew_applicantsspillover_to_next_daytotal_applications_todaycapacity_exceededcapacity_exceeded_bynumber_swabbed_todaynumber_left_queue_not_tested
0010
1110
2210
3310
4410
5510
6610
7710
8810
9910
\n
" + }, + "metadata": {}, + "execution_count": 5 + } + ], + "source": [ + "my_queue.queue_df" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "#my_queue.queue_df.to_pickle('./test/fixtures/queueing_processes/empty_queue_df.pickle')" + ] + }, + { + "source": [ + "### add_new_applicants" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "my_queue.add_new_applicants(['A', 'B', 'C'], [1,2,3], 6)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " id swabbed waiting_to_be_swabbed left_queue_not_swabbed time_symptom_onset \\\n", + "0 A False True \n", + "1 B False True \n", + "2 C False True \n", + "\n", + " time_joined_queue time_swabbed time_received_result time_will_leave_queue \\\n", + "0 7 \n", + "1 8 \n", + "2 9 \n", + "\n", + " symptom_onset \n", + "0 1.0 \n", + "1 2.0 \n", + "2 3.0 " + ], + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
idswabbedwaiting_to_be_swabbedleft_queue_not_swabbedtime_symptom_onsettime_joined_queuetime_swabbedtime_received_resulttime_will_leave_queuesymptom_onset
0AFalseTrue71.0
1BFalseTrue82.0
2CFalseTrue93.0
\n
" + }, + "metadata": {}, + "execution_count": 8 + } + ], + "source": [ + "my_queue.applicant_df" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "# my_queue.applicant_df.to_pickle('./test/fixtures/queueing_processes/Queue_new_applicants_fixture.pickle')" + ] + }, + { + "source": [ + "### swab_applicants" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [], + "source": [ + "my_queue = Queue(days_to_simulate=10, capacity=[10]*10)\n", + "my_queue.add_new_applicants(['A', 'B', 'C'], [1,2,3], 6)\n", + "my_queue.swab_applicants([1, 2], [1,2])" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " id swabbed waiting_to_be_swabbed left_queue_not_swabbed time_symptom_onset \\\n", + "0 A False True \n", + "1 B True False False \n", + "2 C True False False \n", + "\n", + " time_joined_queue time_swabbed time_received_result time_will_leave_queue \\\n", + "0 7 \n", + "1 0 1 8 \n", + "2 0 2 9 \n", + "\n", + " symptom_onset \n", + "0 1.0 \n", + "1 2.0 \n", + "2 3.0 " + ], + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
idswabbedwaiting_to_be_swabbedleft_queue_not_swabbedtime_symptom_onsettime_joined_queuetime_swabbedtime_received_resulttime_will_leave_queuesymptom_onset
0AFalseTrue71.0
1BTrueFalseFalse0182.0
2CTrueFalseFalse0293.0
\n
" + }, + "metadata": {}, + "execution_count": 22 + } + ], + "source": [ + "my_queue.applicant_df" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "# my_queue.applicant_df.to_pickle('./test/fixtures/queueing_processes/Queue_swab_applicants.pickle')" + ] + }, + { + "source": [ + "## Deterministic Queue" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [], + "source": [ + "### Add new test seekers" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "def test_processing_delay_dist():\n", + " return 1\n", + "\n", + "def symptom_onset_delay_dist():\n", + " return 2 \n", + "\n", + "my_det_queue = DeterministicQueue(\n", + " days_to_simulate = 10,\n", + " demand = [10]*10,\n", + " capacity = [10]*10,\n", + " max_time_in_queue = 10,\n", + " test_processing_delay_dist = test_processing_delay_dist,\n", + " symptom_onset_delay_dist = symptom_onset_delay_dist\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "my_det_queue.add_new_test_seekers()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " id swabbed waiting_to_be_swabbed left_queue_not_swabbed time_symptom_onset \\\n", + "0 10 False True \n", + "1 10 False True \n", + "2 10 False True \n", + "3 10 False True \n", + "4 10 False True \n", + "5 10 False True \n", + "6 10 False True \n", + "7 10 False True \n", + "8 10 False True \n", + "9 10 False True \n", + "\n", + " time_joined_queue time_swabbed time_received_result time_will_leave_queue \\\n", + "0 12 \n", + "1 12 \n", + "2 12 \n", + "3 12 \n", + "4 12 \n", + "5 12 \n", + "6 12 \n", + "7 12 \n", + "8 12 \n", + "9 12 \n", + "\n", + " symptom_onset \n", + "0 2.0 \n", + "1 2.0 \n", + "2 2.0 \n", + "3 2.0 \n", + "4 2.0 \n", + "5 2.0 \n", + "6 2.0 \n", + "7 2.0 \n", + "8 2.0 \n", + "9 2.0 " + ], + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
idswabbedwaiting_to_be_swabbedleft_queue_not_swabbedtime_symptom_onsettime_joined_queuetime_swabbedtime_received_resulttime_will_leave_queuesymptom_onset
010FalseTrue122.0
110FalseTrue122.0
210FalseTrue122.0
310FalseTrue122.0
410FalseTrue122.0
510FalseTrue122.0
610FalseTrue122.0
710FalseTrue122.0
810FalseTrue122.0
910FalseTrue122.0
\n
" + }, + "metadata": {}, + "execution_count": 8 + } + ], + "source": [ + "my_det_queue.queue.applicant_df" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "my_det_queue.queue.applicant_df.to_pickle('./test/fixtures/queueing_processes/DeterministicQueue_add_new_test_seekers.pickle')" + ] + }, + { + "source": [ + "### test_process_queue_excess_capacity" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "def test_processing_delay_dist():\n", + " return 1\n", + "\n", + "def symptom_onset_delay_dist():\n", + " return 2 \n", + "\n", + "my_det_queue = DeterministicQueue(\n", + " days_to_simulate = 10,\n", + " demand = [5]*10,\n", + " capacity = [10]*10,\n", + " max_time_in_queue = 10,\n", + " test_processing_delay_dist = test_processing_delay_dist,\n", + " symptom_onset_delay_dist = symptom_onset_delay_dist\n", + ")\n", + "\n", + "my_det_queue.add_new_test_seekers()\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " id swabbed waiting_to_be_swabbed left_queue_not_swabbed time_symptom_onset \\\n", + "0 5 False True \n", + "1 5 False True \n", + "2 5 False True \n", + "3 5 False True \n", + "4 5 False True \n", + "\n", + " time_joined_queue time_swabbed time_received_result time_will_leave_queue \\\n", + "0 12 \n", + "1 12 \n", + "2 12 \n", + "3 12 \n", + "4 12 \n", + "\n", + " symptom_onset \n", + "0 2.0 \n", + "1 2.0 \n", + "2 2.0 \n", + "3 2.0 \n", + "4 2.0 " + ], + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
idswabbedwaiting_to_be_swabbedleft_queue_not_swabbedtime_symptom_onsettime_joined_queuetime_swabbedtime_received_resulttime_will_leave_queuesymptom_onset
05FalseTrue122.0
15FalseTrue122.0
25FalseTrue122.0
35FalseTrue122.0
45FalseTrue122.0
\n
" + }, + "metadata": {}, + "execution_count": 11 + } + ], + "source": [ + "my_det_queue.queue.applicant_df" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "my_det_queue.process_queue()" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " id swabbed waiting_to_be_swabbed left_queue_not_swabbed time_symptom_onset \\\n", + "0 5 True False False \n", + "1 5 True False False \n", + "2 5 True False False \n", + "3 5 True False False \n", + "4 5 True False False \n", + "\n", + " time_joined_queue time_swabbed time_received_result time_will_leave_queue \\\n", + "0 0 1 12 \n", + "1 0 1 12 \n", + "2 0 1 12 \n", + "3 0 1 12 \n", + "4 0 1 12 \n", + "\n", + " symptom_onset \n", + "0 2.0 \n", + "1 2.0 \n", + "2 2.0 \n", + "3 2.0 \n", + "4 2.0 " + ], + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
idswabbedwaiting_to_be_swabbedleft_queue_not_swabbedtime_symptom_onsettime_joined_queuetime_swabbedtime_received_resulttime_will_leave_queuesymptom_onset
05TrueFalseFalse01122.0
15TrueFalseFalse01122.0
25TrueFalseFalse01122.0
35TrueFalseFalse01122.0
45TrueFalseFalse01122.0
\n
" + }, + "metadata": {}, + "execution_count": 13 + } + ], + "source": [ + "my_det_queue.queue.applicant_df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ] +} \ No newline at end of file From 3c3375b5cfab138332cffbfa906e02746e245fe9 Mon Sep 17 00:00:00 2001 From: Martyn Date: Thu, 8 Jul 2021 21:05:48 +0100 Subject: [PATCH 05/70] added growth_rate_view --- household_contact_tracing/branching_process_controller.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/household_contact_tracing/branching_process_controller.py b/household_contact_tracing/branching_process_controller.py index 60965f9..39a772b 100644 --- a/household_contact_tracing/branching_process_controller.py +++ b/household_contact_tracing/branching_process_controller.py @@ -1,9 +1,11 @@ +from household_contact_tracing.views.growth_rate_view import GrowthRateView from household_contact_tracing.branching_process_model import BranchingProcessModel from household_contact_tracing.views.shell_view import ShellView from household_contact_tracing.views.csv_file_view import CSVFileView from household_contact_tracing.views.graph_view import GraphView from household_contact_tracing.views.graph_pyvis_view import GraphPyvisView from household_contact_tracing.views.timeline_graph_view import TimelineGraphView +from household_contact_tracing.views.growth_rate_view import GrowthRateView class BranchingProcessController: @@ -47,6 +49,7 @@ def __init__(self, model: BranchingProcessModel): self.timeline_view = TimelineGraphView(model) self.shell_view = ShellView(model) self.csv_view = CSVFileView(model) + self.growth_rate_view = GrowthRateView(model) self.set_graphic_displays(False) From eb715dc53f6f6eef07ceb20da00c3793c4981ede Mon Sep 17 00:00:00 2001 From: Martyn Date: Thu, 8 Jul 2021 21:06:46 +0100 Subject: [PATCH 06/70] started making growth_rate_view --- .../views/growth_rate_view.py | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 household_contact_tracing/views/growth_rate_view.py diff --git a/household_contact_tracing/views/growth_rate_view.py b/household_contact_tracing/views/growth_rate_view.py new file mode 100644 index 0000000..fef95c0 --- /dev/null +++ b/household_contact_tracing/views/growth_rate_view.py @@ -0,0 +1,96 @@ +# Code to estimate the growth rate of a simulated branching process + +from household_contact_tracing.views.branching_process_view import BranchingProcessView +from household_contact_tracing.branching_process_model import BranchingProcessModel + +class GrowthRateView(BranchingProcessView): + + """ + View that estimates the growth rate of a completed simulation + """ + + def __init__(self, model: BranchingProcessModel): + + self._model = model + + def model_state_change(self, subject: BranchingProcessModel): + """ + Respond to changes in model state (e.g. running, extinct, timed-out) + + Parameters: + subject (BranchingProcessModel): The branching process model being displayed by this simulation view. + + Returns: + None + """ + pass + + def model_step_increment(self, subject: BranchingProcessModel): + """ + Respond to single step increment in simulation + + Parameters: + subject (BranchingProcessModel): The branching process model being displayed by this simulation view. + + Returns: + None + """ + pass + + def model_simulation_stopped(self, subject: BranchingProcessModel): + """ + Respond to end of simulation run + + Parameters: + subject (BranchingProcessModel): The branching process model being displayed by this simulation view. + + Returns: + None + """ + pass + + def graph_change(self, subject: BranchingProcessModel): + """ + Respond to changes in graph (nodes/households network) + + Parameters: + subject (SimulationModel): The branching process model being displayed by this simulation view. + + Returns: + None + """ + pass + + def set_display(self, show: bool): + """ + Sets whether this pyvis graph view is displayed or not. + + Parameters: + show (bool): To display this view, set to True + + Returns: + None + """ + pass + + def get_infection_times(self): + """ + Returns a list containing the times at which each node was infected + """ + return [node.time_infected for node in self._model.network.all_nodes()] + + def get_daily_incidence(self): + """Returns a list of the new infections at each time point. + + The list contains [time, incidence] pairs + """ + + infection_times = self.get_infection_times() + return([ + infection_times.count(t) + for t in range(self._model.time) + ]) + + def estimate_growth_rate(self): + + print('estimating growth rate') From ed8e4893ccf3fa40bdad81ee04bc0a83d2d43ab0 Mon Sep 17 00:00:00 2001 From: Martyn Date: Fri, 9 Jul 2021 08:45:00 +0100 Subject: [PATCH 07/70] Added some error handling --- household_contact_tracing/exceptions.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 household_contact_tracing/exceptions.py diff --git a/household_contact_tracing/exceptions.py b/household_contact_tracing/exceptions.py new file mode 100644 index 0000000..03d86db --- /dev/null +++ b/household_contact_tracing/exceptions.py @@ -0,0 +1,21 @@ +# error handling module + +from household_contact_tracing.branching_process_state import BranchingProcessState + +class Error(Exception): + """Base class for exceptions in this module""" + pass + + +class ModelStateError(Error): + """Exception raised when model is in an inapropriate state + when a method or function is called. + + Args: + state (BranchingProcessState): the state of the model when the error occurred. + message (str): explanation of why the state was incorrect + """ + + def __init__(self, state: BranchingProcessState, message: str): + self.state = state + self.message = message From 88180296623545047885444066affb3ca36d007c Mon Sep 17 00:00:00 2001 From: Martyn Date: Fri, 9 Jul 2021 08:58:36 +0100 Subject: [PATCH 08/70] reanming parameter --- .../branching_process_controller.py | 8 ++-- .../branching_process_model.py | 6 +-- .../branching_process_models.py | 10 ++--- .../views/growth_rate_view.py | 39 +++++++++++++++++-- 4 files changed, 48 insertions(+), 15 deletions(-) diff --git a/household_contact_tracing/branching_process_controller.py b/household_contact_tracing/branching_process_controller.py index 39a772b..1a1c019 100644 --- a/household_contact_tracing/branching_process_controller.py +++ b/household_contact_tracing/branching_process_controller.py @@ -28,7 +28,7 @@ class BranchingProcessController: set_graphic_displays(self, display: bool) choose whether to show the graphical outputs - run_simulation(self, max_time: int = 20, infection_threshold: int = 5000) + run_simulation(self, max_time: int = 20, max_active_infections: int = 5000) runs the simulation """ @@ -72,15 +72,15 @@ def set_graphic_displays(self, display: bool): self.graph_pyvis_view.set_display(display) self.timeline_view.set_display(display) - def run_simulation(self, max_time: int = 20, infection_threshold: int = 5000): + def run_simulation(self, max_time: int = 20, max_active_infections: int = 5000): """ Run the simulation until it stops (e.g times out, too many infectious nodes or goes extinct) Parameters: max_time (int): The maximum number of iterations (eg. days) to be run (simulation stops if reached) - infection_threshold (int): The maximum number of infectious nodes (simulation stops if reached) + max_active_infections (int): The maximum number of infectious nodes (simulation stops if reached) Returns: None """ - self._model.run_simulation(max_time, infection_threshold) + self._model.run_simulation(max_time, max_active_infections) diff --git a/household_contact_tracing/branching_process_model.py b/household_contact_tracing/branching_process_model.py index b5d9c79..b6cc399 100644 --- a/household_contact_tracing/branching_process_model.py +++ b/household_contact_tracing/branching_process_model.py @@ -20,7 +20,7 @@ class BranchingProcessModel(ABC, Parameterised): Methods ------- - run_simulation(self, max_time: int, infection_threshold: int) -> None: + run_simulation(self, max_time: int, max_active_infections: int) -> None: runs the simulation """ @@ -74,13 +74,13 @@ def root_dir(self) -> str: return self.__ROOT_DIR @abstractmethod - def run_simulation(self, max_time: int, infection_threshold: int) -> None: + def run_simulation(self, max_time: int, max_active_infections: int) -> None: """ Run the simulation until it stops (e.g times out, too many infectious nodes or goes extinct) Parameters: max_time (int): The maximum number of iterations (eg. days) to be run (simulation stops if reached) - infection_threshold (int): The maximum number of infectious nodes (simulation stops if reached) + max_active_infections (int): The maximum number of infectious nodes (simulation stops if reached) Returns: None diff --git a/household_contact_tracing/branching_process_models.py b/household_contact_tracing/branching_process_models.py index 19c5899..1e98fee 100644 --- a/household_contact_tracing/branching_process_models.py +++ b/household_contact_tracing/branching_process_models.py @@ -32,7 +32,7 @@ class HouseholdLevelTracing(BranchingProcessModel): Methods ------- - run_simulation(self, max_time: int, infection_threshold: int = 1000) -> None + run_simulation(self, max_time: int, max_active_infections: int = 1000) -> None Runs the simulation up to a maximum number of increments and max allowed number of infected nodes. @@ -100,7 +100,7 @@ def simulate_one_step(self): # increment time self.time += 1 - def run_simulation(self, max_time: int, infection_threshold: int = 1000) -> None: + def run_simulation(self, max_time: int, max_active_infections: int = 1000) -> None: """ Runs the simulation: Sets model state, Announces start/stopped and step increments to observers @@ -108,7 +108,7 @@ def run_simulation(self, max_time: int, infection_threshold: int = 1000) -> None Arguments: max_time -- The maximum number of step increments to perform (stops if self.time >= max_time). Self.time is cumulative throughout multiple calls to run_simulation. - infection_threshold -- The maximum number of infectious nodes allowed, + max_active_infections -- The maximum number of infectious nodes allowed, before stopping simulation Returns: @@ -116,7 +116,7 @@ def run_simulation(self, max_time: int, infection_threshold: int = 1000) -> None """ # Switch model to RunningState - self._state.switch(RunningState, max_time=max_time, infection_threshold=infection_threshold) + self._state.switch(RunningState, max_time=max_time, max_active_infections=max_active_infections) while type(self.state) is RunningState: prev_network = deepcopy(self.network) @@ -144,7 +144,7 @@ def run_simulation(self, max_time: int, infection_threshold: int = 1000) -> None total_increments=self.time, non_recovered_nodes=0, total_nodes=self.network.node_count) - elif self.network.count_non_recovered_nodes() > infection_threshold: + elif self.network.count_non_recovered_nodes() > max_active_infections: # Simulation ends if number of infectious nodes > threshold self.state.switch(MaxNodesInfectiousState, total_increments=self.time, diff --git a/household_contact_tracing/views/growth_rate_view.py b/household_contact_tracing/views/growth_rate_view.py index fef95c0..6c91884 100644 --- a/household_contact_tracing/views/growth_rate_view.py +++ b/household_contact_tracing/views/growth_rate_view.py @@ -1,7 +1,8 @@ # Code to estimate the growth rate of a simulated branching process - from household_contact_tracing.views.branching_process_view import BranchingProcessView from household_contact_tracing.branching_process_model import BranchingProcessModel +from household_contact_tracing.branching_process_state import MaxNodesInfectiousState, ReadyState, RunningState, ExtinctState +from household_contact_tracing.exceptions import Error, ModelStateError class GrowthRateView(BranchingProcessView): @@ -91,6 +92,38 @@ def get_daily_incidence(self): for t in range(self._model.time) ]) - def estimate_growth_rate(self): + def estimate_growth_rate(self, discard_first_n_days: int = 10): + """Uses Poisson regression to estimate the growth rate of the epidemic. + The first few days of a simulation are typically discarded while the process becomes mixed + after it's artificial initial conditions + + Args: + discard_first_n_days (int, optional): estimate growth rate from data after the first n days. Defaults to 10. + """ + + if isinstance(self._model.state, ReadyState): + raise ModelStateError(self._model.state, 'Simulation has not started yet. Cannot estimate growth rate.') + + # we work out how many + time = self._model.time + elligible_dates = time - discard_first_n_days + + if elligible_dates < 2: + # there is not enough data to estimate the growth rate + + if isinstance(self._model.state, RunningState): + raise Error("""Cannot estimate growth rate due to insufficient elligible dates. + This simulation is still running, consider continuing the simulation before estimating the growth rate.""") + + if isinstance(self._model.state, ExtinctState): + raise Error("""Cannot estimate growth rate due to insufficient elligible dates. + This simulation went extinct, possibly before discard_first_n_days. Consider starting the simulation with more infections""") + + if isinstance(self._model.state, MaxNodesInfectiousState): + raise Error("""Cannot estimate growth rate due to insufficient elligible dates. + This simulation exceeded the maximum number of infectious nodes. Consider raising""") + + + + - print('estimating growth rate') From cb4a0705abf9323438a2cb40e9360df1a0c306ec Mon Sep 17 00:00:00 2001 From: Martyn Date: Fri, 9 Jul 2021 10:15:37 +0100 Subject: [PATCH 09/70] adding statsmodels to requirements --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f01fa6b..2230c84 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,5 @@ loguru pyyaml pandas beautifulsoup4==4.9.3 -pyvis==0.1.9 \ No newline at end of file +pyvis==0.1.9 +statsmodels From 43bf4246746346ce9cc5fa3ef170c7cde1b24eb4 Mon Sep 17 00:00:00 2001 From: Martyn Date: Fri, 9 Jul 2021 10:16:11 +0100 Subject: [PATCH 10/70] Finalised growth rate view --- .../views/growth_rate_view.py | 76 ++++++++++++++++--- 1 file changed, 66 insertions(+), 10 deletions(-) diff --git a/household_contact_tracing/views/growth_rate_view.py b/household_contact_tracing/views/growth_rate_view.py index 6c91884..8289214 100644 --- a/household_contact_tracing/views/growth_rate_view.py +++ b/household_contact_tracing/views/growth_rate_view.py @@ -3,7 +3,9 @@ from household_contact_tracing.branching_process_model import BranchingProcessModel from household_contact_tracing.branching_process_state import MaxNodesInfectiousState, ReadyState, RunningState, ExtinctState from household_contact_tracing.exceptions import Error, ModelStateError - +from sklearn import linear_model +import statsmodels.api as sm +import numpy as np class GrowthRateView(BranchingProcessView): """ @@ -92,7 +94,7 @@ def get_daily_incidence(self): for t in range(self._model.time) ]) - def estimate_growth_rate(self, discard_first_n_days: int = 10): + def _estimate_growth_rate(self, discard_first_n_days: int = 10, verbose = True): """Uses Poisson regression to estimate the growth rate of the epidemic. The first few days of a simulation are typically discarded while the process becomes mixed after it's artificial initial conditions @@ -106,24 +108,78 @@ def estimate_growth_rate(self, discard_first_n_days: int = 10): # we work out how many time = self._model.time - elligible_dates = time - discard_first_n_days + num_eligible_dates = time - discard_first_n_days - if elligible_dates < 2: + if num_eligible_dates < 2: # there is not enough data to estimate the growth rate if isinstance(self._model.state, RunningState): - raise Error("""Cannot estimate growth rate due to insufficient elligible dates. + raise Error("""Cannot estimate growth rate due to insufficient eligible dates. This simulation is still running, consider continuing the simulation before estimating the growth rate.""") - if isinstance(self._model.state, ExtinctState): - raise Error("""Cannot estimate growth rate due to insufficient elligible dates. + elif isinstance(self._model.state, ExtinctState): + raise Error("""Cannot estimate growth rate due to insufficient eligible dates. This simulation went extinct, possibly before discard_first_n_days. Consider starting the simulation with more infections""") - if isinstance(self._model.state, MaxNodesInfectiousState): - raise Error("""Cannot estimate growth rate due to insufficient elligible dates. - This simulation exceeded the maximum number of infectious nodes. Consider raising""") + elif isinstance(self._model.state, MaxNodesInfectiousState): + raise Error("""Cannot estimate growth rate due to insufficient eligible dates. + This simulation exceeded the maximum number of infectious nodes. Consider raising max_active_infections when simulating.""") + + else: + raise Error("""Cannot estimate growth rate due to insufficient eligible dates.""") + + else: + # there is some data that can be used to estimate the growth rate. Perform analysis + + if verbose: + print(f'Estimating growth rate using {num_eligible_dates} time periods') + + # the incidence after the first n days + y = self.get_daily_incidence()[discard_first_n_days:] + + # create a simple design matrix + X = [[t] for t in range(num_eligible_dates)] + X = sm.add_constant(X, prepend=False) + + glm_poisson = sm.GLM(y, X, family=sm.families.Poisson()) + self.glm_poisson = glm_poisson.fit() + + + def get_growth_rate(self, discard_first_n_days: int = 10, verbose: bool = True): + """Returns the growth rate of the simulated epidemic, estimated using poisson regression. + + The first few days of a simulation are typically discarded while the process becomes mixed + after it's artificial initial conditions. + Args: + discard_first_n_days (int, optional): estimate growth rate from data after the first n days. Defaults to 10. + """ + self._estimate_growth_rate(discard_first_n_days, verbose) + + return self.glm_poisson.params[0] + def growth_rate_summary(self, discard_first_n_days: int = 10, alpha: float = 0.05): + """Returns the growth rate of the simulated epidemic, estimated using poisson regression. + The first few days of a simulation are typically discarded while the process becomes mixed + after it's artificial initial conditions. + Args: + discard_first_n_days (int, optional): estimate growth rate from data after the first n days. Defaults to 10. + """ + + self._estimate_growth_rate(discard_first_n_days, verbose = False) + + num_eligible_dates = self._model.time - discard_first_n_days + growth_rate = self.glm_poisson.params[0] + growth_rate_ci = self.glm_poisson.conf_int(alpha = alpha, cols = [0])[0] + doubling_time = np.log(2) / np.log(1 + growth_rate) + doubling_time_ci = np.log(2) / np.log(1 + np.array(growth_rate_ci)) + + print('GLM regression summary:') + print(self.glm_poisson.summary()) + print(f'{num_eligible_dates} time periods were used to estimate the growth rate.') + print(f'The estimated growth rate was {round(growth_rate*100, 2)}% ({100*(1-alpha)}% CI: {round(growth_rate_ci[0]*100,2)}-{round(growth_rate_ci[1]*100,2)}%) per day.') + print(f'The estimated doubling time is {round(doubling_time, 2)} ({100*(1-alpha)}% CI: {round(doubling_time_ci[1],2)}-{round(doubling_time_ci[0],2)}) days.') + From e2d4271afadec5877e3e1ea1d52a298069fb37f3 Mon Sep 17 00:00:00 2001 From: Martyn Date: Fri, 9 Jul 2021 13:48:22 +0100 Subject: [PATCH 11/70] added first auto calibration routine --- household_contact_tracing/calibration.py | 168 +++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 household_contact_tracing/calibration.py diff --git a/household_contact_tracing/calibration.py b/household_contact_tracing/calibration.py new file mode 100644 index 0000000..a3308cd --- /dev/null +++ b/household_contact_tracing/calibration.py @@ -0,0 +1,168 @@ +""" +Code that deals with common hyperparameter optimisation routines. + +Often, we want to calibrate an epidemic to a desired growth rate and household secondary attack rate. +""" + +from abc import ABC +from household_contact_tracing.branching_process_models import HouseholdLevelTracing +from household_contact_tracing.branching_process_controller import BranchingProcessController +from ax import optimize +from copy import Error, copy + + +class Calibration(ABC): + """ + Base class for hyperparameter optimisation of infection dynamics. + """ + + def __init__(self) -> None: + pass + + def setup_and_run_model(self): + pass + + def compute_evaluation(self) -> float: + pass + + def optimize(self): + pass + + def plot_results(self): + pass + + def evaluate_fit(self): + pass + +class StandardCalibrationHouseholdLevelTracing(Calibration): + """Our standard calibration of HouseholdLevelTracing tunes the models growth rate and + household secondary attack rate given inputs: asymptomatic prob, asymptomatic relative infectiousness, + symptom reporting probability. + + The calibration is carried out in the absence of contact tracing to define a baseline epidemic. Contact tracing + can then be evaluated against the baseline epidemic. + """ + + def __init__( + self, + #household_pairwise_survival_prob: float, + desired_growth_rate: float, + asymptomatic_prob: float, + asymptomatic_relative_infectivity: float, + infection_reporting_prob: float, + reduce_contacts_by: float, + starting_infections: int = 100 + ): + + # initialise non-infection parameters that are held constant between simulations + self.fixed_params = { + 'household_pairwise_survival_prob': 0.2, + 'contact_tracing_success_prob': 0.0, + 'overdispersion': 0.32, + 'infection_reporting_prob': 0.25, + 'contact_trace': False, + 'test_delay': 2, + 'contact_trace_delay': 1, + 'incubation_period_delay': 5, + 'symptom_reporting_delay': 1, + 'do_2_step': False, + 'reduce_contacts_by': 0.6, + 'prob_has_trace_app': 0, + 'hh_propensity_to_use_trace_app': 1, + 'test_before_propagate_tracing': True, + 'starting_infections': 100, + 'node_will_uptake_isolation_prob': 1, + 'self_isolation_duration': 0, + 'quarantine_duration': 0, + 'transmission_probability_multiplier': 1, + 'propensity_imperfect_quarantine': 0, + 'global_contact_reduction_imperfect_quarantine': 0 + } + + # set the inputted defaults + self.fixed_params['asymptomatic_prob'] = asymptomatic_prob + self.fixed_params['asymptomatic_relative_infectivity'] = asymptomatic_relative_infectivity + self.fixed_params['infection_reporting_prob'] = infection_reporting_prob + self.fixed_params['reduce_contacts_by'] = reduce_contacts_by + self.fixed_params['starting_infections'] = starting_infections + + self.desired_growth_rate = desired_growth_rate + + self.optimisation_complete = False + + def eval_growth_rate( + self, + outside_household_infectivity_scaling: float, + max_time: int = 20, + max_active_infections: int = 1e5) -> float: + """Sets up a model, runs it, and returns the evaluated growth rate. + + Args: + outside_household_infectivity_scaling (float): controls how infectious global contacts are + max_time (int, optional): upper limit of days to simulate. Defaults to 20. + max_active_infections (int, optional): simulation ends early if maximum number of infections is exceeded. Defaults to 1e5. + """ + + params = copy(self.fixed_params) + params['outside_household_infectivity_scaling'] = outside_household_infectivity_scaling + + controller = BranchingProcessController(HouseholdLevelTracing(params)) + + controller.run_simulation(max_time, max_active_infections) + + return controller.growth_rate_view.get_growth_rate() + + def evaluate_fit(self, outside_household_infectivity_scaling) -> float: + + return abs(self.desired_growth_rate - self.eval_growth_rate(outside_household_infectivity_scaling)) + + def optimise(self, + outside_household_infectivity_scaling_range: list[float], + total_trials: int = 20): + """Performs the hyperparameter optimization step with proposals from the specified ranges. + + Args: + outside_household_infectivity_scaling_range (list[float]): The lower and upper values for this parameter. + total_trials (int): The total number of trials to perform. Defaults to 20 + """ + + self.best_parameters, self.values, self.experiment, self.model = optimize( + parameters=[ + { + "name": "outside_household_infectivity_scaling", + "type": "range", + "bounds": outside_household_infectivity_scaling_range, + } + ], + evaluation_function = lambda p: self.evaluate_fit(p["outside_household_infectivity_scaling"]), + minimize = True, + total_trials = total_trials + ) + + self.optimisation_complete = True + + return self.best_parameters, self.values + + def get_fitted_growth_rate_samples( + self, + n_obs: int = 10) -> list[float]: + """If optimisation has been completed, this method generates sample of the growth rate using + the results from the optimisation step. + + + Args: + n_obs (int, optional): Number of fitted sample of the growth rate to get. Defaults to 10. + + Returns: + [list]: A list containing fitted samples of the growth rate. + """ + + if self.optimisation_complete: + + return [ + self.eval_growth_rate(self.best_parameters['outside_household_infectivity_scaling']) + for _ in range(20) + ] + + else: + raise Error('Optimisation has not yet been performed. Please run optimise before trying to get fitted samples.') From 099ff95511ed0b86107a33d4a30195182d3207e3 Mon Sep 17 00:00:00 2001 From: Martyn Date: Fri, 9 Jul 2021 15:19:12 +0100 Subject: [PATCH 12/70] removed old dependecy --- household_contact_tracing/views/growth_rate_view.py | 1 - 1 file changed, 1 deletion(-) diff --git a/household_contact_tracing/views/growth_rate_view.py b/household_contact_tracing/views/growth_rate_view.py index 8289214..c5f47dd 100644 --- a/household_contact_tracing/views/growth_rate_view.py +++ b/household_contact_tracing/views/growth_rate_view.py @@ -3,7 +3,6 @@ from household_contact_tracing.branching_process_model import BranchingProcessModel from household_contact_tracing.branching_process_state import MaxNodesInfectiousState, ReadyState, RunningState, ExtinctState from household_contact_tracing.exceptions import Error, ModelStateError -from sklearn import linear_model import statsmodels.api as sm import numpy as np class GrowthRateView(BranchingProcessView): From f72c23b79a32ba5712aa881c38e66a51b62aa790 Mon Sep 17 00:00:00 2001 From: Martyn Date: Fri, 9 Jul 2021 15:20:55 +0100 Subject: [PATCH 13/70] updating dependecies --- env_household_contact_tracing.yml | 3 +++ requirements.txt | 2 ++ 2 files changed, 5 insertions(+) diff --git a/env_household_contact_tracing.yml b/env_household_contact_tracing.yml index 86886fd..c9ecc36 100644 --- a/env_household_contact_tracing.yml +++ b/env_household_contact_tracing.yml @@ -13,3 +13,6 @@ dependencies: - pandas - pyvis - beautifulsoup4 + - statsmodels + - torch + - ax diff --git a/requirements.txt b/requirements.txt index 2230c84..ade7b51 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,5 @@ pandas beautifulsoup4==4.9.3 pyvis==0.1.9 statsmodels +torch +ax From fbd3f16b9c58d01d46e80ef20bf256d2fa4b5fca Mon Sep 17 00:00:00 2001 From: Martyn Date: Fri, 9 Jul 2021 15:44:54 +0100 Subject: [PATCH 14/70] stopped csv's getting saves at every optim step --- household_contact_tracing/calibration.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/household_contact_tracing/calibration.py b/household_contact_tracing/calibration.py index a3308cd..a1b762a 100644 --- a/household_contact_tracing/calibration.py +++ b/household_contact_tracing/calibration.py @@ -108,6 +108,7 @@ def eval_growth_rate( controller = BranchingProcessController(HouseholdLevelTracing(params)) + controller.csv_view.set_display(False) controller.run_simulation(max_time, max_active_infections) return controller.growth_rate_view.get_growth_rate() @@ -132,6 +133,7 @@ def optimise(self, "name": "outside_household_infectivity_scaling", "type": "range", "bounds": outside_household_infectivity_scaling_range, + "value_type": "float" } ], evaluation_function = lambda p: self.evaluate_fit(p["outside_household_infectivity_scaling"]), From 35853eaaa3ce790a10e6e3d087a663e2924fe530 Mon Sep 17 00:00:00 2001 From: Martyn Date: Fri, 9 Jul 2021 15:45:06 +0100 Subject: [PATCH 15/70] typos --- household_contact_tracing/views/growth_rate_view.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/household_contact_tracing/views/growth_rate_view.py b/household_contact_tracing/views/growth_rate_view.py index c5f47dd..58cee8c 100644 --- a/household_contact_tracing/views/growth_rate_view.py +++ b/household_contact_tracing/views/growth_rate_view.py @@ -5,6 +5,7 @@ from household_contact_tracing.exceptions import Error, ModelStateError import statsmodels.api as sm import numpy as np + class GrowthRateView(BranchingProcessView): """ @@ -65,7 +66,7 @@ def graph_change(self, subject: BranchingProcessModel): def set_display(self, show: bool): """ - Sets whether this pyvis graph view is displayed or not. + Sets whether this view is displayed or not. Parameters: show (bool): To display this view, set to True From 006e99b2c1978615c4748a20017052a33ffd30bb Mon Sep 17 00:00:00 2001 From: Martyn Date: Tue, 13 Jul 2021 09:07:55 +0100 Subject: [PATCH 16/70] changing terminology to be more general --- .../queueing_processes.py | 88 +++++++++---------- .../queueing_processes/update_fixtures.ipynb | 40 ++++----- test/test_queueing_processes.py | 4 +- 3 files changed, 66 insertions(+), 66 deletions(-) diff --git a/household_contact_tracing/queueing_processes.py b/household_contact_tracing/queueing_processes.py index 29ff0b9..c7272b0 100644 --- a/household_contact_tracing/queueing_processes.py +++ b/household_contact_tracing/queueing_processes.py @@ -55,7 +55,7 @@ def create_queue_df(self): self.queue_df['total_applications_today'] = '' self.queue_df['capacity_exceeded'] = '' self.queue_df['capacity_exceeded_by'] = '' - self.queue_df['number_swabbed_today'] = '' + self.queue_df['number_processed_today'] = '' self.queue_df['number_left_queue_not_tested'] = '' @@ -67,12 +67,12 @@ def create_applicants_df(self): # create empty columns for applicants self.applicant_df['id'] = '' - self.applicant_df['swabbed'] = '' - self.applicant_df['waiting_to_be_swabbed'] = '' - self.applicant_df['left_queue_not_swabbed'] = '' + self.applicant_df['processed'] = '' + self.applicant_df['waiting_to_be_processed'] = '' + self.applicant_df['left_queue_not_processed'] = '' self.applicant_df['time_symptom_onset'] = '' self.applicant_df['time_joined_queue'] = '' - self.applicant_df['time_swabbed'] = '' + self.applicant_df['time_processed'] = '' self.applicant_df['time_received_result'] = '' self.applicant_df['time_will_leave_queue'] = '' @@ -97,43 +97,43 @@ def add_new_applicants( ) # initialise other columns with default values - new_applicant_df['swabbed'] = False - new_applicant_df['waiting_to_be_swabbed'] = True # default value, initially the queue is empty - new_applicant_df['left_queue_not_swabbed'] = '' + new_applicant_df['processed'] = False + new_applicant_df['waiting_to_be_processed'] = True # default value, initially the queue is empty + new_applicant_df['left_queue_not_processed'] = '' new_applicant_df['time_symptom_onset'] = '' new_applicant_df['time_joined_queue'] = '' - new_applicant_df['time_swabbed'] = '' + new_applicant_df['time_processed'] = '' new_applicant_df['time_received_result'] = '' self.applicant_df = self.applicant_df.append(new_applicant_df, ignore_index = True) def swab_applicants(self, - to_be_swabbed: list, + to_be_processed: list, test_processing_delays: list): """For a list of applicants who were successful in getting thorugh the queue, update their variables associated with swabbing Args: - to_be_swabbed (list): A list of integers, referring the rows of the applicant_dataframe that will get processed + to_be_processed (list): A list of integers, referring the rows of the applicant_dataframe that will get processed """ # The columns that will be updated columns_to_update = [ - 'waiting_to_be_swabbed', - 'time_swabbed', - 'left_queue_not_swabbed', - 'swabbed' + 'waiting_to_be_processed', + 'time_processed', + 'left_queue_not_processed', + 'processed' ] - # record an attribtue of which individuals were swabbed today for use later - self.todays_swabbed_index = to_be_swabbed + # record an attribtue of which individuals were processed today for use later + self.todays_processed_index = to_be_processed - # update the above status to show they have been swabbed - self.applicant_df.loc[to_be_swabbed, columns_to_update] = [False, self.time, False, True] + # update the above status to show they have been processed + self.applicant_df.loc[to_be_processed, columns_to_update] = [False, self.time, False, True] # work out when they receive their result, and update the data - self.applicant_df.loc[to_be_swabbed, 'time_received_result'] = self.time + np.array(test_processing_delays) + self.applicant_df.loc[to_be_processed, 'time_received_result'] = self.time + np.array(test_processing_delays) # update the queue_df table with the number of individuals processed today - self.queue_df.loc[self.queue_df.time == self.time, ['number_swabbed_today']] = len(to_be_swabbed) + self.queue_df.loc[self.queue_df.time == self.time, ['number_processed_today']] = len(to_be_processed) def update_queue_leaver_status(self): @@ -141,7 +141,7 @@ def update_queue_leaver_status(self): """ # These people will leave the queue today - self.leavers = (self.applicant_df.time_will_leave_queue <= self.time) & (self.applicant_df.waiting_to_be_swabbed == True) + self.leavers = (self.applicant_df.time_will_leave_queue <= self.time) & (self.applicant_df.waiting_to_be_processed == True) # record the number of people who carry over to the next day @@ -153,18 +153,18 @@ def update_queue_leaver_status(self): # self.queue_df.loc[self.time, ['spillover_to_next_day', 'number_left_queue_not_tested']] = [spillover_to_next_day, sum(self.leavers)] - # Set their waiting to be swabbed status to False - self.applicant_df.loc[self.leavers, ['waiting_to_be_swabbed', 'left_queue_not_swabbed']] = [False, True] + # Set their waiting to be processed status to False + self.applicant_df.loc[self.leavers, ['waiting_to_be_processed', 'left_queue_not_processed']] = [False, True] @property def current_applicants(self) -> list: - """Gets the indexes of individuals waiting to be swabbed. + """Gets the indexes of individuals waiting to be processed. Returns: - list: The indexes of individuals waiting to be swabbed + list: The indexes of individuals waiting to be processed """ - return list(self.applicant_df[self.applicant_df.waiting_to_be_swabbed].index) + return list(self.applicant_df[self.applicant_df.waiting_to_be_processed].index) @property @@ -187,7 +187,7 @@ def number_swabs_performed_today(self) -> int: int: The number of swabs that have been performed today """ - return sum(self.applicant_df.time_swabbed == self.time) + return sum(self.applicant_df.time_processed == self.time) # controller layout @@ -287,19 +287,19 @@ def process_queue(self): # is todays remaining capacity exceeded? if number_applicants <= remaining_swabbing_capacity: - # if capacity not exceeded, then everyone gets swabbed + # if capacity not exceeded, then everyone gets processed test_delays = [ self.test_processing_delay_dist() for _ in range(number_applicants) ] self.queue.swab_applicants( - to_be_swabbed = self.queue.current_applicants, + to_be_processed = self.queue.current_applicants, test_processing_delays = test_delays) else: # Then swabbing capacity is being exceeded. We swab up to capacity. - # We must select who gets swabbed, at the moment there is only one method + # We must select who gets processed, at the moment there is only one method # implemented that does this, that picks a subset without replacement successful_applicants = npr.choice( @@ -313,7 +313,7 @@ def process_queue(self): ] self.queue.swab_applicants( - to_be_swabbed = successful_applicants, + to_be_processed = successful_applicants, test_processing_delays = test_delays) def update_queue_leaver_status(self): @@ -321,16 +321,16 @@ def update_queue_leaver_status(self): """ # These people will leave the queue today - self.leavers = (self.queue.applicant_df.time_will_leave_queue <= self.time) & (self.queue.applicant_df.waiting_to_be_swabbed == True) + self.leavers = (self.queue.applicant_df.time_will_leave_queue <= self.time) & (self.queue.applicant_df.waiting_to_be_processed == True) self.queue.queue_df.loc[self.time, 'number_left_queue_not_tested'] = [sum(self.leavers)] - # Set their waiting to be swabbed status to False - self.queue.applicant_df.loc[self.leavers, ['waiting_to_be_swabbed', 'left_queue_not_swabbed']] = [False, True] + # Set their waiting to be processed status to False + self.queue.applicant_df.loc[self.leavers, ['waiting_to_be_processed', 'left_queue_not_processed']] = [False, True] # work out who will come back the next day - # not left and not swabbed - returners_index = self.queue.applicant_df.waiting_to_be_swabbed == True + # not left and not processed + returners_index = self.queue.applicant_df.waiting_to_be_processed == True self.queue.queue_df.loc[self.time, 'spillover_to_next_day'] = [sum(returners_index)] @@ -374,14 +374,14 @@ def get_todays_queue_output(self): branching process model. Returns: - dict: output dict, with the ids and number of swabbed individuals + dict: output dict, with the ids and number of processed individuals """ - swabbed_individuals = self.queue.applicant_df.loc[self.queue.todays_swabbed_index] + processed_individuals = self.queue.applicant_df.loc[self.queue.todays_processed_index] output = { 'leaving_the_queue_node_ids': self.queue.todays_leavers, - 'swabbed_individuals': swabbed_individuals + 'processed_individuals': processed_individuals } return output @@ -403,9 +403,9 @@ def get_prob_getting_tested(self, time_entered_queue: int): Args: time_entered_queue (int): The day of interest """ - valid_individuals = (self.queue.applicant_df.time_entered_queue == time_entered_queue) & (self.applicant_df.waiting_to_be_swabbed == False) - left_queue_not_swabbed = self.applicant_df[valid_individuals].left_queue_not_swabbed - return 1 - left_queue_not_swabbed.mean() + valid_individuals = (self.queue.applicant_df.time_entered_queue == time_entered_queue) & (self.applicant_df.waiting_to_be_processed == False) + left_queue_not_processed = self.applicant_df[valid_individuals].left_queue_not_processed + return 1 - left_queue_not_processed.mean() def get_delays_for(self, time_entered_queue: int, delay_from_column: str, delay_to_column: str): """ @@ -416,7 +416,7 @@ def get_delays_for(self, time_entered_queue: int, delay_from_column: str, delay_ delay_from_column (str): The earliest timepoint delay_to_column (str): The latest timepoint """ - day_index = (self.applicant_df.time_entered_queue == time_entered_queue) & (self.applicant_df.swabbed == True) + day_index = (self.applicant_df.time_entered_queue == time_entered_queue) & (self.applicant_df.processed == True) delay_from_column = self.applicant_df.loc[day_index, delay_from_column] delay_to_column = self.applicant_df.loc[day_index, delay_to_column] return delay_to_column - delay_from_column diff --git a/test/fixtures/queueing_processes/update_fixtures.ipynb b/test/fixtures/queueing_processes/update_fixtures.ipynb index 140297d..33cd525 100644 --- a/test/fixtures/queueing_processes/update_fixtures.ipynb +++ b/test/fixtures/queueing_processes/update_fixtures.ipynb @@ -74,10 +74,10 @@ "data": { "text/plain": [ "Empty DataFrame\n", - "Columns: [id, swabbed, waiting_to_be_swabbed, left_queue_not_swabbed, time_symptom_onset, time_joined_queue, time_swabbed, time_received_result, time_will_leave_queue]\n", + "Columns: [id, processed, waiting_to_be_processed, left_queue_not_processed, time_symptom_onset, time_joined_queue, time_processed, time_received_result, time_will_leave_queue]\n", "Index: []" ], - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
idswabbedwaiting_to_be_swabbedleft_queue_not_swabbedtime_symptom_onsettime_joined_queuetime_swabbedtime_received_resulttime_will_leave_queue
\n
" + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
idprocessedwaiting_to_be_processedleft_queue_not_processedtime_symptom_onsettime_joined_queuetime_processedtime_received_resulttime_will_leave_queue
\n
" }, "metadata": {}, "execution_count": 3 @@ -136,7 +136,7 @@ "8 \n", "9 \n", "\n", - " number_swabbed_today number_left_queue_not_tested \n", + " number_processed_today number_left_queue_not_tested \n", "0 \n", "1 \n", "2 \n", @@ -148,7 +148,7 @@ "8 \n", "9 " ], - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
timecapacitynew_applicantsspillover_to_next_daytotal_applications_todaycapacity_exceededcapacity_exceeded_bynumber_swabbed_todaynumber_left_queue_not_tested
0010
1110
2210
3310
4410
5510
6610
7710
8810
9910
\n
" + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
timecapacitynew_applicantsspillover_to_next_daytotal_applications_todaycapacity_exceededcapacity_exceeded_bynumber_processed_todaynumber_left_queue_not_tested
0010
1110
2210
3310
4410
5510
6610
7710
8810
9910
\n
" }, "metadata": {}, "execution_count": 5 @@ -192,12 +192,12 @@ "output_type": "execute_result", "data": { "text/plain": [ - " id swabbed waiting_to_be_swabbed left_queue_not_swabbed time_symptom_onset \\\n", + " id processed waiting_to_be_processed left_queue_not_processed time_symptom_onset \\\n", "0 A False True \n", "1 B False True \n", "2 C False True \n", "\n", - " time_joined_queue time_swabbed time_received_result time_will_leave_queue \\\n", + " time_joined_queue time_processed time_received_result time_will_leave_queue \\\n", "0 7 \n", "1 8 \n", "2 9 \n", @@ -207,7 +207,7 @@ "1 2.0 \n", "2 3.0 " ], - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
idswabbedwaiting_to_be_swabbedleft_queue_not_swabbedtime_symptom_onsettime_joined_queuetime_swabbedtime_received_resulttime_will_leave_queuesymptom_onset
0AFalseTrue71.0
1BFalseTrue82.0
2CFalseTrue93.0
\n
" + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
idprocessedwaiting_to_be_processedleft_queue_not_processedtime_symptom_onsettime_joined_queuetime_processedtime_received_resulttime_will_leave_queuesymptom_onset
0AFalseTrue71.0
1BFalseTrue82.0
2CFalseTrue93.0
\n
" }, "metadata": {}, "execution_count": 8 @@ -253,12 +253,12 @@ "output_type": "execute_result", "data": { "text/plain": [ - " id swabbed waiting_to_be_swabbed left_queue_not_swabbed time_symptom_onset \\\n", + " id processed waiting_to_be_processed left_queue_not_processed time_symptom_onset \\\n", "0 A False True \n", "1 B True False False \n", "2 C True False False \n", "\n", - " time_joined_queue time_swabbed time_received_result time_will_leave_queue \\\n", + " time_joined_queue time_processed time_received_result time_will_leave_queue \\\n", "0 7 \n", "1 0 1 8 \n", "2 0 2 9 \n", @@ -268,7 +268,7 @@ "1 2.0 \n", "2 3.0 " ], - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
idswabbedwaiting_to_be_swabbedleft_queue_not_swabbedtime_symptom_onsettime_joined_queuetime_swabbedtime_received_resulttime_will_leave_queuesymptom_onset
0AFalseTrue71.0
1BTrueFalseFalse0182.0
2CTrueFalseFalse0293.0
\n
" + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
idprocessedwaiting_to_be_processedleft_queue_not_processedtime_symptom_onsettime_joined_queuetime_processedtime_received_resulttime_will_leave_queuesymptom_onset
0AFalseTrue71.0
1BTrueFalseFalse0182.0
2CTrueFalseFalse0293.0
\n
" }, "metadata": {}, "execution_count": 22 @@ -343,7 +343,7 @@ "output_type": "execute_result", "data": { "text/plain": [ - " id swabbed waiting_to_be_swabbed left_queue_not_swabbed time_symptom_onset \\\n", + " id processed waiting_to_be_processed left_queue_not_processed time_symptom_onset \\\n", "0 10 False True \n", "1 10 False True \n", "2 10 False True \n", @@ -355,7 +355,7 @@ "8 10 False True \n", "9 10 False True \n", "\n", - " time_joined_queue time_swabbed time_received_result time_will_leave_queue \\\n", + " time_joined_queue time_processed time_received_result time_will_leave_queue \\\n", "0 12 \n", "1 12 \n", "2 12 \n", @@ -379,7 +379,7 @@ "8 2.0 \n", "9 2.0 " ], - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
idswabbedwaiting_to_be_swabbedleft_queue_not_swabbedtime_symptom_onsettime_joined_queuetime_swabbedtime_received_resulttime_will_leave_queuesymptom_onset
010FalseTrue122.0
110FalseTrue122.0
210FalseTrue122.0
310FalseTrue122.0
410FalseTrue122.0
510FalseTrue122.0
610FalseTrue122.0
710FalseTrue122.0
810FalseTrue122.0
910FalseTrue122.0
\n
" + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
idprocessedwaiting_to_be_processedleft_queue_not_processedtime_symptom_onsettime_joined_queuetime_processedtime_received_resulttime_will_leave_queuesymptom_onset
010FalseTrue122.0
110FalseTrue122.0
210FalseTrue122.0
310FalseTrue122.0
410FalseTrue122.0
510FalseTrue122.0
610FalseTrue122.0
710FalseTrue122.0
810FalseTrue122.0
910FalseTrue122.0
\n
" }, "metadata": {}, "execution_count": 8 @@ -439,14 +439,14 @@ "output_type": "execute_result", "data": { "text/plain": [ - " id swabbed waiting_to_be_swabbed left_queue_not_swabbed time_symptom_onset \\\n", + " id processed waiting_to_be_processed left_queue_not_processed time_symptom_onset \\\n", "0 5 False True \n", "1 5 False True \n", "2 5 False True \n", "3 5 False True \n", "4 5 False True \n", "\n", - " time_joined_queue time_swabbed time_received_result time_will_leave_queue \\\n", + " time_joined_queue time_processed time_received_result time_will_leave_queue \\\n", "0 12 \n", "1 12 \n", "2 12 \n", @@ -460,7 +460,7 @@ "3 2.0 \n", "4 2.0 " ], - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
idswabbedwaiting_to_be_swabbedleft_queue_not_swabbedtime_symptom_onsettime_joined_queuetime_swabbedtime_received_resulttime_will_leave_queuesymptom_onset
05FalseTrue122.0
15FalseTrue122.0
25FalseTrue122.0
35FalseTrue122.0
45FalseTrue122.0
\n
" + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
idprocessedwaiting_to_be_processedleft_queue_not_processedtime_symptom_onsettime_joined_queuetime_processedtime_received_resulttime_will_leave_queuesymptom_onset
05FalseTrue122.0
15FalseTrue122.0
25FalseTrue122.0
35FalseTrue122.0
45FalseTrue122.0
\n
" }, "metadata": {}, "execution_count": 11 @@ -488,14 +488,14 @@ "output_type": "execute_result", "data": { "text/plain": [ - " id swabbed waiting_to_be_swabbed left_queue_not_swabbed time_symptom_onset \\\n", + " id processed waiting_to_be_processed left_queue_not_processed time_symptom_onset \\\n", "0 5 True False False \n", "1 5 True False False \n", "2 5 True False False \n", "3 5 True False False \n", "4 5 True False False \n", "\n", - " time_joined_queue time_swabbed time_received_result time_will_leave_queue \\\n", + " time_joined_queue time_processed time_received_result time_will_leave_queue \\\n", "0 0 1 12 \n", "1 0 1 12 \n", "2 0 1 12 \n", @@ -509,7 +509,7 @@ "3 2.0 \n", "4 2.0 " ], - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
idswabbedwaiting_to_be_swabbedleft_queue_not_swabbedtime_symptom_onsettime_joined_queuetime_swabbedtime_received_resulttime_will_leave_queuesymptom_onset
05TrueFalseFalse01122.0
15TrueFalseFalse01122.0
25TrueFalseFalse01122.0
35TrueFalseFalse01122.0
45TrueFalseFalse01122.0
\n
" + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
idprocessedwaiting_to_be_processedleft_queue_not_processedtime_symptom_onsettime_joined_queuetime_processedtime_received_resulttime_will_leave_queuesymptom_onset
05TrueFalseFalse01122.0
15TrueFalseFalse01122.0
25TrueFalseFalse01122.0
35TrueFalseFalse01122.0
45TrueFalseFalse01122.0
\n
" }, "metadata": {}, "execution_count": 13 @@ -527,4 +527,4 @@ "source": [] } ] -} \ No newline at end of file +} diff --git a/test/test_queueing_processes.py b/test/test_queueing_processes.py index 7fc3b10..e1bb1ca 100644 --- a/test/test_queueing_processes.py +++ b/test/test_queueing_processes.py @@ -56,7 +56,7 @@ def test_Queue_new_applicants(simple_queue, Queue_new_applicants_fixture): @pytest.fixture def Queue_swab_applicants_fixture(): - """Loads a fixture where some applicants have been swabbed. + """Loads a fixture where some applicants have been processed. """ return pd.read_pickle('./test/fixtures/queueing_processes/Queue_swab_applicants.pickle') @@ -70,7 +70,7 @@ def test_Queue_swab_applicants(simple_queue, Queue_swab_applicants_fixture): def test_Queue_current_applicants(simple_queue): - """Checks that the waiting to be swabbed indexes are returned. + """Checks that the waiting to be processed indexes are returned. Add 3 people to the queue, swab 2 """ From b53fb2e74ae06dc47445a7d953ddd1c4ee3994f0 Mon Sep 17 00:00:00 2001 From: Martyn Date: Tue, 13 Jul 2021 13:18:15 +0100 Subject: [PATCH 17/70] terminology update --- test/test_queueing_processes.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test_queueing_processes.py b/test/test_queueing_processes.py index e1bb1ca..62ef12f 100644 --- a/test/test_queueing_processes.py +++ b/test/test_queueing_processes.py @@ -61,7 +61,7 @@ def Queue_swab_applicants_fixture(): return pd.read_pickle('./test/fixtures/queueing_processes/Queue_swab_applicants.pickle') def test_Queue_swab_applicants(simple_queue, Queue_swab_applicants_fixture): - """Adds some applicants, swabs some of the and checks the applicant df + """Adds some applicants, processes some of the and checks the applicant df """ simple_queue.add_new_applicants(['A', 'B', 'C'], [1,2,3], 6) simple_queue.swab_applicants([1, 2], [1,2]) @@ -72,7 +72,7 @@ def test_Queue_swab_applicants(simple_queue, Queue_swab_applicants_fixture): def test_Queue_current_applicants(simple_queue): """Checks that the waiting to be processed indexes are returned. - Add 3 people to the queue, swab 2 + Add 3 people to the queue, process 2 """ simple_queue.add_new_applicants(['A', 'B', 'C'], [1,2,3], 6) simple_queue.swab_applicants([1, 2], [1,2]) @@ -99,7 +99,7 @@ def test_DeterministicQueue_add_new_test_seekers(DeterministicQueue_add_new_test Checks that the add_new_test_seekers method correctly modifies the dataframe by adding test seekers based upon the demand """ - def test_processing_delay_dist(): + def processing_delay_dist(): return 1 def symptom_onset_delay_dist(): @@ -110,7 +110,7 @@ def symptom_onset_delay_dist(): demand = [10]*10, capacity = [10]*10, max_time_in_queue = 10, - test_processing_delay_dist = test_processing_delay_dist, + processing_delay_dist = processing_delay_dist, symptom_onset_delay_dist = symptom_onset_delay_dist ) From 6016901c1946fdcdfbaf6b6d7c580e124cf7ef24 Mon Sep 17 00:00:00 2001 From: Martyn Date: Tue, 13 Jul 2021 13:18:40 +0100 Subject: [PATCH 18/70] terminology update, different selection methods --- .../queueing_processes.py | 109 ++++++++++++------ 1 file changed, 71 insertions(+), 38 deletions(-) diff --git a/household_contact_tracing/queueing_processes.py b/household_contact_tracing/queueing_processes.py index c7272b0..1b41639 100644 --- a/household_contact_tracing/queueing_processes.py +++ b/household_contact_tracing/queueing_processes.py @@ -1,10 +1,12 @@ ''' Contains queueing process objects that are used to model testing delays and probability of -not being able to get testing when there are constrained testing resources. +not being able to get testing when there are constrained processing resource. +Processing resources can refer to either swabbing capacity, or genetic sequencing capacity. ''' # TODO consider moving away from dataframes for the applicants. Allocating all the memory beforehand could be restrictive,# # Also not sure if dataframes are the fastest if we are constantly writing to them +from datetime import time import pandas as pd import numpy as np import numpy.random as npr @@ -22,8 +24,8 @@ def __init__( Args: days_to_simulate (int): The total number of days that will be simulated - capacity (list): The swabbing capacity (integer values) at each timepoint - max_time_in_queue (int): Maximum days from symptom onset to ineligibility for swabbing + capacity (list): The processing capacity (integer values) at each timepoint + max_time_in_queue (int): Maximum days from symptom onset to ineligibility for processing verbose (bool, optional): If true prints some outputs. Defaults to False. """ @@ -79,6 +81,7 @@ def create_applicants_df(self): def add_new_applicants( self, ids: list, + time: int, symptom_onset_times: list, max_time_in_queue: int ): @@ -101,7 +104,7 @@ def add_new_applicants( new_applicant_df['waiting_to_be_processed'] = True # default value, initially the queue is empty new_applicant_df['left_queue_not_processed'] = '' new_applicant_df['time_symptom_onset'] = '' - new_applicant_df['time_joined_queue'] = '' + new_applicant_df['time_joined_queue'] = time new_applicant_df['time_processed'] = '' new_applicant_df['time_received_result'] = '' @@ -109,8 +112,8 @@ def add_new_applicants( def swab_applicants(self, to_be_processed: list, - test_processing_delays: list): - """For a list of applicants who were successful in getting thorugh the queue, update their variables associated with swabbing + processing_delays: list): + """For a list of applicants who were successful in getting thorugh the queue, update their variables associated with processing Args: to_be_processed (list): A list of integers, referring the rows of the applicant_dataframe that will get processed """ @@ -130,7 +133,7 @@ def swab_applicants(self, self.applicant_df.loc[to_be_processed, columns_to_update] = [False, self.time, False, True] # work out when they receive their result, and update the data - self.applicant_df.loc[to_be_processed, 'time_received_result'] = self.time + np.array(test_processing_delays) + self.applicant_df.loc[to_be_processed, 'time_received_result'] = self.time + np.array(processing_delays) # update the queue_df table with the number of individuals processed today self.queue_df.loc[self.queue_df.time == self.time, ['number_processed_today']] = len(to_be_processed) @@ -169,22 +172,22 @@ def current_applicants(self) -> list: @property def todays_capacity(self) -> int: - """Gets the number of swabs that can be performed today. + """Gets the number of processes that can be performed today. Returns: - int: The number of swabs that can be performed today + int: The number of processes that can be performed today """ return int(self.queue_df[self.queue_df.time == self.time].capacity) @property - def number_swabs_performed_today(self) -> int: + def number_processes_performed_today(self) -> int: """ - Gets the number of swabs that have been formed today. This will be the number of new applicants - or the swabbing capacity. + Gets the number of processes that have been completed. This will be the number of new applicants + or the processing capacity. Returns: - int: The number of swabs that have been performed today + int: The number of processes that have been performed today """ return sum(self.applicant_df.time_processed == self.time) @@ -213,6 +216,7 @@ def simulate_one_day(self): class DeterministicQueue(QueueController): + # TODO: Rename, it's not deterministic, but the inputs are def __init__( self, @@ -220,9 +224,9 @@ def __init__( demand: List[int], capacity: List[int], max_time_in_queue: int, - test_processing_delay_dist: Callable, - symptom_onset_delay_dist: Callable - ): + processing_delay_dist: Callable, + symptom_onset_delay_dist: Callable, + selection_method: str): # TODO: add to description """A simple queueing process object that does not interact with a branching process model. The test demand and capacity are pre-determined, and the model works out what happens to the queue. @@ -230,11 +234,12 @@ def __init__( Args: days_to_simulate (int): Number of simulation steps to be performed demand (List[int]): The number of new test seekers at each time step. - capacity (List[int]): The swabbing capacity of the queue at each time step. + capacity (List[int]): The processing capacity of the queue at each time step. max_time_in_queue (int): How long since symptom onset that an individual can remain in the queue they become ineligible for testing - test_processing_delay_dist (Callable): A callable that returns integer test processing delays + processing_delay_dist (Callable): A callable that returns integer test processing delays symptom_onset_delay_dist (Callable): A callable the returns integer delays of the time from symptom onset to booking a test. + selection_method ('uniform', 'newest'): Method for selecting which applicants to process when demand exceeds capacity. """ # initialise the queue @@ -245,10 +250,11 @@ def __init__( # set parameters self.demand = demand - self.test_processing_delay_dist = test_processing_delay_dist + self.processing_delay_dist = processing_delay_dist self.symptom_onset_delay_dist = symptom_onset_delay_dist self.max_time_in_queue = max_time_in_queue self.days_to_simulate = days_to_simulate + self.selection_method = selection_method # ease of acccess stuff self.time = self.queue.time @@ -263,15 +269,40 @@ def add_new_test_seekers(self): self.queue.add_new_applicants( ids = self.demand[self.time], + time = self.time, symptom_onset_times = [ self.symptom_onset_delay_dist() for _ in range(self.demand[self.time]) ], max_time_in_queue = self.max_time_in_queue ) - def process_queue(self): + def select_applicants_for_processing(self, remaining_processing_capacity: int) -> list: + """Given the current demand and remaining testing capacity, compute which individuals get selected for testing. + + Args: + current_queue_applicants (list): A list of id's of individuals who are waiting to get processed. + remaining_processing_capacity (int): The remaining capacity for individuals to get processed. + + Returns: + list: The list of processed individuals. """ - Performs swabbing of individuals up to capacity, and updates the dataframes that store the calculations + + if self.selection_method == 'uniform': + return( + npr.choice( + a = self.queue.current_applicants, + size = remaining_processing_capacity, + replace = False + ) + ) + elif self.selection_method == 'newest': + return( + self.queue.applicant_df.sort_values('time_joined_queue')[0:remaining_processing_capacity] + ) + + def process_queue_random_selection(self): + """ + Performs processing of individuals up to capacity, and updates the dataframes that store the calculations. """ # Note: this method is set up so that it can be called multiple times in one day @@ -282,39 +313,41 @@ def process_queue(self): # update queue_df with the number of applicants today self.queue.queue_df.loc[self.queue.queue_df.time == self.time, ['total_applications_today']] = [number_applicants] - # how much swabbing capacity do we have remaining? The method - remaining_swabbing_capacity = self.queue.todays_capacity - self.queue.number_swabs_performed_today + # how much processing capacity do we have remaining? The method + remaining_processing_capacity = self.queue.todays_capacity - self.queue.number_processes_performed_today # is todays remaining capacity exceeded? - if number_applicants <= remaining_swabbing_capacity: + if number_applicants <= remaining_processing_capacity: # if capacity not exceeded, then everyone gets processed - test_delays = [ - self.test_processing_delay_dist() for _ in range(number_applicants) + processing_delays = [ + self.processing_delay_dist() for _ in range(number_applicants) ] self.queue.swab_applicants( to_be_processed = self.queue.current_applicants, - test_processing_delays = test_delays) + processing_delays = processing_delays) else: - # Then swabbing capacity is being exceeded. We swab up to capacity. + # Then processing capacity is being exceeded. We process up to capacity. # We must select who gets processed, at the moment there is only one method # implemented that does this, that picks a subset without replacement + self.select_applicants_for_processing(remaining_processing_capacity) + successful_applicants = npr.choice( a = self.queue.current_applicants, - size = remaining_swabbing_capacity, + size = remaining_processing_capacity, replace = False ) - test_delays = [ - self.test_processing_delay_dist() for _ in range(remaining_swabbing_capacity) + processing_delays = [ + self.processing_delay_dist() for _ in range(remaining_processing_capacity) ] self.queue.swab_applicants( to_be_processed = successful_applicants, - test_processing_delays = test_delays) + processing_delays = processing_delays) def update_queue_leaver_status(self): """These individuals have been in the queue too long. They are no longer trying/able to get a swab. @@ -342,7 +375,7 @@ def simulate_one_day(self, verbose: bool = True): # steps required to simulate one day self.add_new_test_seekers() self.update_queue_leaver_status() - self.process_queue() + self.process_queue_random_selection() self.time @@ -396,18 +429,18 @@ def __init__( self.applicant_df = queue.applicant_df self.queue_df = queue.queue_df - def get_prob_getting_tested(self, time_entered_queue: int): + def get_prob_getting_tested(self, time_joined_queue: int): """ Returns the probability of getting tested if you join the queue on a specified day Args: - time_entered_queue (int): The day of interest + time_joined_queue (int): The day of interest """ - valid_individuals = (self.queue.applicant_df.time_entered_queue == time_entered_queue) & (self.applicant_df.waiting_to_be_processed == False) + valid_individuals = (self.queue.applicant_df.time_joined_queue == time_joined_queue) & (self.applicant_df.waiting_to_be_processed == False) left_queue_not_processed = self.applicant_df[valid_individuals].left_queue_not_processed return 1 - left_queue_not_processed.mean() - def get_delays_for(self, time_entered_queue: int, delay_from_column: str, delay_to_column: str): + def get_delays_for(self, time_joined_queue: int, delay_from_column: str, delay_to_column: str): """ Return a list of the delays between two timepoints who joined on a specified day @@ -416,7 +449,7 @@ def get_delays_for(self, time_entered_queue: int, delay_from_column: str, delay_ delay_from_column (str): The earliest timepoint delay_to_column (str): The latest timepoint """ - day_index = (self.applicant_df.time_entered_queue == time_entered_queue) & (self.applicant_df.processed == True) + day_index = (self.applicant_df.time_joined_queue == time_joined_queue) & (self.applicant_df.processed == True) delay_from_column = self.applicant_df.loc[day_index, delay_from_column] delay_to_column = self.applicant_df.loc[day_index, delay_to_column] return delay_to_column - delay_from_column From 9b883f99bc39fe1bd8aa3d698225e8dfd55acebc Mon Sep 17 00:00:00 2001 From: Martyn Date: Tue, 13 Jul 2021 13:29:35 +0100 Subject: [PATCH 19/70] reworked adding nodes to Queue --- .../queueing_processes.py | 58 ++++++++++--------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/household_contact_tracing/queueing_processes.py b/household_contact_tracing/queueing_processes.py index 1b41639..93ab2c1 100644 --- a/household_contact_tracing/queueing_processes.py +++ b/household_contact_tracing/queueing_processes.py @@ -52,13 +52,13 @@ def create_queue_df(self): }) # create some empty columns for storing results - self.queue_df['new_applicants'] = '' - self.queue_df['spillover_to_next_day'] = '' - self.queue_df['total_applications_today'] = '' - self.queue_df['capacity_exceeded'] = '' - self.queue_df['capacity_exceeded_by'] = '' - self.queue_df['number_processed_today'] = '' - self.queue_df['number_left_queue_not_tested'] = '' + self.queue_df['new_applicants'] = '' + self.queue_df['spillover_to_next_day'] = '' + self.queue_df['total_applications_today'] = '' + self.queue_df['capacity_exceeded'] = '' + self.queue_df['capacity_exceeded_by'] = '' + self.queue_df['number_processed_today'] = '' + self.queue_df['number_left_queue_not_tested'] = '' def create_applicants_df(self): @@ -68,33 +68,31 @@ def create_applicants_df(self): self.applicant_df = pd.DataFrame() # create empty columns for applicants - self.applicant_df['id'] = '' - self.applicant_df['processed'] = '' - self.applicant_df['waiting_to_be_processed'] = '' - self.applicant_df['left_queue_not_processed'] = '' - self.applicant_df['time_symptom_onset'] = '' - self.applicant_df['time_joined_queue'] = '' - self.applicant_df['time_processed'] = '' - self.applicant_df['time_received_result'] = '' - self.applicant_df['time_will_leave_queue'] = '' + self.applicant_df['id'] = '' + self.applicant_df['processed'] = '' + self.applicant_df['waiting_to_be_processed'] = '' + self.applicant_df['left_queue_not_processed'] = '' + self.applicant_df['time_symptom_onset'] = '' + self.applicant_df['time_joined_queue'] = '' + self.applicant_df['time_processed'] = '' + self.applicant_df['time_received_result'] = '' + self.applicant_df['time_will_leave_queue'] = '' def add_new_applicants( self, ids: list, time: int, symptom_onset_times: list, - max_time_in_queue: int + queue_leaving_times: list ): """ Adds new applicants to the queue. """ - queue_leaving_times = np.array(symptom_onset_times) + max_time_in_queue - new_applicant_df = pd.DataFrame( { 'id': ids, - 'symptom_onset': symptom_onset_times, + 'time_symptom_onset': symptom_onset_times, 'time_will_leave_queue': queue_leaving_times } ) @@ -103,10 +101,9 @@ def add_new_applicants( new_applicant_df['processed'] = False new_applicant_df['waiting_to_be_processed'] = True # default value, initially the queue is empty new_applicant_df['left_queue_not_processed'] = '' - new_applicant_df['time_symptom_onset'] = '' - new_applicant_df['time_joined_queue'] = time + new_applicant_df['time_joined_queue'] = time new_applicant_df['time_processed'] = '' - new_applicant_df['time_received_result'] = '' + new_applicant_df['time_received_result'] = '' self.applicant_df = self.applicant_df.append(new_applicant_df, ignore_index = True) @@ -266,14 +263,19 @@ def add_new_test_seekers(self): For this model, the new test seekers at each time point are defined a priori. """ + symptom_onset_times = [ + self.time - self.symptom_onset_delay_dist() for _ in range(self.demand[self.time]) + ] + + queue_leaving_times = [ + onset_time + self.max_time_in_queue for onset_time in symptom_onset_times + ] self.queue.add_new_applicants( - ids = self.demand[self.time], + ids = [''] * self.demand[self.time], time = self.time, - symptom_onset_times = [ - self.symptom_onset_delay_dist() for _ in range(self.demand[self.time]) - ], - max_time_in_queue = self.max_time_in_queue + symptom_onset_times = symptom_onset_times, + queue_leaving_times = queue_leaving_times ) def select_applicants_for_processing(self, remaining_processing_capacity: int) -> list: From 5259168ff41022c0ea14a0cfda65bd0794cc7bfe Mon Sep 17 00:00:00 2001 From: Martyn Date: Tue, 13 Jul 2021 14:06:39 +0100 Subject: [PATCH 20/70] cleaning --- .../queueing_processes.py | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/household_contact_tracing/queueing_processes.py b/household_contact_tracing/queueing_processes.py index 93ab2c1..3ca2e63 100644 --- a/household_contact_tracing/queueing_processes.py +++ b/household_contact_tracing/queueing_processes.py @@ -302,7 +302,7 @@ def select_applicants_for_processing(self, remaining_processing_capacity: int) - self.queue.applicant_df.sort_values('time_joined_queue')[0:remaining_processing_capacity] ) - def process_queue_random_selection(self): + def process_queue(self): """ Performs processing of individuals up to capacity, and updates the dataframes that store the calculations. """ @@ -369,7 +369,7 @@ def update_queue_leaver_status(self): self.queue.queue_df.loc[self.time, 'spillover_to_next_day'] = [sum(returners_index)] - def simulate_one_day(self, verbose: bool = True): + def simulate_one_day(self): """ Simulates one day of the queue. """ @@ -377,21 +377,17 @@ def simulate_one_day(self, verbose: bool = True): # steps required to simulate one day self.add_new_test_seekers() self.update_queue_leaver_status() - self.process_queue_random_selection() - - self.time + self.process_queue() - # make a nice little status update - if verbose: - print(f'Model time {self.time}, progress: {round((self.time + 1) / self.queue.days_to_simulate * 100)}%', end = '\r') + self.queue.time += 1 - def run_simulation(self, verbose: bool = True): + def run_simulation(self): """Runs the queueing process model. """ while self.time < self.days_to_simulate: - self.simulate_one_day(verbose) + self.simulate_one_day() self.time += 1 @@ -431,9 +427,9 @@ def __init__( self.applicant_df = queue.applicant_df self.queue_df = queue.queue_df - def get_prob_getting_tested(self, time_joined_queue: int): + def get_prob_getting_processed(self, time_joined_queue: int): """ - Returns the probability of getting tested if you join the queue on a specified day + Returns the probability of getting processed if you join the queue on a specified day Args: time_joined_queue (int): The day of interest From ac195d614687a39452998831315d69be8a90a223 Mon Sep 17 00:00:00 2001 From: Martyn Date: Wed, 14 Jul 2021 10:36:56 +0100 Subject: [PATCH 21/70] Added variant sequencing queue --- .../queueing_processes.py | 207 +++++++++++++++++- 1 file changed, 204 insertions(+), 3 deletions(-) diff --git a/household_contact_tracing/queueing_processes.py b/household_contact_tracing/queueing_processes.py index 3ca2e63..9ec5fa7 100644 --- a/household_contact_tracing/queueing_processes.py +++ b/household_contact_tracing/queueing_processes.py @@ -3,8 +3,6 @@ not being able to get testing when there are constrained processing resource. Processing resources can refer to either swabbing capacity, or genetic sequencing capacity. ''' -# TODO consider moving away from dataframes for the applicants. Allocating all the memory beforehand could be restrictive,# -# Also not sure if dataframes are the fastest if we are constantly writing to them from datetime import time import pandas as pd @@ -223,7 +221,7 @@ def __init__( max_time_in_queue: int, processing_delay_dist: Callable, symptom_onset_delay_dist: Callable, - selection_method: str): # TODO: add to description + selection_method: str): """A simple queueing process object that does not interact with a branching process model. The test demand and capacity are pre-determined, and the model works out what happens to the queue. @@ -391,6 +389,209 @@ def run_simulation(self): self.time += 1 + +class DeterministicQueueVariantSequencing(QueueController): + + def __init__( + self, + days_to_simulate: int, + demand: List[int], + demand_variant: list[int], + capacity: List[int], + max_time_in_queue: int, + processing_delay_dist: Callable, + symptom_onset_delay_dist: Callable, + selection_method: str): + """A simple queueing process object that does not interact with a branching process model. + + The test demand and capacity are pre-determined, and the model works out what happens to the queue. + + Args: + days_to_simulate (int): Number of simulation steps to be performed + demand (List[int]): The number of new test seekers at each time step. + capacity (List[int]): The processing capacity of the queue at each time step. + max_time_in_queue (int): How long since symptom onset that an individual can remain in the queue + they become ineligible for testing + processing_delay_dist (Callable): A callable that returns integer test processing delays + symptom_onset_delay_dist (Callable): A callable the returns integer delays of the time from symptom onset to booking a test. + selection_method ('uniform', 'newest'): Method for selecting which applicants to process when demand exceeds capacity. + """ + + # initialise the queue + self.queue = Queue( + days_to_simulate = days_to_simulate, + capacity = capacity + ) + + # add an empty column to store variant status + self.queue.applicant_df['variant'] = '' + + # set parameters + self.demand = demand + self.demand_variant = demand_variant + self.processing_delay_dist = processing_delay_dist + self.symptom_onset_delay_dist = symptom_onset_delay_dist + self.max_time_in_queue = max_time_in_queue + self.days_to_simulate = days_to_simulate + self.selection_method = selection_method + + # ease of acccess stuff + self.time = self.queue.time + + + def add_new_queue_joiners(self): + """ + Adds new test seekers to the queue. + + For this model, the new test seekers at each time point are defined a priori. + """ + total_new_joiners = self.demand[self.time] + self.demand_variant[self.time] + + symptom_onset_times = [ + self.time - self.symptom_onset_delay_dist() for _ in range(total_new_joiners) + ] + + queue_leaving_times = [ + onset_time + self.max_time_in_queue for onset_time in symptom_onset_times + ] + + self.queue.add_new_applicants( + ids = [''] * total_new_joiners, + time = self.time, + symptom_onset_times = symptom_onset_times, + queue_leaving_times = queue_leaving_times + ) + + # work out which of the new joiners are variants + variant_ids = npr.choice( + a = list(range(total_new_joiners)), + size = self.demand_variant[self.time], + replace = False) + + # by default cases are not variants + variant_status = [False]*total_new_joiners + for _ in variant_ids: + variant_status[_] = True + + # set the variant status on the applicant dataframe column + todays_joiner_index = self.queue.applicant_df.time_joined_queue == self.time + self.queue.applicant_df.loc[todays_joiner_index, 'variant'] = variant_status + + + def select_applicants_for_processing(self, remaining_processing_capacity: int) -> list: + """Given the current demand and remaining testing capacity, compute which individuals get selected for testing. + + Args: + current_queue_applicants (list): A list of id's of individuals who are waiting to get processed. + remaining_processing_capacity (int): The remaining capacity for individuals to get processed. + + Returns: + list: The list of processed individuals. + """ + + if self.selection_method == 'uniform': + return( + npr.choice( + a = self.queue.current_applicants, + size = remaining_processing_capacity, + replace = False + ) + ) + elif self.selection_method == 'newest': + return( + self.queue.applicant_df.sort_values('time_joined_queue')[0:remaining_processing_capacity] + ) + + def process_queue(self): + """ + Performs processing of individuals up to capacity, and updates the dataframes that store the calculations. + """ + + # Note: this method is set up so that it can be called multiple times in one day + # in case new applicants are added multiple times in a day. This is sometimes useful + + number_applicants = len(self.queue.current_applicants) + + # update queue_df with the number of applicants today + self.queue.queue_df.loc[self.queue.queue_df.time == self.time, ['total_applications_today']] = [number_applicants] + + # how much processing capacity do we have remaining? The method + remaining_processing_capacity = self.queue.todays_capacity - self.queue.number_processes_performed_today + + # is todays remaining capacity exceeded? + if number_applicants <= remaining_processing_capacity: + # if capacity not exceeded, then everyone gets processed + + processing_delays = [ + self.processing_delay_dist() for _ in range(number_applicants) + ] + + self.queue.swab_applicants( + to_be_processed = self.queue.current_applicants, + processing_delays = processing_delays) + + else: + # Then processing capacity is being exceeded. We process up to capacity. + # We must select who gets processed, at the moment there is only one method + # implemented that does this, that picks a subset without replacement + + self.select_applicants_for_processing(remaining_processing_capacity) + + successful_applicants = npr.choice( + a = self.queue.current_applicants, + size = remaining_processing_capacity, + replace = False + ) + + processing_delays = [ + self.processing_delay_dist() for _ in range(remaining_processing_capacity) + ] + + self.queue.swab_applicants( + to_be_processed = successful_applicants, + processing_delays = processing_delays) + + def update_queue_leaver_status(self): + """These individuals have been in the queue too long. They are no longer trying/able to get a swab. + """ + + # These people will leave the queue today + self.leavers = (self.queue.applicant_df.time_will_leave_queue <= self.time) & (self.queue.applicant_df.waiting_to_be_processed == True) + + self.queue.queue_df.loc[self.time, 'number_left_queue_not_tested'] = [sum(self.leavers)] + + # Set their waiting to be processed status to False + self.queue.applicant_df.loc[self.leavers, ['waiting_to_be_processed', 'left_queue_not_processed']] = [False, True] + + # work out who will come back the next day + # not left and not processed + returners_index = self.queue.applicant_df.waiting_to_be_processed == True + + self.queue.queue_df.loc[self.time, 'spillover_to_next_day'] = [sum(returners_index)] + + def simulate_one_day(self): + """ + Simulates one day of the queue. + """ + + # steps required to simulate one day + self.add_new_queue_joiners() + self.update_queue_leaver_status() + self.process_queue() + + self.queue.time += 1 + + def run_simulation(self): + """Runs the queueing process model. + """ + + while self.time < self.days_to_simulate: + + self.simulate_one_day() + + self.time += 1 + + class QueueBranchingProcessController(): def __init__( From fb3b25e33a323bfd9e9eb01eb48750082644bc57 Mon Sep 17 00:00:00 2001 From: Martyn Date: Wed, 14 Jul 2021 11:37:17 +0100 Subject: [PATCH 22/70] initial view commit --- .../views/resource_demand_views.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 household_contact_tracing/views/resource_demand_views.py diff --git a/household_contact_tracing/views/resource_demand_views.py b/household_contact_tracing/views/resource_demand_views.py new file mode 100644 index 0000000..584ce67 --- /dev/null +++ b/household_contact_tracing/views/resource_demand_views.py @@ -0,0 +1,79 @@ +''' +Contains several views that can be used to record the demand for various resources +at each time step of a simulation. Resources might include the total number of individuals +attempting to book a test at each timestep +''' +from household_contact_tracing.views.branching_process_view import BranchingProcessView +from household_contact_tracing.branching_process_models import BranchingProcessModel + +class PositiveTestRecord(BranchingProcessView): + + def __init__(self, model: BranchingProcessModel): + + self._model = model + self.view_name = 'positive_test_record' + + self.positive_tests_requested = [] + + def model_state_change(self, subject: BranchingProcessModel): + """ + Respond to changes in model state (e.g. running, extinct, timed-out) + + Parameters: + subject (BranchingProcessModel): The branching process model being displayed by this simulation view. + + Returns: + None + """ + pass + + def model_step_increment(self, subject: BranchingProcessModel): + """ + Respond to single step increment in simulation + + Parameters: + subject (BranchingProcessModel): The branching process model being displayed by this simulation view. + + Returns: + None + """ + pass + + def model_simulation_stopped(self, subject: BranchingProcessModel): + """ + Respond to end of simulation run + + Parameters: + subject (BranchingProcessModel): The branching process model being displayed by this simulation view. + + Returns: + None + """ + pass + + def graph_change(self, subject: BranchingProcessModel): + """ + Respond to changes in graph (nodes/households network) + + Parameters: + subject (SimulationModel): The branching process model being displayed by this simulation view. + + Returns: + None + """ + pass + + def set_display(self, show: bool): + """ + Sets whether this view is displayed or not. + + Parameters: + show (bool): To display this view, set to True + + Returns: + None + """ + + + def print_my_name(self): + print('science bitch') From 72a689370562eeab848a258cb16983155c11fd41 Mon Sep 17 00:00:00 2001 From: Martyn Date: Wed, 14 Jul 2021 11:37:42 +0100 Subject: [PATCH 23/70] Added ability to set non-default views --- .../branching_process_controller.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/household_contact_tracing/branching_process_controller.py b/household_contact_tracing/branching_process_controller.py index e9613ea..4c24f6c 100644 --- a/household_contact_tracing/branching_process_controller.py +++ b/household_contact_tracing/branching_process_controller.py @@ -1,3 +1,5 @@ +from typing import List, Optional +from household_contact_tracing.views.branching_process_view import BranchingProcessView from household_contact_tracing.views.growth_rate_view import GrowthRateView from household_contact_tracing.branching_process_model import BranchingProcessModel from household_contact_tracing.views.shell_view import ShellView @@ -33,7 +35,7 @@ class BranchingProcessController: """ - def __init__(self, model: BranchingProcessModel): + def __init__(self, model: BranchingProcessModel, additional_views: Optional[List[BranchingProcessView]]): """ Constructor for BranchingProcessController @@ -51,6 +53,12 @@ def __init__(self, model: BranchingProcessModel): self.csv_view = CSVFileView(model) self.growth_rate_view = GrowthRateView(model) + # initialise any views that are required, but included as defaults + for view in additional_views: + + initialised_view = view(model) + setattr(self, initialised_view.view_name, initialised_view) + self.set_graphic_displays(False) @property From 8f7d1b04cb3aeaf981ec3acfed5b86ea5b4de57a Mon Sep 17 00:00:00 2001 From: Martyn Date: Wed, 14 Jul 2021 11:38:01 +0100 Subject: [PATCH 24/70] minor tweaks and comments --- .../views/growth_rate_view.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/household_contact_tracing/views/growth_rate_view.py b/household_contact_tracing/views/growth_rate_view.py index 58cee8c..8fa6b8c 100644 --- a/household_contact_tracing/views/growth_rate_view.py +++ b/household_contact_tracing/views/growth_rate_view.py @@ -15,6 +15,7 @@ class GrowthRateView(BranchingProcessView): def __init__(self, model: BranchingProcessModel): self._model = model + self.show = False def model_state_change(self, subject: BranchingProcessModel): """ @@ -26,6 +27,7 @@ def model_state_change(self, subject: BranchingProcessModel): Returns: None """ + # nothing to do here, usually it only makes sense to estimate the growth rate after the simulation is complete. pass def model_step_increment(self, subject: BranchingProcessModel): @@ -38,6 +40,7 @@ def model_step_increment(self, subject: BranchingProcessModel): Returns: None """ + # nothing to do here, usually it only makes sense to estimate the growth rate after the simulation is complete. pass def model_simulation_stopped(self, subject: BranchingProcessModel): @@ -50,7 +53,8 @@ def model_simulation_stopped(self, subject: BranchingProcessModel): Returns: None """ - pass + if self.show: + self._estimate_growth_rate() def graph_change(self, subject: BranchingProcessModel): """ @@ -62,6 +66,7 @@ def graph_change(self, subject: BranchingProcessModel): Returns: None """ + # nothing to do here, usually it only makes sense to estimate the growth rate after the simulation is complete. pass def set_display(self, show: bool): @@ -74,7 +79,7 @@ def set_display(self, show: bool): Returns: None """ - pass + self.show = show def get_infection_times(self): """ @@ -158,7 +163,7 @@ def get_growth_rate(self, discard_first_n_days: int = 10, verbose: bool = True): return self.glm_poisson.params[0] - def growth_rate_summary(self, discard_first_n_days: int = 10, alpha: float = 0.05): + def growth_rate_summary(self, discard_first_n_days: int = 10, alpha: float = 0.05, glm_summary: bool = False): """Returns the growth rate of the simulated epidemic, estimated using poisson regression. The first few days of a simulation are typically discarded while the process becomes mixed @@ -176,8 +181,8 @@ def growth_rate_summary(self, discard_first_n_days: int = 10, alpha: float = 0.0 doubling_time = np.log(2) / np.log(1 + growth_rate) doubling_time_ci = np.log(2) / np.log(1 + np.array(growth_rate_ci)) - print('GLM regression summary:') - print(self.glm_poisson.summary()) + if glm_summary: + print(self.glm_poisson.summary()) print(f'{num_eligible_dates} time periods were used to estimate the growth rate.') print(f'The estimated growth rate was {round(growth_rate*100, 2)}% ({100*(1-alpha)}% CI: {round(growth_rate_ci[0]*100,2)}-{round(growth_rate_ci[1]*100,2)}%) per day.') print(f'The estimated doubling time is {round(doubling_time, 2)} ({100*(1-alpha)}% CI: {round(doubling_time_ci[1],2)}-{round(doubling_time_ci[0],2)}) days.') From 05c3232d31fc3c9de23cee8701a51b8e69cb115a Mon Sep 17 00:00:00 2001 From: Martyn Date: Wed, 14 Jul 2021 11:52:13 +0100 Subject: [PATCH 25/70] minor fix --- household_contact_tracing/branching_process_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/household_contact_tracing/branching_process_controller.py b/household_contact_tracing/branching_process_controller.py index 4c24f6c..7226588 100644 --- a/household_contact_tracing/branching_process_controller.py +++ b/household_contact_tracing/branching_process_controller.py @@ -35,7 +35,7 @@ class BranchingProcessController: """ - def __init__(self, model: BranchingProcessModel, additional_views: Optional[List[BranchingProcessView]]): + def __init__(self, model: BranchingProcessModel, additional_views: Optional[List[BranchingProcessView]] = []): """ Constructor for BranchingProcessController From 6147ada38b9d4acf261c3c96e0cb310e345d6a7c Mon Sep 17 00:00:00 2001 From: Martyn Date: Wed, 14 Jul 2021 13:21:02 +0100 Subject: [PATCH 26/70] changing default values to be something more useful --- household_contact_tracing/branching_process_models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/household_contact_tracing/branching_process_models.py b/household_contact_tracing/branching_process_models.py index 37893d6..17ca706 100644 --- a/household_contact_tracing/branching_process_models.py +++ b/household_contact_tracing/branching_process_models.py @@ -189,16 +189,16 @@ def prob_pcr_positive(self, fn: Callable[[int], float]): @staticmethod def default_prob_lfa_positive(infectious_age): """Default LFA test result probability.""" - if infectious_age in [4, 5, 6]: - return 1 + if infectious_age in [2, 3, 4, 5, 6, 7]: + return 0.75 else: return 0 @staticmethod def default_prob_pcr_positive(infectious_age): """Default PCR test result probability.""" - if infectious_age in [4, 5, 6]: - return 0 + if infectious_age in [1, 2, 3, 4, 5, 6, 7, 8]: + return 1 else: return 0 From aef7d1da442c4cc5f7c1095d18bdba5e4fc31628 Mon Sep 17 00:00:00 2001 From: Peter Crowther <13117069+merrygoat@users.noreply.github.com> Date: Mon, 26 Jul 2021 15:19:42 +0100 Subject: [PATCH 27/70] Remove duplicate simulate method --- .../branching_process_models.py | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/household_contact_tracing/branching_process_models.py b/household_contact_tracing/branching_process_models.py index c38db6d..2b34256 100644 --- a/household_contact_tracing/branching_process_models.py +++ b/household_contact_tracing/branching_process_models.py @@ -226,27 +226,6 @@ def _initialise_intervention(self): return new_intervention - def simulate_one_step(self): - """Simulates one day of the infection and contact tracing.""" - - # Perform one day of the infection - self.infection.increment(self.time) - # isolate nodes reached by tracing, isolate nodes due to self-reporting - self.intervention.isolation.isolate_self_reporting_cases(self.time) - # isolate self-reporting-nodes while they wait for tests - self.intervention.isolation.update_households_contact_traced(self.time) - self.intervention.isolation.update_isolation(self.time) - for step in range(5): - self.intervention.increment_tracing.increment_contact_tracing(self.time) - # node recoveries - self.infection.perform_recoveries(self.time) - # release nodes from quarantine or intervention if the time has arrived - self.intervention.completed_isolation(self.time) - self.intervention.completed_quarantine(self.time) - # increment time - self.time += 1 - - class IndividualTracingDailyTesting(IndividualLevelTracing): """A class used to represent a simulation of contact tracing of households along with contacting every individual and their contacts, whether they have tested positive or not, along From cec40c333be13f1f858e8ae0c9448104bacf0ff8 Mon Sep 17 00:00:00 2001 From: Peter Crowther <13117069+merrygoat@users.noreply.github.com> Date: Mon, 26 Jul 2021 15:38:25 +0100 Subject: [PATCH 28/70] Generalise simulation stopping criteria --- examples/run_testing_contact_model.py | 10 +-- examples/simple_test_script.py | 8 +- .../branching_process_controller.py | 8 +- .../branching_process_model.py | 6 +- .../branching_process_models.py | 85 ++++++++++++------- household_contact_tracing/utilities.py | 2 +- test/test_integration.py | 6 +- 7 files changed, 76 insertions(+), 49 deletions(-) diff --git a/examples/run_testing_contact_model.py b/examples/run_testing_contact_model.py index 2c986fc..6dad7fa 100644 --- a/examples/run_testing_contact_model.py +++ b/examples/run_testing_contact_model.py @@ -44,9 +44,9 @@ def prob_testing_positive_function(time_relative_to_symptom_onset): controller.timeline_view.set_display(True) controller.graph_pyvis_view.open_in_browser = True controller.graph_view.set_display(True) - controller.run_simulation(15) + controller.run_simulation({"max_time": 15}) controller.graph_pyvis_view.set_display(False) - controller.run_simulation(20) + controller.run_simulation({"max_time": 20}) @@ -92,7 +92,7 @@ def prob_pcr_positive(infectious_age): controller.shell_view.set_display(False) controller.timeline_view.set_display(True) controller.graph_view.set_display(True) - controller.run_simulation(2) + controller.run_simulation({"max_time": 2}) # Re-initialise and re-run model multiple times and save result to specified file # instead of default ('/temp/sumulation_ouput_[todays date].csv') @@ -102,7 +102,7 @@ def prob_pcr_positive(infectious_age): for idx in range(0, 10): controller.model = IndividualTracingDailyTesting(params) controller.csv_view.display_params = ["household_pairwise_survival_prob", "asymptomatic_relative_infectivity"] - controller.run_simulation(20) + controller.run_simulation({"max_time": 20}) # define some easy to look at test sensitivity functions @@ -151,7 +151,7 @@ def example_3(): controller.graph_view.set_display(True) controller.timeline_view.set_display(True) - controller.run_simulation(16) + controller.run_simulation({"max_time": 16}) if __name__ == "__main__": diff --git a/examples/simple_test_script.py b/examples/simple_test_script.py index 4551003..9e054fd 100644 --- a/examples/simple_test_script.py +++ b/examples/simple_test_script.py @@ -34,7 +34,7 @@ # Create controller and add model, then run controller = BranchingProcessController(bpm.HouseholdLevelTracing(params)) -controller.run_simulation(10) +controller.run_simulation({"max_time": 10}) # Update parameters params['infection_reporting_prob'] = 0.5 @@ -42,7 +42,7 @@ # Re initialise with new parameters and Re-run controller.model = bpm.HouseholdLevelTracing(params) -controller.run_simulation(10) +controller.run_simulation({"max_time": 10}) # Add further parameters @@ -56,7 +56,7 @@ # Switch on a view (e.g. the timeline graph views) controller.timeline_view.set_display(True) controller.graph_view.set_display(True) -controller.run_simulation(10) +controller.run_simulation({"max_time": 10}) # Repeat runs and output to a named CSV file @@ -68,4 +68,4 @@ controller.csv_view.filename = save_path controller.csv_view.display_params = ['number_of_days_to_trace_backwards', 'number_of_days_to_trace_forwards'] - controller.run_simulation(5) + controller.run_simulation({"max_time": 5}) diff --git a/household_contact_tracing/branching_process_controller.py b/household_contact_tracing/branching_process_controller.py index d301ee0..fe8c3fe 100644 --- a/household_contact_tracing/branching_process_controller.py +++ b/household_contact_tracing/branching_process_controller.py @@ -79,15 +79,15 @@ def set_graphic_displays(self, display: bool): self.graph_pyvis_view.set_display(display) self.timeline_view.set_display(display) - def run_simulation(self, max_time: int = 20, infection_threshold: int = 5000): + def run_simulation(self, state_criteria: dict): """ Run the simulation until it stops (e.g times out, too many infectious nodes or goes extinct) Parameters: - max_time (int): The maximum number of iterations (eg. days) to be run (simulation stops if reached) - infection_threshold (int): The maximum number of infectious nodes (simulation stops if reached) + state_criteria: Named variables which are evaluated each step of the model to determine + whether the state of the model will change. Returns: None """ - self._model.run_simulation(max_time, infection_threshold) + self._model.run_simulation(state_criteria) diff --git a/household_contact_tracing/branching_process_model.py b/household_contact_tracing/branching_process_model.py index e3b1f24..ee65e14 100644 --- a/household_contact_tracing/branching_process_model.py +++ b/household_contact_tracing/branching_process_model.py @@ -74,13 +74,13 @@ def root_dir(self) -> str: return self.__ROOT_DIR @abstractmethod - def run_simulation(self, max_time: int, infection_threshold: int) -> None: + def run_simulation(self, state_criteria: dict) -> None: """ Run the simulation until it stops (e.g times out, too many infectious nodes or goes extinct) Parameters: - max_time (int): The maximum number of iterations (eg. days) to be run (simulation stops if reached) - infection_threshold (int): The maximum number of infectious nodes (simulation stops if reached) + state_criteria: Named variables which are evaluated each step of the model to determine + whether the state of the model will change. Returns: None diff --git a/household_contact_tracing/branching_process_models.py b/household_contact_tracing/branching_process_models.py index 2b34256..5338db6 100644 --- a/household_contact_tracing/branching_process_models.py +++ b/household_contact_tracing/branching_process_models.py @@ -1,3 +1,4 @@ +import math import os from typing import Callable from copy import deepcopy @@ -14,6 +15,7 @@ import household_contact_tracing.behaviours.intervention.increment_tracing as increment import household_contact_tracing.behaviours.intervention.isolation as isolation import household_contact_tracing.behaviours.infection.new_infection as new_infection +from household_contact_tracing.utilities import ParameterError class HouseholdLevelTracing(BranchingProcessModel): @@ -100,23 +102,23 @@ def simulate_one_step(self): # increment time self.time += 1 - def run_simulation(self, max_time: int, infection_threshold: int = 1000) -> None: + def run_simulation(self, state_criteria: dict) -> None: """ Runs the simulation: Sets model state, Announces start/stopped and step increments to observers Arguments: - max_time -- The maximum number of step increments to perform (stops if self.time >= - max_time). Self.time is cumulative throughout multiple calls to run_simulation. - infection_threshold -- The maximum number of infectious nodes allowed, - before stopping simulation + state_criteria: Named variables which are evaluated each step of the model to determine + whether the state of the model will change. Returns: None """ + self.set_default_state_criteria(state_criteria) + # Switch model to RunningState - self._state.switch(RunningState, max_time=max_time, infection_threshold=infection_threshold) + self._state.switch(RunningState, state_criteria) while type(self.state) is RunningState: prev_network = deepcopy(self.network) @@ -131,29 +133,54 @@ def run_simulation(self, max_time: int, infection_threshold: int = 1000) -> None # Call parent completed step super()._completed_step_increment() - if self.time >= max_time: - # Simulation ends if max_time is reached - self.state.switch(TimedOutState, - total_increments=self.time, - non_recovered_nodes=self.network.count_non_recovered_nodes(), - total_nodes=self.network.node_count - ) - elif self.network.count_non_recovered_nodes() == 0: - # Simulation ends if no more infectious nodes - self.state.switch(ExtinctState, - total_increments=self.time, - non_recovered_nodes=0, - total_nodes=self.network.node_count) - elif self.network.count_non_recovered_nodes() > infection_threshold: - # Simulation ends if number of infectious nodes > threshold - self.state.switch(MaxNodesInfectiousState, - total_increments=self.time, - non_recovered_nodes=0, - total_nodes=self.network.node_count) + self.evaluate_model_state(state_criteria) # Tell parent simulation stopped super()._simulation_stopped() + def evaluate_model_state(self, state_criteria: dict): + """Determine whether the state of the model has changed by evaluating the data from the last simulation step + against criteria which trigger a change of state.""" + + if self.time >= state_criteria["max_time"]: + # Simulation ends if max_time is reached + self.state.switch(TimedOutState, + total_increments=self.time, + non_recovered_nodes=self.network.count_non_recovered_nodes(), + total_nodes=self.network.node_count + ) + elif self.network.count_non_recovered_nodes() == state_criteria["min_non_recovered_nodes"]: + # Simulation ends if no more infectious nodes + self.state.switch(ExtinctState, + total_increments=self.time, + non_recovered_nodes=self.network.count_non_recovered_nodes(), + total_nodes=self.network.node_count) + elif self.network.count_non_recovered_nodes() > state_criteria["infection_threshold"]: + # Simulation ends if number of infectious nodes > threshold + self.state.switch(MaxNodesInfectiousState, + total_increments=self.time, + non_recovered_nodes=0, + total_nodes=self.network.node_count) + + @staticmethod + def set_default_state_criteria(state_criteria: dict): + """Set default values for the state criteria if they have not yet been set.""" + valid_state_criteria = ["max_time", "min_non_recovered_nodes", "infection_threshold"] + + for criterion in state_criteria: + if criterion not in valid_state_criteria: + raise ParameterError(f"Criterion '{criterion}', is not a valid state criterion.\n" + f"Valid state criteria are: {valid_state_criteria}.") + + if "infection_threshold" not in state_criteria: + state_criteria["infection_threshold"] = 10000 + + if "max_time" not in state_criteria: + state_criteria["max_time"] = math.inf + + if "min_non_recovered_nodes" not in state_criteria: + state_criteria["min_non_recovered_nodes"] = 0 + class IndividualLevelTracing(HouseholdLevelTracing): """ @@ -250,10 +277,10 @@ def _initialise_infection(self): def _initialise_intervention(self): """ Initialise an Intervention class, passing in the required behaviours into its constructor """ - new_intervention = Intervention(self.network, - isolation.DailyTestingIsolation, - increment.IncrementTracingIndividualDailyTesting, - self.params) + new_intervention = Intervention(self.network, + isolation.DailyTestingIsolation, + increment.IncrementTracingIndividualDailyTesting, + self.params) # Set a new positive pcr probability function new_intervention.increment_tracing.prob_pcr_positive = self.prob_pcr_positive diff --git a/household_contact_tracing/utilities.py b/household_contact_tracing/utilities.py index 560b553..ee6d979 100644 --- a/household_contact_tracing/utilities.py +++ b/household_contact_tracing/utilities.py @@ -17,7 +17,7 @@ def run_parameterised_simulation(model_type: Type[HouseholdLevelTracing], num_st model_results = [] for param_set in processed_params: model = model_type(param_set) - model.run_simulation(num_steps, 1000) + model.run_simulation({"max_time": num_steps, "infection_threshold": 1000}) model_results.append(model) print(len(model_results)) diff --git a/test/test_integration.py b/test/test_integration.py index f4cdc6f..5fd04d7 100644 --- a/test/test_integration.py +++ b/test/test_integration.py @@ -51,7 +51,7 @@ def run_simulation(params: dict, days=10) -> BranchingProcessModel: model.""" controller = BranchingProcessController(bpm.HouseholdLevelTracing(params)) controller.set_graphic_displays(False) - controller.run_simulation(days) + controller.run_simulation({"max_time": days}) return controller.model @@ -300,7 +300,7 @@ def run_simulation(params: dict, days=10) -> BranchingProcessModel: model.""" controller = BranchingProcessController(bpm.IndividualLevelTracing(params)) controller.set_graphic_displays(False) - controller.run_simulation(days) + controller.run_simulation({"max_time": days}) return controller.model @@ -350,7 +350,7 @@ def run_simulation(params: dict, days=10) -> BranchingProcessModel: return the model.""" controller = BranchingProcessController(bpm.IndividualTracingDailyTesting(params)) controller.set_graphic_displays(False) - controller.run_simulation(days) + controller.run_simulation({"max_time": days}) return controller.model From 37baf492532f4d29859852861a01009f5b456740 Mon Sep 17 00:00:00 2001 From: Peter Crowther <13117069+merrygoat@users.noreply.github.com> Date: Mon, 26 Jul 2021 15:59:47 +0100 Subject: [PATCH 29/70] Change state_criteria to model attribute --- .../branching_process_model.py | 1 + .../branching_process_models.py | 54 +++++++++---------- .../branching_process_state.py | 3 +- household_contact_tracing/utilities.py | 9 ++-- 4 files changed, 33 insertions(+), 34 deletions(-) diff --git a/household_contact_tracing/branching_process_model.py b/household_contact_tracing/branching_process_model.py index ee65e14..bb98f5a 100644 --- a/household_contact_tracing/branching_process_model.py +++ b/household_contact_tracing/branching_process_model.py @@ -42,6 +42,7 @@ def __init__(self): # Set state self._state = ReadyState(self) + self.state_criteria = [] @property def state(self) -> BranchingProcessState: diff --git a/household_contact_tracing/branching_process_models.py b/household_contact_tracing/branching_process_models.py index 5338db6..c10ebb6 100644 --- a/household_contact_tracing/branching_process_models.py +++ b/household_contact_tracing/branching_process_models.py @@ -114,11 +114,12 @@ def run_simulation(self, state_criteria: dict) -> None: Returns: None """ + self.state_criteria = state_criteria - self.set_default_state_criteria(state_criteria) + self.set_default_state_criteria() # Switch model to RunningState - self._state.switch(RunningState, state_criteria) + self._state.switch(RunningState, self.state_criteria) while type(self.state) is RunningState: prev_network = deepcopy(self.network) @@ -133,53 +134,48 @@ def run_simulation(self, state_criteria: dict) -> None: # Call parent completed step super()._completed_step_increment() - self.evaluate_model_state(state_criteria) + self.evaluate_model_state() # Tell parent simulation stopped super()._simulation_stopped() - def evaluate_model_state(self, state_criteria: dict): + def evaluate_model_state(self, ): """Determine whether the state of the model has changed by evaluating the data from the last simulation step against criteria which trigger a change of state.""" - if self.time >= state_criteria["max_time"]: + if self.time >= self.state_criteria["max_time"]: # Simulation ends if max_time is reached - self.state.switch(TimedOutState, - total_increments=self.time, - non_recovered_nodes=self.network.count_non_recovered_nodes(), - total_nodes=self.network.node_count - ) - elif self.network.count_non_recovered_nodes() == state_criteria["min_non_recovered_nodes"]: + self.state.switch(TimedOutState, {"total_increments": self.time, + "non_recovered_nodes": self.network.count_non_recovered_nodes(), + "total_nodes": self.network.node_count}) + elif self.network.count_non_recovered_nodes() == self.state_criteria["min_non_recovered_nodes"]: # Simulation ends if no more infectious nodes - self.state.switch(ExtinctState, - total_increments=self.time, - non_recovered_nodes=self.network.count_non_recovered_nodes(), - total_nodes=self.network.node_count) - elif self.network.count_non_recovered_nodes() > state_criteria["infection_threshold"]: + self.state.switch(ExtinctState, {"total_increments": self.time, + "non_recovered_nodes": self.network.count_non_recovered_nodes(), + "total_nodes": self.network.node_count}) + elif self.network.count_non_recovered_nodes() > self.state_criteria["infection_threshold"]: # Simulation ends if number of infectious nodes > threshold - self.state.switch(MaxNodesInfectiousState, - total_increments=self.time, - non_recovered_nodes=0, - total_nodes=self.network.node_count) + self.state.switch(MaxNodesInfectiousState, {"total_increments": self.time, + "non_recovered_nodes": 0, + "total_nodes": self.network.node_count}) - @staticmethod - def set_default_state_criteria(state_criteria: dict): + def set_default_state_criteria(self): """Set default values for the state criteria if they have not yet been set.""" valid_state_criteria = ["max_time", "min_non_recovered_nodes", "infection_threshold"] - for criterion in state_criteria: + for criterion in self.state_criteria: if criterion not in valid_state_criteria: raise ParameterError(f"Criterion '{criterion}', is not a valid state criterion.\n" f"Valid state criteria are: {valid_state_criteria}.") - if "infection_threshold" not in state_criteria: - state_criteria["infection_threshold"] = 10000 + if "infection_threshold" not in self.state_criteria: + self.state_criteria["infection_threshold"] = 10000 - if "max_time" not in state_criteria: - state_criteria["max_time"] = math.inf + if "max_time" not in self.state_criteria: + self.state_criteria["max_time"] = math.inf - if "min_non_recovered_nodes" not in state_criteria: - state_criteria["min_non_recovered_nodes"] = 0 + if "min_non_recovered_nodes" not in self.state_criteria: + self.state_criteria["min_non_recovered_nodes"] = 0 class IndividualLevelTracing(HouseholdLevelTracing): diff --git a/household_contact_tracing/branching_process_state.py b/household_contact_tracing/branching_process_state.py index da0bf5b..9d06a60 100644 --- a/household_contact_tracing/branching_process_state.py +++ b/household_contact_tracing/branching_process_state.py @@ -30,13 +30,14 @@ class BranchingProcessState(ABC): def __init__(self, simulation_model): self._simulation_model = simulation_model - def switch(self, state: Type[BranchingProcessState], **state_info): + def switch(self, state: Type[BranchingProcessState], state_info: dict): """ Switch to a new state if new state is allowed (for current state) (If not, raise ValueError) Parameters: state: The new state to be switched to + state_info: A dictionary of information about the model at the time of the change of state. Returns: None diff --git a/household_contact_tracing/utilities.py b/household_contact_tracing/utilities.py index ee6d979..bc19fc3 100644 --- a/household_contact_tracing/utilities.py +++ b/household_contact_tracing/utilities.py @@ -1,16 +1,17 @@ -from typing import Type, List +from __future__ import annotations +from typing import Type, List, TYPE_CHECKING from copy import deepcopy import itertools -from household_contact_tracing.branching_process_models import HouseholdLevelTracing +if TYPE_CHECKING: + from household_contact_tracing.branching_process_models import HouseholdLevelTracing class ParameterError(Exception): """Raised if simulation parameters cannot be parsed.""" -def run_parameterised_simulation(model_type: Type[HouseholdLevelTracing], num_steps: int, - params: dict): +def run_parameterised_simulation(model_type: Type[HouseholdLevelTracing], num_steps: int, params: dict): """Assume sequence nesting is combinatorial at first.""" processed_params = process_sequences(params) From 6e47bf0b8c59ae7e2771db9a722c9fcf8073e1dc Mon Sep 17 00:00:00 2001 From: annannfryingpan Date: Sat, 31 Jul 2021 08:41:01 +0100 Subject: [PATCH 30/70] Added node_attributes module with all attribute types --- household_contact_tracing/network.py | 1 + household_contact_tracing/node_attributes.py | 111 +++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 household_contact_tracing/node_attributes.py diff --git a/household_contact_tracing/network.py b/household_contact_tracing/network.py index cff569c..404122f 100644 --- a/household_contact_tracing/network.py +++ b/household_contact_tracing/network.py @@ -6,6 +6,7 @@ from dataclasses import dataclass from household_contact_tracing.parameterised import Parameterised +from household_contact_tracing.node_attributes import NodeAttributes class EdgeType(Enum): diff --git a/household_contact_tracing/node_attributes.py b/household_contact_tracing/node_attributes.py new file mode 100644 index 0000000..df4ab39 --- /dev/null +++ b/household_contact_tracing/node_attributes.py @@ -0,0 +1,111 @@ +from typing import Optional, Iterator, List, Tuple, Dict, Callable + +from household_contact_tracing.parameterised import Parameterised + + +class NodeAttributes(Parameterised): + """ + + """ + pass + + +class InfectionAttributes(NodeAttributes): + """ + + """ + + def __init__(self, **attributes): + self.asymptomatic = None + self.infecting_node = None + self.isolated = None + self.outside_house_contacts_made = 0 + self.recovered = None + self.recovery_time = None + self.spread_to_global_node_time_tuples = [] + self.time_infected = None + + # Update instance variables with anything in attributes + self.update_params(attributes) + + +class LFDTestingAttributes(NodeAttributes): + """ + + """ + + def __init__(self, **attributes): + self.avenue_of_testing = None + self.being_lateral_flow_tested = None + self.positive_test_time = None + self.taken_confirmatory_PCR_test = None + self.time_started_lfa_testing = None + + # Todo Ann estimated location - CHECK + self.propensity_risky_behaviour_lfa_testing = None + self.propensity_to_miss_lfa_tests = None + self.confirmatory_PCR_test_result_time = None + self.completed_lateral_flow_testing_time = None + self.lateral_flow_testing_duration = 0 + + # Update instance variables with anything in attributes + self.update_params(attributes) + + +class LFDTestingAdherenceAttributes(NodeAttributes): + """ + + """ + + def __init__(self, **attributes): + self.confirmatory_PCR_result_was_positive: Optional[bool] = None + self.node_will_take_up_lfa_testing = None + + # Update instance variables with anything in attributes + self.update_params(attributes) + + +class ReturningTravellerAttributes(NodeAttributes): + """ + + """ + + def __init__(self, **attributes): + self.pseudo_symptom_onset_time = None + + # Update instance variables with anything in attributes + self.update_params(attributes) + + +class TracingAttributes(NodeAttributes): + """ + + """ + + def __init__(self, **attributes): + self.contact_traced = None + self.has_contact_tracing_app = None + self.propagated_contact_tracing = False + self.received_positive_test_result = False + self.received_result = False + self.symptom_onset_time = None + self.testing_delay = None + self.time_of_reporting = None + self.will_report_infection = None + self.completed_isolation = None + + # Update instance variables with anything in attributes + self.update_params(attributes) + + +class TracingAdherenceAttributes(NodeAttributes): + """ + + """ + + def __init__(self, **attributes): + self.propensity_imperfect_isolation = None + self.will_uptake_isolation = None + + # Update instance variables with anything in attributes + self.update_params(attributes) From da36704bce9e39c991093023b725d681db27fce2 Mon Sep 17 00:00:00 2001 From: annannfryingpan Date: Sat, 31 Jul 2021 10:41:12 +0100 Subject: [PATCH 31/70] Switched params in Node init --- household_contact_tracing/network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/household_contact_tracing/network.py b/household_contact_tracing/network.py index 404122f..a19f3c8 100644 --- a/household_contact_tracing/network.py +++ b/household_contact_tracing/network.py @@ -257,7 +257,7 @@ class Node(Parameterised): """ - def __init__(self, node_id: int, time_infected: int, household: Household, isolated: bool, + def __init__(self, node_id: int, household: Household, time_infected: int, isolated: bool, will_uptake_isolation: bool, propensity_imperfect_isolation: bool, asymptomatic: bool, symptom_onset_time: float, pseudo_symptom_onset_time: int, recovery_time: int, will_report_infection: bool, time_of_reporting: int, From 48498f8afcc05c55a639ee572e7bfa85beefadbd Mon Sep 17 00:00:00 2001 From: annannfryingpan Date: Sat, 31 Jul 2021 13:41:03 +0100 Subject: [PATCH 32/70] Implemented node attribute parameters for 4/6 of the attribute categories --- examples/run_testing_contact_model.py | 68 +++++++++- .../infection/contact_rate_reduction.py | 10 +- .../behaviours/infection/new_infection.py | 27 ++-- .../intervention/increment_tracing.py | 26 ++-- .../behaviours/intervention/isolation.py | 18 +-- household_contact_tracing/intervention.py | 16 ++- household_contact_tracing/network.py | 125 +++++++++--------- household_contact_tracing/node_attributes.py | 14 +- test/test_TestingContactModel.py | 34 ++--- test/test_integration.py | 2 +- 10 files changed, 206 insertions(+), 134 deletions(-) diff --git a/examples/run_testing_contact_model.py b/examples/run_testing_contact_model.py index 2c986fc..a640ba8 100644 --- a/examples/run_testing_contact_model.py +++ b/examples/run_testing_contact_model.py @@ -1,14 +1,16 @@ import os -from copy import copy +from copy import copy, deepcopy from household_contact_tracing.branching_process_controller import BranchingProcessController from household_contact_tracing.branching_process_models import IndividualTracingDailyTesting def main(): - #example_1() - #example_2() + example_1() + example_2() example_3() + #example_4() + def example_1(): @@ -153,6 +155,66 @@ def example_3(): controller.timeline_view.set_display(True) controller.run_simulation(16) +def example_4(): + params = {"outside_household_infectivity_scaling": 0.3, + "contact_tracing_success_prob": 0.7, + "overdispersion": 0.32, + "asymptomatic_prob": 0.2, + "asymptomatic_relative_infectivity": 0.35, + "infection_reporting_prob": 0.3, + "LFA_testing_requires_confirmatory_PCR": False, + "test_delay": 1, + "contact_trace_delay": 1, + "incubation_period_delay": 5, + "symptom_reporting_delay": 1, + "household_pairwise_survival_prob": 0.2, + "propensity_risky_behaviour_lfa_testing": 0, + "global_contact_reduction_risky_behaviour": 0, + "household_positive_policy": "lfa_testing_no_quarantine" + } + # Copy of test_TestingContactModel.py - test_traced_nodes_are_lateral_flow_tested(simple_model_high_test_prob): + """Checks that a node who is traced is placed under lateral flow testing. + + To do this we: + * Initialises a model with 100% contact tracing success probability + * Create a new infection outside the initial household + * Household 1 traces household 2 with 100% success probability and delay 1 + * Simulate one day twice + + """ + def prob_testing_positive_lfa_func(infectious_age): + if infectious_age in [4, 5, 6]: + return 1 + else: + return 0 + + def prob_testing_positive_pcr_func(infectious_age): + if infectious_age in [4, 5, 6]: + return 0 + else: + return 0 + + params["contact_tracing_success_prob"] = 1 + + model = IndividualTracingDailyTesting(params) + model.intervention.increment_tracing.prob_pcr_positive = prob_testing_positive_pcr_func + model.prob_lfa_positive = prob_testing_positive_lfa_func + + model.infection.new_outside_household_infection(time=0, infecting_node=model.network.node(1)) + + model.intervention.increment_tracing.attempt_contact_trace_of_household( + house_to=model.network.household(2), + house_from=model.network.household(1), + days_since_contact_occurred=0, + contact_trace_delay=0, + time=0 + ) + + model.simulate_one_step() + model.simulate_one_step() + + assert model.network.node(2).lfd_testing.being_lateral_flow_tested is True + if __name__ == "__main__": main() diff --git a/household_contact_tracing/behaviours/infection/contact_rate_reduction.py b/household_contact_tracing/behaviours/infection/contact_rate_reduction.py index aced721..5399cf0 100644 --- a/household_contact_tracing/behaviours/infection/contact_rate_reduction.py +++ b/household_contact_tracing/behaviours/infection/contact_rate_reduction.py @@ -58,9 +58,9 @@ def get_contact_rate_reduction(self, node: Node) -> int: intervention parameters """ - if node.isolated and node.propensity_imperfect_isolation: + if node.isolated and node.tracing_adherence.propensity_imperfect_isolation: return self.global_contact_reduction_imperfect_quarantine - elif node.isolated and not node.propensity_imperfect_isolation: + elif node.isolated and not node.tracing_adherence.propensity_imperfect_isolation: # return 1 means 100% of contacts are stopped return 1 else: @@ -81,15 +81,15 @@ def get_contact_rate_reduction(self, node: Node) -> int: """ # the isolated status should never apply to an individual who will not uptake intervention - if node.isolated and not node.propensity_imperfect_isolation: + if node.isolated and not node.tracing_adherence.propensity_imperfect_isolation: # perfect intervention return 1 - elif node.isolated and node.propensity_imperfect_isolation: + elif node.isolated and node.tracing_adherence.propensity_imperfect_isolation: # imperfect intervention return self.global_contact_reduction_imperfect_quarantine - elif node.being_lateral_flow_tested and node.propensity_risky_behaviour_lfa_testing: + elif node.lfd_testing.being_lateral_flow_tested and node.lfd_testing.propensity_risky_behaviour_lfa_testing: # engaging in risky behaviour while testing negative return self.global_contact_reduction_risky_behaviour diff --git a/household_contact_tracing/behaviours/infection/new_infection.py b/household_contact_tracing/behaviours/infection/new_infection.py index 9642556..2548d60 100644 --- a/household_contact_tracing/behaviours/infection/new_infection.py +++ b/household_contact_tracing/behaviours/infection/new_infection.py @@ -169,13 +169,19 @@ def new_infection(self, time: int, household: Household, infecting_node: Optiona else: node_is_isolated = False + tracing_adherence_attributes = {'will_uptake_isolation': isolation_uptake, + 'propensity_imperfect_isolation': self.get_propensity_imperfect_isolation() + } + returning_travellers_attributes = {'pseudo_symptom_onset_time': pseudo_symptom_onset_time} + new_node = self.network.add_node(time_infected=time, - household_id=household.id, isolated=node_is_isolated, - will_uptake_isolation=isolation_uptake, - propensity_imperfect_isolation=self.get_propensity_imperfect_isolation(), - asymptomatic=asymptomatic, contact_traced=household.contact_traced, + household_id=household.id, + isolated=node_is_isolated, + tracing_adherence_attributes=tracing_adherence_attributes, + returning_travellers_attributes=returning_travellers_attributes, + asymptomatic=asymptomatic, + contact_traced=household.contact_traced, symptom_onset_time=symptom_onset_time, - pseudo_symptom_onset_time=pseudo_symptom_onset_time, recovery_time=recovery_time, will_report_infection=will_report_infection, time_of_reporting=time_of_reporting, @@ -271,15 +277,20 @@ def new_infection(self, time: int, household: Household, infecting_node: Optiona else: node_is_isolated = False + tracing_adherence_attributes = { + 'will_uptake_isolation': isolation_uptake, + 'propensity_imperfect_isolation': self.get_propensity_imperfect_isolation() + } + returning_travellers_attributes = {'pseudo_symptom_onset_time': pseudo_symptom_onset_time} + new_node = self.network.add_node(time_infected=time, household_id=household.id, isolated=node_is_isolated, - will_uptake_isolation=isolation_uptake, - propensity_imperfect_isolation=self.get_propensity_imperfect_isolation(), + tracing_adherence_attributes=tracing_adherence_attributes, + returning_travellers_attributes=returning_travellers_attributes, asymptomatic=asymptomatic, contact_traced=household.contact_traced, symptom_onset_time=symptom_onset_time, - pseudo_symptom_onset_time=pseudo_symptom_onset_time, recovery_time=recovery_time, will_report_infection=will_report_infection, time_of_reporting=time_of_reporting, diff --git a/household_contact_tracing/behaviours/intervention/increment_tracing.py b/household_contact_tracing/behaviours/intervention/increment_tracing.py index 0826466..d9028e8 100644 --- a/household_contact_tracing/behaviours/intervention/increment_tracing.py +++ b/household_contact_tracing/behaviours/intervention/increment_tracing.py @@ -199,11 +199,11 @@ def pcr_test_node(self, node: Node, time: int): node.received_result = True infectious_age_when_tested = time - node.testing_delay - node.time_infected prob_positive_result = self.prob_pcr_positive(infectious_age_when_tested) - node.avenue_of_testing = TestType.pcr + node.lfd_testing.avenue_of_testing = TestType.pcr if np.random.binomial(1, prob_positive_result) == 1: node.received_positive_test_result = True - node.positive_test_time = time + node.lfd_testing.positive_test_time = time else: node.received_positive_test_result = False @@ -367,22 +367,22 @@ def receive_pcr_test_results(self, time: int): def increment_contact_tracing(self, time: int): for node in self.network.all_nodes(): if node.received_positive_test_result: - if node.avenue_of_testing == TestType.pcr: + if node.lfd_testing.avenue_of_testing == TestType.pcr: if not node.propagated_contact_tracing: self.propagate_contact_tracing(node, time) if not self.LFA_testing_requires_confirmatory_PCR: for node in self.network.all_nodes(): if node.received_positive_test_result: - if node.avenue_of_testing == TestType.lfa: + if node.lfd_testing.avenue_of_testing == TestType.lfa: if not node.propagated_contact_tracing: self.propagate_contact_tracing(node, time) elif self.LFA_testing_requires_confirmatory_PCR: for node in self.network.all_nodes(): - if node.confirmatory_PCR_test_result_time == time: - if node.confirmatory_PCR_result_was_positive: - if node.avenue_of_testing == TestType.lfa: + if node.lfd_testing.confirmatory_PCR_test_result_time == time: + if node.lfd_testing_adherence.confirmatory_PCR_result_was_positive: + if node.lfd_testing.avenue_of_testing == TestType.lfa: if not node.propagated_contact_tracing: self.propagate_contact_tracing(node, time) @@ -410,7 +410,7 @@ def propagate_contact_tracing(self, node: Node, time: int): # if the infector is not already isolated and the time the node was infected captured # by going backwards the node.time_infected is when they had a contact with their # infector. - if node.avenue_of_testing == TestType.pcr: + if node.lfd_testing.avenue_of_testing == TestType.pcr: if not infected_by_node.isolated and \ node.time_infected >= node.symptom_onset_time - \ @@ -423,12 +423,12 @@ def propagate_contact_tracing(self, node: Node, time: int): days_since_contact_occurred=time - node.time_infected, time=time) - elif node.avenue_of_testing == TestType.lfa: + elif node.lfd_testing.avenue_of_testing == TestType.lfa: if not self.LFA_testing_requires_confirmatory_PCR: if not infected_by_node.isolated and node.time_infected >= \ - node.positive_test_time - self.number_of_days_prior_to_LFA_result_to_trace: + node.lfd_testing.positive_test_time - self.number_of_days_prior_to_LFA_result_to_trace: # Then attempt to contact trace the household of the node that infected you self.attempt_contact_trace_of_household( @@ -447,7 +447,7 @@ def propagate_contact_tracing(self, node: Node, time: int): child_node = self.network.node(child_node_id) - if node.avenue_of_testing == TestType.pcr: + if node.lfd_testing.avenue_of_testing == TestType.pcr: # If the node was infected 2 days prior to symptom onset, or 7 days post and is # not already isolated @@ -461,13 +461,13 @@ def propagate_contact_tracing(self, node: Node, time: int): days_since_contact_occurred=time - time_t, time=time) - elif node.avenue_of_testing == TestType.lfa: + elif node.lfd_testing.avenue_of_testing == TestType.lfa: if not self.LFA_testing_requires_confirmatory_PCR: # If the node was infected 2 days prior to symptom onset, or 7 days post and # is not already isolated - if time_t >= node.positive_test_time - \ + if time_t >= node.lfd_testing.positive_test_time - \ self.number_of_days_prior_to_LFA_result_to_trace: self.attempt_contact_trace_of_household( diff --git a/household_contact_tracing/behaviours/intervention/isolation.py b/household_contact_tracing/behaviours/intervention/isolation.py index 59fb389..6a29957 100644 --- a/household_contact_tracing/behaviours/intervention/isolation.py +++ b/household_contact_tracing/behaviours/intervention/isolation.py @@ -89,7 +89,7 @@ def isolate_self_reporting_cases(self, time: int): who will not uptake intervention """ for node in self.network.all_nodes(): - if node.will_uptake_isolation: + if node.tracing_adherence.will_uptake_isolation: if node.time_of_reporting == time: node.isolated = True @@ -153,15 +153,15 @@ def update_households_contact_traced(self, time: int): household.update_network() traced_node = household.find_traced_node() # the traced node is now being lateral flow tested - if traced_node.node_will_take_up_lfa_testing: + if traced_node.lfd_testing_adherence.node_will_take_up_lfa_testing: if not traced_node.received_positive_test_result: traced_node.being_lateral_flow_tested = True - traced_node.time_started_lfa_testing = time + traced_node.lfd_testing.time_started_lfa_testing = time def update_isolation(self, time: int): for node in self.network.all_nodes(): - if node.positive_test_time == time: - if node.avenue_of_testing == TestType.pcr: + if node.lfd_testing.positive_test_time == time: + if node.lfd_testing.avenue_of_testing == TestType.pcr: if node.received_positive_test_result: if not node.household.applied_household_positive_policy: node.household.apply_positive_policy(time, self.household_positive_policy) @@ -174,7 +174,7 @@ def act_on_confirmatory_pcr_results(self, time: int): * Contact tracing is propagated """ for node in self.network.all_nodes(): - if node.confirmatory_PCR_test_result_time == time: + if node.lfd_testing.confirmatory_PCR_test_result_time == time: node.household.apply_positive_policy(time, self.household_positive_policy) def isolate_positive_lateral_flow_tests(self, time: int, positive_nodes: List[Node]): @@ -187,11 +187,11 @@ def isolate_positive_lateral_flow_tests(self, time: int, positive_nodes: List[No for node in positive_nodes: node.received_positive_test_result = True - if node.will_uptake_isolation: + if node.tracing_adherence.will_uptake_isolation: node.isolated = True node.avenue_of_testing = TestType.lfa - node.positive_test_time = time + node.lfd_testing.positive_test_time = time node.being_lateral_flow_tested = False if not node.household.applied_household_positive_policy and \ @@ -210,5 +210,5 @@ def act_on_positive_LFA_tests(self, time: int, positive_nodes: List[Node]): def confirmatory_pcr_test_LFA_nodes(self, time: int, positive_nodes: List[Node]): """Nodes who receive a positive LFA result will be tested using a PCR test.""" for node in positive_nodes: - if not node.taken_confirmatory_PCR_test: + if not node.lfd_testing.taken_confirmatory_PCR_test: node.take_confirmatory_pcr_test(time, self.prob_pcr_positive) diff --git a/household_contact_tracing/intervention.py b/household_contact_tracing/intervention.py index a09d907..282f8f7 100644 --- a/household_contact_tracing/intervention.py +++ b/household_contact_tracing/intervention.py @@ -54,7 +54,7 @@ def lft_nodes(self, time: int, prob_lfa_positive: Callable) -> List[Node]: positive_nodes = [] for node in self.network.all_nodes(): - if node.being_lateral_flow_tested: + if node.lfd_testing.being_lateral_flow_tested: if node.will_lfa_test_today(self.node_daily_prob_lfa_test): if not node.received_positive_test_result: if node.lfa_test_node(time, prob_lfa_positive): @@ -110,8 +110,8 @@ def completed_isolation(self, time: int): infection_status = node.infection_status(time) if infection_status in [InfectionStatus.known_infection, InfectionStatus.self_recognised_infection]: - if node.avenue_of_testing == TestType.lfa: - if time >= node.positive_test_time + self.self_isolation_duration: + if node.lfd_testing.avenue_of_testing == TestType.lfa: + if time >= node.lfd_testing.positive_test_time + self.self_isolation_duration: node.isolated = False node.completed_isolation = True else: @@ -137,7 +137,9 @@ def completed_lateral_flow_testing(self, time: int): """ for node in self.network.all_nodes(): - if time >= node.time_started_lfa_testing + self.lateral_flow_testing_duration \ - and node.being_lateral_flow_tested: - node.being_lateral_flow_tested = False - node.completed_lateral_flow_testing_time = time + # Todo: Check Ann's addition of node.lfd_testing.time_started_lfa_testing (not null) condition + if node.lfd_testing.time_started_lfa_testing and\ + time >= node.lfd_testing.time_started_lfa_testing + self.lateral_flow_testing_duration \ + and node.lfd_testing.being_lateral_flow_tested: + node.lfd_testing.being_lateral_flow_tested = False + node.lfd_testing.completed_lateral_flow_testing_time = time diff --git a/household_contact_tracing/network.py b/household_contact_tracing/network.py index a19f3c8..c7f545e 100644 --- a/household_contact_tracing/network.py +++ b/household_contact_tracing/network.py @@ -6,7 +6,8 @@ from dataclasses import dataclass from household_contact_tracing.parameterised import Parameterised -from household_contact_tracing.node_attributes import NodeAttributes +from household_contact_tracing.node_attributes import LFDTestingAdherenceAttributes, TracingAdherenceAttributes, \ + ReturningTravellerAttributes, LFDTestingAttributes class EdgeType(Enum): @@ -193,21 +194,25 @@ def is_edge_app_traced(self, edge: Tuple[int, int]) -> bool: node_2_app = self.node(edge[1]).has_contact_tracing_app return node_1_app and node_2_app - def add_node(self, time_infected, household_id, isolated, will_uptake_isolation, - propensity_imperfect_isolation, asymptomatic, symptom_onset_time, - pseudo_symptom_onset_time, recovery_time, will_report_infection, + def add_node(self, time_infected, household_id, isolated, asymptomatic, symptom_onset_time, + recovery_time, will_report_infection, time_of_reporting, has_contact_tracing_app, contact_traced, testing_delay=0, additional_attributes: Optional[dict] = None, - infecting_node: Optional[Node] = None, completed_isolation=False) -> Node: + infecting_node: Optional[Node] = None, completed_isolation=False, + lfd_testing_adherence_attributes: Optional[dict] = None, + tracing_adherence_attributes: Optional[dict] = None, + returning_travellers_attributes: Optional[dict] = None, + lfd_testing_attributes: Optional[dict] = None + ) -> Node: new_node_id = self.node_count + 1 self.graph.add_node(new_node_id) new_node_household = self.household(household_id) - node = Node(node_id=new_node_id, time_infected=time_infected, - household=new_node_household, isolated=isolated, - will_uptake_isolation=will_uptake_isolation, - propensity_imperfect_isolation=propensity_imperfect_isolation, - asymptomatic=asymptomatic, symptom_onset_time=symptom_onset_time, - pseudo_symptom_onset_time=pseudo_symptom_onset_time, + node = Node(node_id=new_node_id, + time_infected=time_infected, + household=new_node_household, + isolated=isolated, + asymptomatic=asymptomatic, + symptom_onset_time=symptom_onset_time, recovery_time=recovery_time, will_report_infection=will_report_infection, time_of_reporting=time_of_reporting, @@ -215,6 +220,10 @@ def add_node(self, time_infected, household_id, isolated, will_uptake_isolation, contact_traced=contact_traced, testing_delay=testing_delay, additional_attributes=additional_attributes, + lfd_testing_adherence_attributes=lfd_testing_adherence_attributes, + tracing_adherence_attributes=tracing_adherence_attributes, + returning_travellers_attributes=returning_travellers_attributes, + lfd_testing_attributes=lfd_testing_attributes, infecting_node=infecting_node, completed_isolation=completed_isolation) self.graph.nodes[new_node_id]['node_obj'] = node @@ -257,57 +266,45 @@ class Node(Parameterised): """ - def __init__(self, node_id: int, household: Household, time_infected: int, isolated: bool, - will_uptake_isolation: bool, propensity_imperfect_isolation: bool, - asymptomatic: bool, symptom_onset_time: float, pseudo_symptom_onset_time: int, - recovery_time: int, will_report_infection: bool, time_of_reporting: int, - has_contact_tracing_app: bool, contact_traced: bool, testing_delay: int = 0, - completed_isolation=False, outside_house_contacts_made=0, recovered=False, - infecting_node: Optional[Node] = None, additional_attributes: dict = None): + def __init__(self, node_id: int, household: Household, **attributes): self.id = node_id - self.time_infected = time_infected self.household = household - self.isolated = isolated - self.will_uptake_isolation = will_uptake_isolation - self.propensity_imperfect_isolation = propensity_imperfect_isolation - self.asymptomatic = asymptomatic - self.symptom_onset_time = symptom_onset_time - self.pseudo_symptom_onset_time = pseudo_symptom_onset_time - self.recovery_time = recovery_time - self.will_report_infection = will_report_infection - self.time_of_reporting = time_of_reporting - self.has_contact_tracing_app = has_contact_tracing_app - self.testing_delay = testing_delay - self.contact_traced = contact_traced - self.outside_house_contacts_made = outside_house_contacts_made + + self.time_infected = None + self.isolated = None + self.asymptomatic = None + self.symptom_onset_time = None + self.recovery_time = None + self.will_report_infection = None + self.time_of_reporting = None + self.has_contact_tracing_app = None + self.testing_delay = 0 + self.contact_traced = None + self.outside_house_contacts_made = 0 self.spread_to_global_node_time_tuples = [] - self.recovered = recovered + self.recovered = False self.propagated_contact_tracing = False - self.infecting_node = infecting_node if infecting_node else None - self.completed_isolation = completed_isolation + self.infecting_node = None + self.completed_isolation = False self.received_result = False self.received_positive_test_result = False - self.being_lateral_flow_tested = None - self.time_started_lfa_testing = None - self.avenue_of_testing: Optional[TestType] = None - self.positive_test_time = None - self.node_will_take_up_lfa_testing = None - self.confirmatory_PCR_result_was_positive: Optional[bool] = None - self.taken_confirmatory_PCR_test: Optional[bool] = None - self.confirmatory_PCR_test_result_time = None - self.propensity_risky_behaviour_lfa_testing = None - self.propensity_to_miss_lfa_tests = None + # Update node attribute classes + self.lfd_testing_adherence = LFDTestingAdherenceAttributes(attributes['lfd_testing_adherence_attributes']) + self.tracing_adherence = TracingAdherenceAttributes(attributes['tracing_adherence_attributes']) + self.returning_travellers = ReturningTravellerAttributes(attributes['returning_travellers_attributes']) + self.lfd_testing = LFDTestingAttributes(attributes['lfd_testing_attributes']) # Update instance variables with anything in `additional_attributes` - self.update_params(additional_attributes) + self.update_params(attributes) + self.update_params(attributes['additional_attributes']) def time_relative_to_symptom_onset(self, time: int) -> int: # asymptomatics do not have a symptom onset time # pseudo_symptom_onset time is a fake onset we give them # so we can work out when they test positive - return time - self.pseudo_symptom_onset_time + return time - self.returning_travellers.pseudo_symptom_onset_time def locally_infected(self) -> bool: if self.infecting_node: @@ -335,7 +332,7 @@ def node_type(self, time=None) -> NodeType: params time (int): The current increment / step number (e.g. day number) of the simulation """ - if self.being_lateral_flow_tested: + if self.lfd_testing.being_lateral_flow_tested: if self.isolated: return NodeType.being_lateral_flow_tested_isolated else: @@ -354,9 +351,9 @@ def node_type(self, time=None) -> NodeType: return NodeType.received_pos_test_lfa elif self.received_result and self.avenue_of_testing == TestType.pcr: return NodeType.received_neg_test_pcr - elif self.taken_confirmatory_PCR_test: + elif self.lfd_testing.taken_confirmatory_PCR_test: if time and time >= self.confirmatory_PCR_test_result_time: - if self.confirmatory_PCR_result_was_positive: + if self.lfd_testing_adherence.confirmatory_PCR_result_was_positive: return NodeType.confirmatory_pos_pcr_test else: return NodeType.confirmatory_neg_pcr_test @@ -371,17 +368,17 @@ def take_confirmatory_pcr_test(self, time: int, prob_pcr_positive: Callable): infectious_age_when_tested = time - self.time_infected self.confirmatory_PCR_test_result_time = time + self.testing_delay - self.taken_confirmatory_PCR_test = True + self.lfd_testing.taken_confirmatory_PCR_test = True if numpy.random.binomial(1, prob_pcr_positive(infectious_age_when_tested)) == 1: - self.confirmatory_PCR_result_was_positive = True + self.lfd_testing_adherence.confirmatory_PCR_result_was_positive = True else: - self.confirmatory_PCR_result_was_positive = False + self.lfd_testing_adherence.confirmatory_PCR_result_was_positive = False def will_lfa_test_today(self, daily_prob_lfa_test: float) -> bool: """Determine whether a node will do an LFT test today.""" - if not self.propensity_to_miss_lfa_tests: + if not self.lfd_testing.propensity_to_miss_lfa_tests: return True if numpy.random.binomial(1, daily_prob_lfa_test) == 1: @@ -478,7 +475,7 @@ def get_positive_test_times(self, model_time: int) -> List[int]: for node in self.nodes: if node.infection_status(model_time) == InfectionStatus.known_infection: if node.received_positive_test_result: - positive_test_times.append(node.positive_test_time) + positive_test_times.append(node.lfd_testing.positive_test_time) return positive_test_times def earliest_recognised_symptom_onset(self, model_time: int): @@ -517,7 +514,7 @@ def isolate_household(self, time: int): # Update isolated and contact traced status for Nodes in Household for node in self.nodes: node.contact_traced = True - if node.will_uptake_isolation: + if node.tracing_adherence.will_uptake_isolation: node.isolated = True self._update_edges_on_isolation() @@ -549,11 +546,11 @@ def start_lateral_flow_testing_household(self, time: int): self.being_lateral_flow_tested_start_time = time for node in self.nodes: - if node.node_will_take_up_lfa_testing: + if node.lfd_testing_adherence.node_will_take_up_lfa_testing: if not node.received_positive_test_result: if not node.being_lateral_flow_tested: - node.being_lateral_flow_tested = True - node.time_started_lfa_testing = time + node.lfd_testing.being_lateral_flow_tested = True + node.lfd_testing.time_started_lfa_testing = time def start_lateral_flow_testing_household_and_quarantine(self, time): """Sets the household to the lateral flow testing status so that new within household @@ -567,13 +564,13 @@ def start_lateral_flow_testing_household_and_quarantine(self, time): self.contact_traced = True for node in self.nodes: - if node.node_will_take_up_lfa_testing: + if node.lfd_testing_adherence.node_will_take_up_lfa_testing: if not node.received_positive_test_result: if not node.being_lateral_flow_tested: - node.being_lateral_flow_tested = True - node.time_started_lfa_testing = time + node.lfd_testing.being_lateral_flow_tested = True + node.lfd_testing.time_started_lfa_testing = time - if node.will_uptake_isolation: + if node.tracing_adherence.will_uptake_isolation: node.isolated = True def apply_positive_policy(self, time: int, household_positive_policy: str): @@ -631,5 +628,5 @@ def quarantine_traced_node(self): traced_node = self.find_traced_node() # the traced node should go into quarantine - if not traced_node.isolated and traced_node.will_uptake_isolation: + if not traced_node.isolated and traced_node.tracing_adherence.will_uptake_isolation: traced_node.isolated = True diff --git a/household_contact_tracing/node_attributes.py b/household_contact_tracing/node_attributes.py index df4ab39..a48b1a6 100644 --- a/household_contact_tracing/node_attributes.py +++ b/household_contact_tracing/node_attributes.py @@ -15,7 +15,7 @@ class InfectionAttributes(NodeAttributes): """ - def __init__(self, **attributes): + def __init__(self, attributes): self.asymptomatic = None self.infecting_node = None self.isolated = None @@ -34,8 +34,8 @@ class LFDTestingAttributes(NodeAttributes): """ - def __init__(self, **attributes): - self.avenue_of_testing = None + def __init__(self, attributes): + self.avenue_of_testing: Optional[int] = None self.being_lateral_flow_tested = None self.positive_test_time = None self.taken_confirmatory_PCR_test = None @@ -57,7 +57,7 @@ class LFDTestingAdherenceAttributes(NodeAttributes): """ - def __init__(self, **attributes): + def __init__(self, attributes): self.confirmatory_PCR_result_was_positive: Optional[bool] = None self.node_will_take_up_lfa_testing = None @@ -70,7 +70,7 @@ class ReturningTravellerAttributes(NodeAttributes): """ - def __init__(self, **attributes): + def __init__(self, attributes): self.pseudo_symptom_onset_time = None # Update instance variables with anything in attributes @@ -82,7 +82,7 @@ class TracingAttributes(NodeAttributes): """ - def __init__(self, **attributes): + def __init__(self, attributes): self.contact_traced = None self.has_contact_tracing_app = None self.propagated_contact_tracing = False @@ -103,7 +103,7 @@ class TracingAdherenceAttributes(NodeAttributes): """ - def __init__(self, **attributes): + def __init__(self, attributes): self.propensity_imperfect_isolation = None self.will_uptake_isolation = None diff --git a/test/test_TestingContactModel.py b/test/test_TestingContactModel.py index f3cca6b..73fbcb2 100644 --- a/test/test_TestingContactModel.py +++ b/test/test_TestingContactModel.py @@ -127,13 +127,13 @@ def prob_testing_positive_pcr_func(infectious_age): model.prob_pcr_positive = prob_testing_positive_pcr_func model.prob_lfa_positive = prob_testing_positive_lfa_func - assert model.network.node(1).pseudo_symptom_onset_time == 5 + assert model.network.node(1).returning_travellers.pseudo_symptom_onset_time == 5 def test_pseudo_symptom_onset(simple_model): """Checks that it is also working for symptomatics """ - assert simple_model.network.node(1).pseudo_symptom_onset_time == 5 + assert simple_model.network.node(1).returning_travellers.pseudo_symptom_onset_time == 5 def test_time_relative_to_symptom_onset(simple_model): @@ -165,7 +165,7 @@ def test_being_lateral_flow_tested_attribute(simple_model): """Check nodes are generated with the lateral flow testing attribute """ - assert not simple_model.network.node(1).being_lateral_flow_tested + assert not simple_model.network.node(1).lfd_testing.being_lateral_flow_tested def test_get_positive_lateral_flow_nodes_default_exclusion(simple_model_high_test_prob): @@ -185,7 +185,7 @@ def test_get_positive_lateral_flow_nodes_timings(simple_model_high_test_prob): node_of_interest = model.network.node(1) - node_of_interest.being_lateral_flow_tested = True + node_of_interest.lfd_testing.being_lateral_flow_tested = True assert model.intervention.lft_nodes(model.time, model.prob_lfa_positive) == [] @@ -198,7 +198,7 @@ def test_get_positive_lateral_flow_nodes(simple_model_high_test_prob): node_of_interest = model.network.node(1) - node_of_interest.being_lateral_flow_tested = True + node_of_interest.lfd_testing.being_lateral_flow_tested = True model.time = 5 @@ -247,7 +247,7 @@ def prob_testing_positive_pcr_func(infectious_age): model.simulate_one_step() model.simulate_one_step() - assert model.network.node(2).being_lateral_flow_tested is True + assert model.network.node(2).lfd_testing.being_lateral_flow_tested is True def test_isolate_positive_lateral_flow_tests(simple_model_high_test_prob: IndividualTracingDailyTesting): @@ -257,7 +257,7 @@ def test_isolate_positive_lateral_flow_tests(simple_model_high_test_prob: Indivi model.time = 5 - model.network.node(1).being_lateral_flow_tested = True + model.network.node(1).lfd_testing.being_lateral_flow_tested = True positive_nodes = model.intervention.lft_nodes(model.time, model.prob_lfa_positive) new_isolation = DailyTestingIsolation(model.network, model.params) @@ -271,7 +271,7 @@ def test_isolate_positive_lateral_flow_tests(simple_model_high_test_prob: Indivi assert model.network.household(1).applied_household_positive_policy assert model.network.node(1).received_positive_test_result assert not model.network.node(2).isolated - assert model.network.node(2).being_lateral_flow_tested + assert model.network.node(2).lfd_testing.being_lateral_flow_tested @pytest.fixture @@ -315,7 +315,7 @@ def test_start_lateral_flow_testing_household_and_quarantine( model.time = 5 - model.network.node(1).being_lateral_flow_tested = True + model.network.node(1).lfd_testing.being_lateral_flow_tested = True positive_nodes = model.intervention.lft_nodes(model.time, model.prob_lfa_positive) @@ -329,7 +329,7 @@ def test_start_lateral_flow_testing_household_and_quarantine( assert model.network.household(1).applied_household_positive_policy assert model.network.node(1).received_positive_test_result assert model.network.node(2).isolated - assert model.network.node(2).being_lateral_flow_tested + assert model.network.node(2).lfd_testing.being_lateral_flow_tested @pytest.fixture @@ -375,7 +375,7 @@ def test_household_contacts_quarantine_only( model.time = 5 - model.network.node(1).being_lateral_flow_tested = True + model.network.node(1).lfd_testing.being_lateral_flow_tested = True # this line is required before the isolate_positive_lateral_flow_tests func can work positive_nodes = model.intervention.lft_nodes(model.time, model.prob_lfa_positive) @@ -389,14 +389,14 @@ def test_household_contacts_quarantine_only( assert model.network.household(1).applied_household_positive_policy assert model.network.node(1).received_positive_test_result assert model.network.node(2).isolated - assert model.network.node(2).being_lateral_flow_tested + assert model.network.node(2).lfd_testing.being_lateral_flow_tested def test_risky_behaviour_attributes_default(simple_model: simple_model): """Tests that the default behaviour is no more risky behaviour """ - assert not simple_model.network.node(1).propensity_risky_behaviour_lfa_testing + assert not simple_model.network.node(1).lfd_testing.propensity_risky_behaviour_lfa_testing def test_risky_behaviour_attributes(simple_model_risky_behaviour: simple_model_risky_behaviour): @@ -404,7 +404,7 @@ def test_risky_behaviour_attributes(simple_model_risky_behaviour: simple_model_r being tested. """ - assert simple_model_risky_behaviour.network.node(1).propensity_risky_behaviour_lfa_testing + assert simple_model_risky_behaviour.network.node(1).lfd_testing.propensity_risky_behaviour_lfa_testing @pytest.fixture @@ -458,7 +458,7 @@ def test_lfa_tested_nodes_make_more_contacts_if_risky( model = simple_model_risky_behaviour_2_infections - model.network.node(1).propensity_risky_behaviour_lfa_testing = False + model.network.node(1).lfd_testing.propensity_risky_behaviour_lfa_testing = False # stop there being any within household infections # not sure if this is strictly necessary @@ -468,8 +468,8 @@ def test_lfa_tested_nodes_make_more_contacts_if_risky( model.network.household(2).susceptibles = 0 # set the nodes to being lfa tested - model.network.node(1).being_lateral_flow_tested = True - model.network.node(2).being_lateral_flow_tested = True + model.network.node(1).lfd_testing.being_lateral_flow_tested = True + model.network.node(2).lfd_testing.being_lateral_flow_tested = True for _ in range(5): model.simulate_one_step() diff --git a/test/test_integration.py b/test/test_integration.py index f4cdc6f..7c8a15f 100644 --- a/test/test_integration.py +++ b/test/test_integration.py @@ -242,7 +242,7 @@ def test_imperfect_isolation(self, household_params): numpy.random.seed(42) model = self.run_simulation(household_params) network = model.network - node_imperfect = [node.propensity_imperfect_isolation for node in network.all_nodes()] + node_imperfect = [node.tracing_adherence.propensity_imperfect_isolation for node in network.all_nodes()] assert any(node_imperfect) node_contact_rate_reduction = \ [model.infection.contact_rate_reduction.get_contact_rate_reduction(node) for node in network.all_nodes()] From 33bbf895a6f8802abf375405a8a169facd23d0fa Mon Sep 17 00:00:00 2001 From: annannfryingpan Date: Sat, 31 Jul 2021 14:32:25 +0100 Subject: [PATCH 33/70] Fixed additional_attributes being added in wrong place --- examples/run_testing_contact_model.py | 6 ++-- .../behaviours/infection/new_infection.py | 30 +++++++++++-------- household_contact_tracing/network.py | 7 +++-- 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/examples/run_testing_contact_model.py b/examples/run_testing_contact_model.py index a640ba8..f40c626 100644 --- a/examples/run_testing_contact_model.py +++ b/examples/run_testing_contact_model.py @@ -6,10 +6,10 @@ def main(): - example_1() - example_2() + #example_1() + #example_2() example_3() - #example_4() + example_4() def example_1(): diff --git a/household_contact_tracing/behaviours/infection/new_infection.py b/household_contact_tracing/behaviours/infection/new_infection.py index 2548d60..45c77de 100644 --- a/household_contact_tracing/behaviours/infection/new_infection.py +++ b/household_contact_tracing/behaviours/infection/new_infection.py @@ -224,20 +224,8 @@ def new_infection(self, time: int, household: Household, infecting_node: Optiona time_started_lfa_testing = float('Inf') additional_attributes = { - 'being_lateral_flow_tested': node_being_lateral_flow_tested, - 'time_started_lfa_testing': time_started_lfa_testing, 'received_positive_test_result': False, 'received_result': None, - 'avenue_of_testing': None, - 'positive_test_time': None, - 'node_will_take_up_lfa_testing': node_will_take_up_lfa_testing, - 'confirmatory_PCR_result_was_positive': None, - 'taken_confirmatory_PCR_test': False, - 'confirmatory_PCR_test_time': None, - 'confirmatory_PCR_test_result_time': None, - 'propensity_risky_behaviour_lfa_testing': - self.will_engage_in_risky_behaviour_while_being_lfa_tested(), - 'propensity_to_miss_lfa_tests': self.propensity_to_miss_lfa_tests() } asymptomatic = self.is_asymptomatic_infection() @@ -277,17 +265,35 @@ def new_infection(self, time: int, household: Household, infecting_node: Optiona else: node_is_isolated = False + lfd_testing_adherence_attributes = { + 'node_will_take_up_lfa_testing': node_will_take_up_lfa_testing, + 'confirmatory_PCR_result_was_positive': None, + } + tracing_adherence_attributes = { 'will_uptake_isolation': isolation_uptake, 'propensity_imperfect_isolation': self.get_propensity_imperfect_isolation() } returning_travellers_attributes = {'pseudo_symptom_onset_time': pseudo_symptom_onset_time} + lfd_testing_attributes = { + 'avenue_of_testing': None, + 'being_lateral_flow_tested': node_being_lateral_flow_tested, + 'time_started_lfa_testing': time_started_lfa_testing, + 'positive_test_time': None, + 'taken_confirmatory_PCR_test': False, + 'confirmatory_PCR_test_time': None, + 'confirmatory_PCR_test_result_time': None, + 'propensity_risky_behaviour_lfa_testing': self.will_engage_in_risky_behaviour_while_being_lfa_tested(), + 'propensity_to_miss_lfa_tests': self.propensity_to_miss_lfa_tests() + } new_node = self.network.add_node(time_infected=time, household_id=household.id, isolated=node_is_isolated, tracing_adherence_attributes=tracing_adherence_attributes, returning_travellers_attributes=returning_travellers_attributes, + lfd_testing_attributes=lfd_testing_attributes, + lfd_testing_adherence_attributes=lfd_testing_adherence_attributes, asymptomatic=asymptomatic, contact_traced=household.contact_traced, symptom_onset_time=symptom_onset_time, diff --git a/household_contact_tracing/network.py b/household_contact_tracing/network.py index c7f545e..a70ee81 100644 --- a/household_contact_tracing/network.py +++ b/household_contact_tracing/network.py @@ -290,14 +290,15 @@ def __init__(self, node_id: int, household: Household, **attributes): self.received_result = False self.received_positive_test_result = False + # Update instance variables with anything in `additional_attributes` + self.update_params(attributes) + # Update node attribute classes self.lfd_testing_adherence = LFDTestingAdherenceAttributes(attributes['lfd_testing_adherence_attributes']) self.tracing_adherence = TracingAdherenceAttributes(attributes['tracing_adherence_attributes']) self.returning_travellers = ReturningTravellerAttributes(attributes['returning_travellers_attributes']) self.lfd_testing = LFDTestingAttributes(attributes['lfd_testing_attributes']) - # Update instance variables with anything in `additional_attributes` - self.update_params(attributes) self.update_params(attributes['additional_attributes']) def time_relative_to_symptom_onset(self, time: int) -> int: @@ -548,7 +549,7 @@ def start_lateral_flow_testing_household(self, time: int): for node in self.nodes: if node.lfd_testing_adherence.node_will_take_up_lfa_testing: if not node.received_positive_test_result: - if not node.being_lateral_flow_tested: + if not node.lfd_testing.being_lateral_flow_tested: node.lfd_testing.being_lateral_flow_tested = True node.lfd_testing.time_started_lfa_testing = time From 2b9ab647b71e339b399643c5034c5bf5c0fa6829 Mon Sep 17 00:00:00 2001 From: Martyn Date: Mon, 2 Aug 2021 11:07:14 +0100 Subject: [PATCH 34/70] Minor variable name changes --- .../views/resource_demand_views.py | 22 +++++++++++++++++-- .../queueing_processes/update_fixtures.ipynb | 8 +++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/household_contact_tracing/views/resource_demand_views.py b/household_contact_tracing/views/resource_demand_views.py index 584ce67..0005492 100644 --- a/household_contact_tracing/views/resource_demand_views.py +++ b/household_contact_tracing/views/resource_demand_views.py @@ -7,6 +7,9 @@ from household_contact_tracing.branching_process_models import BranchingProcessModel class PositiveTestRecord(BranchingProcessView): + """This view records the number of individuals at each time step who have tested postivie for + for the first time during that timestep. + """ def __init__(self, model: BranchingProcessModel): @@ -75,5 +78,20 @@ def set_display(self, show: bool): """ - def print_my_name(self): - print('science bitch') + def positive_tests_over_time(self): + """Returns a list of the number of positive tests received at each timestep. + """ + + positive_test_times = [ + node.positive_test_time + for node in self._model.network.all_nodes() + if node.positive_test_time is not None + ] + + return [ + positive_test_times.count(time) + for time in range(self._model.time) + ] + + def active_infections(self): + pass \ No newline at end of file diff --git a/test/fixtures/queueing_processes/update_fixtures.ipynb b/test/fixtures/queueing_processes/update_fixtures.ipynb index 33cd525..468eed5 100644 --- a/test/fixtures/queueing_processes/update_fixtures.ipynb +++ b/test/fixtures/queueing_processes/update_fixtures.ipynb @@ -309,7 +309,7 @@ "metadata": {}, "outputs": [], "source": [ - "def test_processing_delay_dist():\n", + "def processing_delay_dist():\n", " return 1\n", "\n", "def symptom_onset_delay_dist():\n", @@ -320,7 +320,7 @@ " demand = [10]*10,\n", " capacity = [10]*10,\n", " max_time_in_queue = 10,\n", - " test_processing_delay_dist = test_processing_delay_dist,\n", + " processing_delay_dist = processing_delay_dist,\n", " symptom_onset_delay_dist = symptom_onset_delay_dist\n", ")" ] @@ -411,7 +411,7 @@ "metadata": {}, "outputs": [], "source": [ - "def test_processing_delay_dist():\n", + "def processing_delay_dist():\n", " return 1\n", "\n", "def symptom_onset_delay_dist():\n", @@ -422,7 +422,7 @@ " demand = [5]*10,\n", " capacity = [10]*10,\n", " max_time_in_queue = 10,\n", - " test_processing_delay_dist = test_processing_delay_dist,\n", + " processing_delay_dist = processing_delay_dist,\n", " symptom_onset_delay_dist = symptom_onset_delay_dist\n", ")\n", "\n", From 35e1df6c7afded7659d8194c80ffd8a985e0ef70 Mon Sep 17 00:00:00 2001 From: Martyn Fyles Date: Mon, 2 Aug 2021 11:18:36 +0100 Subject: [PATCH 35/70] Removing unecessary view --- .../views/resource_demand_views.py | 97 ------------------- 1 file changed, 97 deletions(-) delete mode 100644 household_contact_tracing/views/resource_demand_views.py diff --git a/household_contact_tracing/views/resource_demand_views.py b/household_contact_tracing/views/resource_demand_views.py deleted file mode 100644 index 0005492..0000000 --- a/household_contact_tracing/views/resource_demand_views.py +++ /dev/null @@ -1,97 +0,0 @@ -''' -Contains several views that can be used to record the demand for various resources -at each time step of a simulation. Resources might include the total number of individuals -attempting to book a test at each timestep -''' -from household_contact_tracing.views.branching_process_view import BranchingProcessView -from household_contact_tracing.branching_process_models import BranchingProcessModel - -class PositiveTestRecord(BranchingProcessView): - """This view records the number of individuals at each time step who have tested postivie for - for the first time during that timestep. - """ - - def __init__(self, model: BranchingProcessModel): - - self._model = model - self.view_name = 'positive_test_record' - - self.positive_tests_requested = [] - - def model_state_change(self, subject: BranchingProcessModel): - """ - Respond to changes in model state (e.g. running, extinct, timed-out) - - Parameters: - subject (BranchingProcessModel): The branching process model being displayed by this simulation view. - - Returns: - None - """ - pass - - def model_step_increment(self, subject: BranchingProcessModel): - """ - Respond to single step increment in simulation - - Parameters: - subject (BranchingProcessModel): The branching process model being displayed by this simulation view. - - Returns: - None - """ - pass - - def model_simulation_stopped(self, subject: BranchingProcessModel): - """ - Respond to end of simulation run - - Parameters: - subject (BranchingProcessModel): The branching process model being displayed by this simulation view. - - Returns: - None - """ - pass - - def graph_change(self, subject: BranchingProcessModel): - """ - Respond to changes in graph (nodes/households network) - - Parameters: - subject (SimulationModel): The branching process model being displayed by this simulation view. - - Returns: - None - """ - pass - - def set_display(self, show: bool): - """ - Sets whether this view is displayed or not. - - Parameters: - show (bool): To display this view, set to True - - Returns: - None - """ - - - def positive_tests_over_time(self): - """Returns a list of the number of positive tests received at each timestep. - """ - - positive_test_times = [ - node.positive_test_time - for node in self._model.network.all_nodes() - if node.positive_test_time is not None - ] - - return [ - positive_test_times.count(time) - for time in range(self._model.time) - ] - - def active_infections(self): - pass \ No newline at end of file From d6d7496fef1a6bacae1d513401f2b1a6a29f0fbd Mon Sep 17 00:00:00 2001 From: Martyn Fyles Date: Mon, 2 Aug 2021 11:35:57 +0100 Subject: [PATCH 36/70] typo fix --- household_contact_tracing/queueing_processes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/household_contact_tracing/queueing_processes.py b/household_contact_tracing/queueing_processes.py index 9ec5fa7..e6b19fb 100644 --- a/household_contact_tracing/queueing_processes.py +++ b/household_contact_tracing/queueing_processes.py @@ -396,7 +396,7 @@ def __init__( self, days_to_simulate: int, demand: List[int], - demand_variant: list[int], + demand_variant: List[int], capacity: List[int], max_time_in_queue: int, processing_delay_dist: Callable, From 8bcd4a3631d70a37be001367e03ef92193fb97bf Mon Sep 17 00:00:00 2001 From: Martyn Fyles Date: Mon, 2 Aug 2021 12:12:21 +0100 Subject: [PATCH 37/70] Test fixes --- ...rministicQueue_add_new_test_seekers.pickle | Bin 1266 -> 1581 bytes .../Queue_new_applicants_fixture.pickle | Bin 1411 -> 1445 bytes .../Queue_swab_applicants.pickle | Bin 1409 -> 1443 bytes .../empty_applicant_df.pickle | Bin 1629 -> 1637 bytes .../queueing_processes/empty_queue_df.pickle | Bin 1936 -> 1970 bytes .../queueing_processes/update_fixtures.ipynb | 1558 +++++++++++++++-- test/test_queueing_processes.py | 51 +- 7 files changed, 1401 insertions(+), 208 deletions(-) diff --git a/test/fixtures/queueing_processes/DeterministicQueue_add_new_test_seekers.pickle b/test/fixtures/queueing_processes/DeterministicQueue_add_new_test_seekers.pickle index 1971f3f3c2f2879a6a7bdd0a29051507c7d20c8a..6cb5c5bb91737edb742b15864c0cc82e91da67f9 100644 GIT binary patch literal 1581 zcmb7ETW`}a6mGhZ+b^JRfR;|Osi|fr8l>bzdTM1;4=u< zrvlwINDMvlIz|i$?4qg;ZRt&P7p?8|#1 zwSQo8D2D+SC(V*J2mxp>^=K%=xJzV0d?1CHWey!lKIM=Di4$3zx+=p6i0DVDOd^7z zgVbu?f*+DFhG0O`>axWOn=V)Y^M!C5mt`QZ5Lg-u_d?DI2f7VMeB9PomDqH&mG5Ed zY)-9A>q183u4%)j`4&pg)>^76B+VpNP*sFXQtdU20cbdOPr$mc1-4{L%vOd4cD%-( zX4h@eN}4@A`IyiqS>$n)Y2%OS^LbugY4SPM&9vC2VbeD% zBkr-afy7bNB+VMC<{J$Y8qYSCQ`NR9&&|wZRYwsd zIEg}6_wx{CC`Pj?c-S^df(OI#9CHhD(}$nzCHuvGBQ`UJdKR7WDr4w7i&nha82a9# zHEeou-z|&Qz1braa$#>fK}kc2GLkveauR{0RztTiQYs~?v`L(FIPF?^NEwSfo1A*mWiyt8M!7iGTNB47ndg{C8efJ=@BhY%q+>wOOG$fk55XChl`8l zq^6a`7nY`$rpD*xmp~*YS25~x^OR)fLKRNl!f3?b!&_XLTTqgp8=s$7oLVyZDWfx^ z!DLM)yL!1ErZkHwogM8{f~IJAGxspsO!4#c^ZE}2V8WZBWJ*$}a{vPbI8ck80yH*5 zB%@y3o1t||h9ty#MsH4UuC^%|ik-1Q$8cf7PB`d*gC2${8NTE~8)7wj^I*cs7npU} z^%6loF_?UTMNyFx=oPTrGgLFwGSo9PGBh)^GPE;vGIUcRsu)?#8SN)qvMP!Led^5! Y@^)$M6pf6`46lr=j9hPq$tPJ;00y_99smFU diff --git a/test/fixtures/queueing_processes/Queue_new_applicants_fixture.pickle b/test/fixtures/queueing_processes/Queue_new_applicants_fixture.pickle index 835038b31260ee1262dcd3a46f411e7d127d36ba..a1421a281f9774d059f96cb826b9528652546f70 100644 GIT binary patch delta 371 zcmZqXUdqkVz&dr-M3z5{oRb+DZEQIUit>|Fi;GiJru2xHCuWvp=B3A%CWie;8o4k}oKY3Axd&c67W!{Y5%-)Rc xQ-Y?H)=tsLSd`%l7G?HkK@klBi?RS+kE}KdEXwB1fg+j!7Ul5foUF*21ORZkfEfS) delta 434 zcmZ3=-OSC>z%sRBBFi5}uE~syHsM9UL1OEUA)<4f}6lTzd1;$k_e zX(jQ6rKzQ<@p<_r5Q)iEjJn)BC7HQUg_E~18u9n=7FXsLl;r2e=jRotmP~%i=*(y^ zS(B+pi`ARCZAyk>XY3S>9)>9yfw>)IR1BB*qm;#he1=>2fky(231!him)kL63 zhQZ_q%-Wo48S1Gpo}wgcE7XM<8X1}yS{d3IItW>?UR}6eMKE6v%CBTGXSAQZkwxEk zW`;+`tc@R7G(x{3t2Q2EDH20ifA-gl+&AQvNLNE0ALxG A!vFvP diff --git a/test/fixtures/queueing_processes/Queue_swab_applicants.pickle b/test/fixtures/queueing_processes/Queue_swab_applicants.pickle index cdf3446460afbe885ed2a03b166aa3346c042ed5..98d7d8c53b18f70fcd648b4b785b586946b91149 100644 GIT binary patch delta 372 zcmZqVUd+wXz&drtM3z5{oRb+DZEQIUit>|Fi;GiJru2xHCuWvp=B3A%ACic6h7OVlNRKX5Pd1A=quu1iEc(fd zGTbv3XDst(^k(*EY@ZS|rL=a6M#iEHU$7{%Hw%hr2w0Q_=mBK4QD9LvZw?gE1h6QF KH|Jz|)+7MXWq?}% delta 426 zcmZ3?-N?<-z%sRLBFi5}uE~syHsM9UL1OEUA)<4f}6lTzd1;$k_e zX(jQ6rKzQ<@p<_r5Q)iEjJn)BC7HQUg_E~18u9n=7FXsLl;r2e=jRotmP~%i=*(y^ zS(B+ph1Hw6ZAyk>XY3S>9)>BMof(11(3`=VAtP|IG_x8jyElh7=j8cJG9Ge0OlcNV zIy>5@1WnQKX6|9Mnd0Z?=k*^5z=StL$&{o{X9)%{_+SsA85|%qhr<-0bSluk$@R?A zlg}}8va2QnMKTN~-(%M1RLf9Lh4B<6SzDoQ%h1Ts%+Siv&d@=~g7i)unwnI)Nd>G37`@ky!iNHP*Rsc9wg gg{7&bsquOFB`~SUC5*bfd?lH=aK)R~Fm7Z406Cx~@Bjb+ delta 90 zcmaFLbC-vufn{pYMixs(bN1r$#H6Iulqo%;<%yXknR)5)CHe75sqt`ev7FSjlK8^X e)Y8=Wy!;Y~#N;YQU2dL|%v`9#&082ZvH$@30wN^< diff --git a/test/fixtures/queueing_processes/empty_queue_df.pickle b/test/fixtures/queueing_processes/empty_queue_df.pickle index 7c3e80a198c8f7332d9b7cb7a9672a1f3573edfd..13daf6e2e88c59ad32fb786287423b1bbffdca8f 100644 GIT binary patch delta 430 zcmbQhzloovfpzNgjVxOk`Ni@|bCXhw;tPuMlT(X}Cm&;+sNW+56fCIJOU^G!)dMn9 zi!zg^^l-3c5e=dC=b-S0+8@z HE%r13t2vu( delta 392 zcmdnQKY^d6fn{pzMwYFNydrs}xk;%-@x|qdNlBAWFisT95XlhtW@w#~A=y49Xo`k6 zqc@W`SKDL_W_9LH=LM5Hm=;c+z^owJ!!RYo2OUoCXI2u%5Y6xzDDll_S&lF=+D)Fs zreA+3!!zS>#xbC8n7tW6UMsDgqLFbZBLFPQ?9GBA8U_|+@n%I4jRA|Ydb6R3CV@rS ayxCDiGr*$k-W(879;kJBAmPbA>}dd`GL5DH diff --git a/test/fixtures/queueing_processes/update_fixtures.ipynb b/test/fixtures/queueing_processes/update_fixtures.ipynb index 468eed5..115140e 100644 --- a/test/fixtures/queueing_processes/update_fixtures.ipynb +++ b/test/fixtures/queueing_processes/update_fixtures.ipynb @@ -10,15 +10,15 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.10" + "version": "3.9.4" }, "orig_nbformat": 4, "kernelspec": { "name": "python3", - "display_name": "Python 3.7.10 64-bit (conda)" + "display_name": "Python 3.9.4 64-bit (conda)" }, "interpreter": { - "hash": "7492ded05b103f4219da6ddb5c402bdc2a09c917dea74c75989a286e893175e7" + "hash": "5e088ab515b9ff01afc6092114ac7786286eca1d23e3587660ba5a8d6e06cf28" } }, "nbformat": 4, @@ -26,48 +26,51 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], + "execution_count": 5, "source": [ - "from household_contact_tracing.queueing_processes import Queue, DeterministicQueue\n", + "from household_contact_tracing.queueing_processes import Queue, DeterministicQueue\r\n", + "import pandas as pd\r\n", "import pickle" - ] + ], + "outputs": [], + "metadata": {} }, { + "cell_type": "markdown", "source": [ "This notebook is used to create the fixtures for test_queueing_processes.py." ], - "cell_type": "markdown", "metadata": {} }, { + "cell_type": "markdown", "source": [ "## Queue" ], - "cell_type": "markdown", "metadata": {} }, { "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], + "execution_count": 17, "source": [ "my_queue = Queue(days_to_simulate=10, capacity=[10]*10)" - ] + ], + "outputs": [], + "metadata": {} }, { + "cell_type": "markdown", "source": [ "### Initialised applicant df" ], - "cell_type": "markdown", "metadata": {} }, { "cell_type": "code", - "execution_count": 3, - "metadata": {}, + "execution_count": 18, + "source": [ + "my_queue.applicant_df" + ], "outputs": [ { "output_type": "execute_result", @@ -77,36 +80,127 @@ "Columns: [id, processed, waiting_to_be_processed, left_queue_not_processed, time_symptom_onset, time_joined_queue, time_processed, time_received_result, time_will_leave_queue]\n", "Index: []" ], - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
idprocessedwaiting_to_be_processedleft_queue_not_processedtime_symptom_onsettime_joined_queuetime_processedtime_received_resulttime_will_leave_queue
\n
" + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idprocessedwaiting_to_be_processedleft_queue_not_processedtime_symptom_onsettime_joined_queuetime_processedtime_received_resulttime_will_leave_queue
\n", + "
" + ] }, "metadata": {}, - "execution_count": 3 + "execution_count": 18 } ], - "source": [ - "my_queue.applicant_df" - ] + "metadata": {} }, { "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], + "execution_count": 19, "source": [ "#my_queue.applicant_df.to_pickle('./test/fixtures/queueing_processes/empty_applicant_df.pickle')" - ] + ], + "outputs": [], + "metadata": {} }, { + "cell_type": "code", + "execution_count": 20, "source": [ - "### Initialised queue_df" + "pd.read_pickle('./test/fixtures/queueing_processes/empty_applicant_df.pickle')" + ], + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "Empty DataFrame\n", + "Columns: [id, processed, waiting_to_be_processed, left_queue_not_processed, time_symptom_onset, time_joined_queue, time_processed, time_received_result, time_will_leave_queue]\n", + "Index: []" + ], + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idprocessedwaiting_to_be_processedleft_queue_not_processedtime_symptom_onsettime_joined_queuetime_processedtime_received_resulttime_will_leave_queue
\n", + "
" + ] + }, + "metadata": {}, + "execution_count": 20 + } ], + "metadata": {} + }, + { "cell_type": "markdown", + "source": [ + "### Initialised queue_df" + ], "metadata": {} }, { "cell_type": "code", - "execution_count": 5, - "metadata": {}, + "execution_count": 21, + "source": [ + "my_queue.queue_df" + ], "outputs": [ { "output_type": "execute_result", @@ -137,67 +231,538 @@ "9 \n", "\n", " number_processed_today number_left_queue_not_tested \n", - "0 \n", - "1 \n", - "2 \n", - "3 \n", - "4 \n", - "5 \n", - "6 \n", - "7 \n", - "8 \n", - "9 " + "0 \n", + "1 \n", + "2 \n", + "3 \n", + "4 \n", + "5 \n", + "6 \n", + "7 \n", + "8 \n", + "9 " ], - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
timecapacitynew_applicantsspillover_to_next_daytotal_applications_todaycapacity_exceededcapacity_exceeded_bynumber_processed_todaynumber_left_queue_not_tested
0010
1110
2210
3310
4410
5510
6610
7710
8810
9910
\n
" + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
timecapacitynew_applicantsspillover_to_next_daytotal_applications_todaycapacity_exceededcapacity_exceeded_bynumber_processed_todaynumber_left_queue_not_tested
0010
1110
2210
3310
4410
5510
6610
7710
8810
9910
\n", + "
" + ] }, "metadata": {}, - "execution_count": 5 + "execution_count": 21 } ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 22, "source": [ - "my_queue.queue_df" - ] + "pd.read_pickle('./test/fixtures/queueing_processes/empty_queue_df.pickle')" + ], + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " time capacity new_applicants spillover_to_next_day \\\n", + "0 0 10 \n", + "1 1 10 \n", + "2 2 10 \n", + "3 3 10 \n", + "4 4 10 \n", + "5 5 10 \n", + "6 6 10 \n", + "7 7 10 \n", + "8 8 10 \n", + "9 9 10 \n", + "\n", + " total_applications_today capacity_exceeded capacity_exceeded_by \\\n", + "0 \n", + "1 \n", + "2 \n", + "3 \n", + "4 \n", + "5 \n", + "6 \n", + "7 \n", + "8 \n", + "9 \n", + "\n", + " number_processed_today number_left_queue_not_tested \n", + "0 \n", + "1 \n", + "2 \n", + "3 \n", + "4 \n", + "5 \n", + "6 \n", + "7 \n", + "8 \n", + "9 " + ], + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
timecapacitynew_applicantsspillover_to_next_daytotal_applications_todaycapacity_exceededcapacity_exceeded_bynumber_processed_todaynumber_left_queue_not_tested
0010
1110
2210
3310
4410
5510
6610
7710
8810
9910
\n", + "
" + ] + }, + "metadata": {}, + "execution_count": 22 + } + ], + "metadata": {} }, { "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], + "execution_count": 23, "source": [ "#my_queue.queue_df.to_pickle('./test/fixtures/queueing_processes/empty_queue_df.pickle')" - ] + ], + "outputs": [], + "metadata": {} }, { + "cell_type": "markdown", "source": [ "### add_new_applicants" ], - "cell_type": "markdown", "metadata": {} }, { "cell_type": "code", - "execution_count": 7, - "metadata": {}, + "execution_count": 24, + "source": [ + "my_queue.add_new_applicants(\r\n", + " ids = ['A', 'B', 'C'],\r\n", + " time = [1,2,3],\r\n", + " symptom_onset_times = [6, 6, 6],\r\n", + " queue_leaving_times=[11,12,13])" + ], "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 25, "source": [ - "my_queue.add_new_applicants(['A', 'B', 'C'], [1,2,3], 6)" - ] + "my_queue.applicant_df" + ], + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " id processed waiting_to_be_processed left_queue_not_processed \\\n", + "0 A False True \n", + "1 B False True \n", + "2 C False True \n", + "\n", + " time_symptom_onset time_joined_queue time_processed time_received_result \\\n", + "0 6 1 \n", + "1 6 2 \n", + "2 6 3 \n", + "\n", + " time_will_leave_queue \n", + "0 11 \n", + "1 12 \n", + "2 13 " + ], + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idprocessedwaiting_to_be_processedleft_queue_not_processedtime_symptom_onsettime_joined_queuetime_processedtime_received_resulttime_will_leave_queue
0AFalseTrue6111
1BFalseTrue6212
2CFalseTrue6313
\n", + "
" + ] + }, + "metadata": {}, + "execution_count": 25 + } + ], + "metadata": {} }, { "cell_type": "code", - "execution_count": 8, - "metadata": {}, + "execution_count": 26, + "source": [ + "pd.read_pickle('./test/fixtures/queueing_processes/Queue_new_applicants_fixture.pickle')" + ], "outputs": [ { "output_type": "execute_result", "data": { "text/plain": [ - " id processed waiting_to_be_processed left_queue_not_processed time_symptom_onset \\\n", + " id swabbed waiting_to_be_swabbed left_queue_not_swabbed time_symptom_onset \\\n", "0 A False True \n", "1 B False True \n", "2 C False True \n", "\n", - " time_joined_queue time_processed time_received_result time_will_leave_queue \\\n", + " time_joined_queue time_swabbed time_received_result time_will_leave_queue \\\n", "0 7 \n", "1 8 \n", "2 9 \n", @@ -207,58 +772,242 @@ "1 2.0 \n", "2 3.0 " ], - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
idprocessedwaiting_to_be_processedleft_queue_not_processedtime_symptom_onsettime_joined_queuetime_processedtime_received_resulttime_will_leave_queuesymptom_onset
0AFalseTrue71.0
1BFalseTrue82.0
2CFalseTrue93.0
\n
" + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idswabbedwaiting_to_be_swabbedleft_queue_not_swabbedtime_symptom_onsettime_joined_queuetime_swabbedtime_received_resulttime_will_leave_queuesymptom_onset
0AFalseTrue71.0
1BFalseTrue82.0
2CFalseTrue93.0
\n", + "
" + ] }, "metadata": {}, - "execution_count": 8 + "execution_count": 26 } ], - "source": [ - "my_queue.applicant_df" - ] + "metadata": {} }, { "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], + "execution_count": 27, "source": [ "# my_queue.applicant_df.to_pickle('./test/fixtures/queueing_processes/Queue_new_applicants_fixture.pickle')" - ] + ], + "outputs": [], + "metadata": {} }, { + "cell_type": "markdown", "source": [ "### swab_applicants" ], - "cell_type": "markdown", "metadata": {} }, { "cell_type": "code", - "execution_count": 21, - "metadata": {}, + "execution_count": 29, + "source": [ + "my_queue = Queue(days_to_simulate=10, capacity=[10]*10)\r\n", + "my_queue.add_new_applicants(\r\n", + " ids = ['A', 'B', 'C'],\r\n", + " time = [1,2,3],\r\n", + " symptom_onset_times = [6, 6, 6],\r\n", + " queue_leaving_times=[11,12,13])\r\n", + "my_queue.swab_applicants(\r\n", + " to_be_processed=[1, 2],\r\n", + " processing_delays=[1, 2])" + ], "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 30, "source": [ - "my_queue = Queue(days_to_simulate=10, capacity=[10]*10)\n", - "my_queue.add_new_applicants(['A', 'B', 'C'], [1,2,3], 6)\n", - "my_queue.swab_applicants([1, 2], [1,2])" - ] + "my_queue.applicant_df" + ], + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " id processed waiting_to_be_processed left_queue_not_processed \\\n", + "0 A False True \n", + "1 B True False False \n", + "2 C True False False \n", + "\n", + " time_symptom_onset time_joined_queue time_processed time_received_result \\\n", + "0 6 1 \n", + "1 6 2 0 1 \n", + "2 6 3 0 2 \n", + "\n", + " time_will_leave_queue \n", + "0 11 \n", + "1 12 \n", + "2 13 " + ], + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idprocessedwaiting_to_be_processedleft_queue_not_processedtime_symptom_onsettime_joined_queuetime_processedtime_received_resulttime_will_leave_queue
0AFalseTrue6111
1BTrueFalseFalse620112
2CTrueFalseFalse630213
\n", + "
" + ] + }, + "metadata": {}, + "execution_count": 30 + } + ], + "metadata": {} }, { "cell_type": "code", - "execution_count": 22, - "metadata": {}, + "execution_count": 31, + "source": [ + "pd.read_pickle('./test/fixtures/queueing_processes/Queue_swab_applicants.pickle')" + ], "outputs": [ { "output_type": "execute_result", "data": { "text/plain": [ - " id processed waiting_to_be_processed left_queue_not_processed time_symptom_onset \\\n", + " id swabbed waiting_to_be_swabbed left_queue_not_swabbed time_symptom_onset \\\n", "0 A False True \n", "1 B True False False \n", "2 C True False False \n", "\n", - " time_joined_queue time_processed time_received_result time_will_leave_queue \\\n", + " time_joined_queue time_swabbed time_received_result time_will_leave_queue \\\n", "0 7 \n", "1 0 1 8 \n", "2 0 2 9 \n", @@ -268,172 +1017,401 @@ "1 2.0 \n", "2 3.0 " ], - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
idprocessedwaiting_to_be_processedleft_queue_not_processedtime_symptom_onsettime_joined_queuetime_processedtime_received_resulttime_will_leave_queuesymptom_onset
0AFalseTrue71.0
1BTrueFalseFalse0182.0
2CTrueFalseFalse0293.0
\n
" + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idswabbedwaiting_to_be_swabbedleft_queue_not_swabbedtime_symptom_onsettime_joined_queuetime_swabbedtime_received_resulttime_will_leave_queuesymptom_onset
0AFalseTrue71.0
1BTrueFalseFalse0182.0
2CTrueFalseFalse0293.0
\n", + "
" + ] }, "metadata": {}, - "execution_count": 22 + "execution_count": 31 } ], - "source": [ - "my_queue.applicant_df" - ] + "metadata": {} }, { "cell_type": "code", - "execution_count": 23, - "metadata": {}, - "outputs": [], + "execution_count": 32, "source": [ "# my_queue.applicant_df.to_pickle('./test/fixtures/queueing_processes/Queue_swab_applicants.pickle')" - ] + ], + "outputs": [], + "metadata": {} }, { + "cell_type": "markdown", "source": [ "## Deterministic Queue" ], - "cell_type": "markdown", "metadata": {} }, { "cell_type": "code", "execution_count": 29, - "metadata": {}, - "outputs": [], "source": [ "### Add new test seekers" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], + "execution_count": 34, "source": [ - "def processing_delay_dist():\n", - " return 1\n", - "\n", - "def symptom_onset_delay_dist():\n", - " return 2 \n", - "\n", - "my_det_queue = DeterministicQueue(\n", - " days_to_simulate = 10,\n", - " demand = [10]*10,\n", - " capacity = [10]*10,\n", - " max_time_in_queue = 10,\n", - " processing_delay_dist = processing_delay_dist,\n", - " symptom_onset_delay_dist = symptom_onset_delay_dist\n", + "def processing_delay_dist():\r\n", + " return 1\r\n", + "\r\n", + "def symptom_onset_delay_dist():\r\n", + " return 2 \r\n", + "\r\n", + "my_det_queue = DeterministicQueue(\r\n", + " days_to_simulate = 10,\r\n", + " demand = [10]*10,\r\n", + " capacity = [10]*10,\r\n", + " max_time_in_queue = 10,\r\n", + " processing_delay_dist = processing_delay_dist,\r\n", + " symptom_onset_delay_dist = symptom_onset_delay_dist,\r\n", + " selection_method = 'uniform'\r\n", ")" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], + "execution_count": 35, "source": [ "my_det_queue.add_new_test_seekers()" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "code", - "execution_count": 8, - "metadata": {}, + "execution_count": 36, + "source": [ + "my_det_queue.queue.applicant_df" + ], "outputs": [ { "output_type": "execute_result", "data": { "text/plain": [ - " id processed waiting_to_be_processed left_queue_not_processed time_symptom_onset \\\n", - "0 10 False True \n", - "1 10 False True \n", - "2 10 False True \n", - "3 10 False True \n", - "4 10 False True \n", - "5 10 False True \n", - "6 10 False True \n", - "7 10 False True \n", - "8 10 False True \n", - "9 10 False True \n", + " id processed waiting_to_be_processed left_queue_not_processed \\\n", + "0 False True \n", + "1 False True \n", + "2 False True \n", + "3 False True \n", + "4 False True \n", + "5 False True \n", + "6 False True \n", + "7 False True \n", + "8 False True \n", + "9 False True \n", "\n", - " time_joined_queue time_processed time_received_result time_will_leave_queue \\\n", - "0 12 \n", - "1 12 \n", - "2 12 \n", - "3 12 \n", - "4 12 \n", - "5 12 \n", - "6 12 \n", - "7 12 \n", - "8 12 \n", - "9 12 \n", + " time_symptom_onset time_joined_queue time_processed time_received_result \\\n", + "0 -2 0 \n", + "1 -2 0 \n", + "2 -2 0 \n", + "3 -2 0 \n", + "4 -2 0 \n", + "5 -2 0 \n", + "6 -2 0 \n", + "7 -2 0 \n", + "8 -2 0 \n", + "9 -2 0 \n", "\n", - " symptom_onset \n", - "0 2.0 \n", - "1 2.0 \n", - "2 2.0 \n", - "3 2.0 \n", - "4 2.0 \n", - "5 2.0 \n", - "6 2.0 \n", - "7 2.0 \n", - "8 2.0 \n", - "9 2.0 " + " time_will_leave_queue \n", + "0 8 \n", + "1 8 \n", + "2 8 \n", + "3 8 \n", + "4 8 \n", + "5 8 \n", + "6 8 \n", + "7 8 \n", + "8 8 \n", + "9 8 " ], - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
idprocessedwaiting_to_be_processedleft_queue_not_processedtime_symptom_onsettime_joined_queuetime_processedtime_received_resulttime_will_leave_queuesymptom_onset
010FalseTrue122.0
110FalseTrue122.0
210FalseTrue122.0
310FalseTrue122.0
410FalseTrue122.0
510FalseTrue122.0
610FalseTrue122.0
710FalseTrue122.0
810FalseTrue122.0
910FalseTrue122.0
\n
" + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idprocessedwaiting_to_be_processedleft_queue_not_processedtime_symptom_onsettime_joined_queuetime_processedtime_received_resulttime_will_leave_queue
0FalseTrue-208
1FalseTrue-208
2FalseTrue-208
3FalseTrue-208
4FalseTrue-208
5FalseTrue-208
6FalseTrue-208
7FalseTrue-208
8FalseTrue-208
9FalseTrue-208
\n", + "
" + ] }, "metadata": {}, - "execution_count": 8 + "execution_count": 36 } ], - "source": [ - "my_det_queue.queue.applicant_df" - ] + "metadata": {} }, { "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], + "execution_count": 37, "source": [ "my_det_queue.queue.applicant_df.to_pickle('./test/fixtures/queueing_processes/DeterministicQueue_add_new_test_seekers.pickle')" - ] + ], + "outputs": [], + "metadata": {} }, { + "cell_type": "markdown", "source": [ "### test_process_queue_excess_capacity" ], - "cell_type": "markdown", "metadata": {} }, { "cell_type": "code", "execution_count": 10, - "metadata": {}, - "outputs": [], "source": [ - "def processing_delay_dist():\n", - " return 1\n", - "\n", - "def symptom_onset_delay_dist():\n", - " return 2 \n", - "\n", - "my_det_queue = DeterministicQueue(\n", - " days_to_simulate = 10,\n", - " demand = [5]*10,\n", - " capacity = [10]*10,\n", - " max_time_in_queue = 10,\n", - " processing_delay_dist = processing_delay_dist,\n", - " symptom_onset_delay_dist = symptom_onset_delay_dist\n", - ")\n", - "\n", - "my_det_queue.add_new_test_seekers()\n", - "\n" - ] + "def processing_delay_dist():\r\n", + " return 1\r\n", + "\r\n", + "def symptom_onset_delay_dist():\r\n", + " return 2 \r\n", + "\r\n", + "my_det_queue = DeterministicQueue(\r\n", + " days_to_simulate = 10,\r\n", + " demand = [5]*10,\r\n", + " capacity = [10]*10,\r\n", + " max_time_in_queue = 10,\r\n", + " processing_delay_dist = processing_delay_dist,\r\n", + " symptom_onset_delay_dist = symptom_onset_delay_dist\r\n", + ")\r\n", + "\r\n", + "my_det_queue.add_new_test_seekers()\r\n", + "\r\n" + ], + "outputs": [], + "metadata": {} }, { "cell_type": "code", "execution_count": 11, - "metadata": {}, + "source": [ + "my_det_queue.queue.applicant_df" + ], "outputs": [ { "output_type": "execute_result", @@ -460,29 +1438,129 @@ "3 2.0 \n", "4 2.0 " ], - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
idprocessedwaiting_to_be_processedleft_queue_not_processedtime_symptom_onsettime_joined_queuetime_processedtime_received_resulttime_will_leave_queuesymptom_onset
05FalseTrue122.0
15FalseTrue122.0
25FalseTrue122.0
35FalseTrue122.0
45FalseTrue122.0
\n
" + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idprocessedwaiting_to_be_processedleft_queue_not_processedtime_symptom_onsettime_joined_queuetime_processedtime_received_resulttime_will_leave_queuesymptom_onset
05FalseTrue122.0
15FalseTrue122.0
25FalseTrue122.0
35FalseTrue122.0
45FalseTrue122.0
\n", + "
" + ] }, "metadata": {}, "execution_count": 11 } ], - "source": [ - "my_det_queue.queue.applicant_df" - ] + "metadata": {} }, { "cell_type": "code", "execution_count": 12, - "metadata": {}, - "outputs": [], "source": [ "my_det_queue.process_queue()" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "code", "execution_count": 13, - "metadata": {}, + "source": [ + "my_det_queue.queue.applicant_df" + ], "outputs": [ { "output_type": "execute_result", @@ -509,22 +1587,120 @@ "3 2.0 \n", "4 2.0 " ], - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
idprocessedwaiting_to_be_processedleft_queue_not_processedtime_symptom_onsettime_joined_queuetime_processedtime_received_resulttime_will_leave_queuesymptom_onset
05TrueFalseFalse01122.0
15TrueFalseFalse01122.0
25TrueFalseFalse01122.0
35TrueFalseFalse01122.0
45TrueFalseFalse01122.0
\n
" + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idprocessedwaiting_to_be_processedleft_queue_not_processedtime_symptom_onsettime_joined_queuetime_processedtime_received_resulttime_will_leave_queuesymptom_onset
05TrueFalseFalse01122.0
15TrueFalseFalse01122.0
25TrueFalseFalse01122.0
35TrueFalseFalse01122.0
45TrueFalseFalse01122.0
\n", + "
" + ] }, "metadata": {}, "execution_count": 13 } ], - "source": [ - "my_det_queue.queue.applicant_df" - ] + "metadata": {} }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "source": [], "outputs": [], - "source": [] + "metadata": {} } ] -} +} \ No newline at end of file diff --git a/test/test_queueing_processes.py b/test/test_queueing_processes.py index 62ef12f..34aacea 100644 --- a/test/test_queueing_processes.py +++ b/test/test_queueing_processes.py @@ -24,8 +24,8 @@ def empty_applicant_df_fixture(): return pd.read_pickle('./test/fixtures/queueing_processes/empty_applicant_df.pickle') -def test_Queue_init_applicant_df(simple_queue, empty_applicant_df_fixture): - return pd.testing.assert_frame_equal(empty_applicant_df_fixture, simple_queue.applicant_df) +def test_Queue_init_applicant_df(simple_Queue, empty_applicant_df_fixture): + return pd.testing.assert_frame_equal(empty_applicant_df_fixture, simple_Queue.applicant_df) @pytest.fixture @@ -36,8 +36,8 @@ def empty_queue_df_fixture(): return pd.read_pickle('./test/fixtures/queueing_processes/empty_queue_df.pickle') -def test_Queue_init_queue_df(simple_queue, empty_queue_df_fixture): - return pd.testing.assert_frame_equal(empty_queue_df_fixture, simple_queue.queue_df) +def test_Queue_init_queue_df(simple_Queue, empty_queue_df_fixture): + return pd.testing.assert_frame_equal(empty_queue_df_fixture, simple_Queue.queue_df) @pytest.fixture @@ -48,10 +48,14 @@ def Queue_new_applicants_fixture(): return pd.read_pickle('./test/fixtures/queueing_processes/Queue_new_applicants_fixture.pickle') -def test_Queue_new_applicants(simple_queue, Queue_new_applicants_fixture): - simple_queue.add_new_applicants(['A', 'B', 'C'], [1,2,3], 6) +def test_Queue_new_applicants(simple_Queue, Queue_new_applicants_fixture): + simple_Queue.add_new_applicants( + ids = ['A', 'B', 'C'], + time = [1,2,3], + symptom_onset_times = [6, 6, 6], + queue_leaving_times=[11,12,13]) - return pd.testing.assert_frame_equal(Queue_new_applicants_fixture, simple_queue.applicant_df) + return pd.testing.assert_frame_equal(Queue_new_applicants_fixture, simple_Queue.applicant_df) @pytest.fixture @@ -60,23 +64,35 @@ def Queue_swab_applicants_fixture(): """ return pd.read_pickle('./test/fixtures/queueing_processes/Queue_swab_applicants.pickle') -def test_Queue_swab_applicants(simple_queue, Queue_swab_applicants_fixture): +def test_Queue_swab_applicants(simple_Queue, Queue_swab_applicants_fixture): """Adds some applicants, processes some of the and checks the applicant df """ - simple_queue.add_new_applicants(['A', 'B', 'C'], [1,2,3], 6) - simple_queue.swab_applicants([1, 2], [1,2]) + simple_Queue.add_new_applicants( + ids = ['A', 'B', 'C'], + time = [1,2,3], + symptom_onset_times = [6, 6, 6], + queue_leaving_times=[11,12,13]) + simple_Queue.swab_applicants( + to_be_processed=[1, 2], + processing_delays=[1, 2]) - return pd.testing.assert_frame_equal(Queue_swab_applicants_fixture, simple_queue.applicant_df) + return pd.testing.assert_frame_equal(Queue_swab_applicants_fixture, simple_Queue.applicant_df) -def test_Queue_current_applicants(simple_queue): +def test_Queue_current_applicants(simple_Queue): """Checks that the waiting to be processed indexes are returned. Add 3 people to the queue, process 2 """ - simple_queue.add_new_applicants(['A', 'B', 'C'], [1,2,3], 6) - simple_queue.swab_applicants([1, 2], [1,2]) - assert simple_queue.current_applicants == [0] + simple_Queue.add_new_applicants( + ids = ['A', 'B', 'C'], + time = [1,2,3], + symptom_onset_times = [6, 6, 6], + queue_leaving_times=[11,12,13]) + simple_Queue.swab_applicants( + to_be_processed=[1, 2], + processing_delays=[1, 2]) + assert simple_Queue.current_applicants == [0] def test_Queue_todays_capacity(): @@ -110,8 +126,9 @@ def symptom_onset_delay_dist(): demand = [10]*10, capacity = [10]*10, max_time_in_queue = 10, - processing_delay_dist = processing_delay_dist, - symptom_onset_delay_dist = symptom_onset_delay_dist + processing_delay_dist = processing_delay_dist, + symptom_onset_delay_dist = symptom_onset_delay_dist, + selection_method = 'uniform' ) my_det_queue.add_new_test_seekers() From 9b24e3ddbd0ecd299d2b91c104d4e5b2dd98b435 Mon Sep 17 00:00:00 2001 From: Martyn Fyles Date: Mon, 2 Aug 2021 12:37:38 +0100 Subject: [PATCH 38/70] backwards comptability fixes --- ...rministicQueue_add_new_test_seekers.pickle | Bin 1581 -> 1581 bytes .../Queue_new_applicants_fixture.pickle | Bin 1445 -> 1445 bytes .../Queue_swab_applicants.pickle | Bin 1443 -> 1443 bytes .../empty_applicant_df.pickle | Bin 1637 -> 1637 bytes .../queueing_processes/empty_queue_df.pickle | Bin 1970 -> 1938 bytes test/test_queueing_processes.py | 1 - 6 files changed, 1 deletion(-) diff --git a/test/fixtures/queueing_processes/DeterministicQueue_add_new_test_seekers.pickle b/test/fixtures/queueing_processes/DeterministicQueue_add_new_test_seekers.pickle index 6cb5c5bb91737edb742b15864c0cc82e91da67f9..77307c38e702c5bfabf23fbc2992d84499801ce8 100644 GIT binary patch delta 11 ScmZ3>vzCXcfn_6;HX8sGuL9iw delta 11 ScmZ3>vzCXcfpsI3HX8sGv;y7$ diff --git a/test/fixtures/queueing_processes/Queue_new_applicants_fixture.pickle b/test/fixtures/queueing_processes/Queue_new_applicants_fixture.pickle index a1421a281f9774d059f96cb826b9528652546f70..83dff415306dbdcd2260bee1899d044270303db6 100644 GIT binary patch delta 11 ScmZ3=y_B1&fn_7pVpaeY>;pmo delta 11 ScmZ3=y_B1&fpsI(VpaeY@dHBu diff --git a/test/fixtures/queueing_processes/Queue_swab_applicants.pickle b/test/fixtures/queueing_processes/Queue_swab_applicants.pickle index 98d7d8c53b18f70fcd648b4b785b586946b91149..c58c3fe3aed51b571375f35b804d711ccfd56bf1 100644 GIT binary patch delta 11 ScmZ3?y_lP+fn_7pLRJ72zymx0 delta 11 ScmZ3?y_lP+fpsI(LRJ72#REM6 diff --git a/test/fixtures/queueing_processes/empty_applicant_df.pickle b/test/fixtures/queueing_processes/empty_applicant_df.pickle index 3476086f06d657b5b56d834de7d4b77506d85518..a25a3f0d0f0b86da114f65bbfdaa78cb549a226d 100644 GIT binary patch delta 11 ScmaFL^OT3Ffn_68G8+IHe*=X8 delta 11 ScmaFL^OT3FfpsHOG8+IHgad{E diff --git a/test/fixtures/queueing_processes/empty_queue_df.pickle b/test/fixtures/queueing_processes/empty_queue_df.pickle index 13daf6e2e88c59ad32fb786287423b1bbffdca8f..0dfae63c375907c84b1a1fe69fb588cc113c0738 100644 GIT binary patch delta 374 zcmdnQKZ&2Efn{p@MwU<}u?&$6ac_p!DH)ROQ-Y>wcr$u4d2_W*)?ij=?sQ%-xr1rp zpMetJe3s=1Bct8qNo@M{hcY}f4rd$#`i9w?5#+Vf z+9?_thcW`dqRiebD57CtQ5J7j6ww&4D62Ocif9s8l+BwRMKl8}%I?hp5#@nemj@D_ I?8BZ00I<4-MgRZ+ delta 406 zcmbQlzloovfpzNgjVz%|`aMEAE zyq4t%BcsRUA~yZ{BN^TqM>9?Uy}|6w*bekUY3&q^j3XIAU{Pjo78KD4uqcZ+D~f0w lSd`V94Mj8sEXwB1jv|@`7G?M5fQa%ytt$WtPu5{i0|0oIl63$8 diff --git a/test/test_queueing_processes.py b/test/test_queueing_processes.py index 34aacea..44d8f4d 100644 --- a/test/test_queueing_processes.py +++ b/test/test_queueing_processes.py @@ -2,7 +2,6 @@ import pytest from household_contact_tracing.queueing_processes import Queue, DeterministicQueue - # Testing the Queue object @pytest.fixture From a07ddf0d2c177f4413a486a457e65886a4085b84 Mon Sep 17 00:00:00 2001 From: Martyn Fyles Date: Mon, 2 Aug 2021 12:37:58 +0100 Subject: [PATCH 39/70] backwards compatibility fixes --- .../queueing_processes/update_fixtures.ipynb | 196 +++++++++--------- 1 file changed, 93 insertions(+), 103 deletions(-) diff --git a/test/fixtures/queueing_processes/update_fixtures.ipynb b/test/fixtures/queueing_processes/update_fixtures.ipynb index 115140e..049c738 100644 --- a/test/fixtures/queueing_processes/update_fixtures.ipynb +++ b/test/fixtures/queueing_processes/update_fixtures.ipynb @@ -26,7 +26,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 5, + "execution_count": 8, "source": [ "from household_contact_tracing.queueing_processes import Queue, DeterministicQueue\r\n", "import pandas as pd\r\n", @@ -38,7 +38,7 @@ { "cell_type": "markdown", "source": [ - "This notebook is used to create the fixtures for test_queueing_processes.py." + "This notebook is used to create the fixtures for test_queueing_processes.py. We save fixtures using pickle protocol 4 for backwards compatibility." ], "metadata": {} }, @@ -51,7 +51,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 9, "source": [ "my_queue = Queue(days_to_simulate=10, capacity=[10]*10)" ], @@ -67,7 +67,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 10, "source": [ "my_queue.applicant_df" ], @@ -117,23 +117,14 @@ ] }, "metadata": {}, - "execution_count": 18 + "execution_count": 10 } ], "metadata": {} }, { "cell_type": "code", - "execution_count": 19, - "source": [ - "#my_queue.applicant_df.to_pickle('./test/fixtures/queueing_processes/empty_applicant_df.pickle')" - ], - "outputs": [], - "metadata": {} - }, - { - "cell_type": "code", - "execution_count": 20, + "execution_count": 11, "source": [ "pd.read_pickle('./test/fixtures/queueing_processes/empty_applicant_df.pickle')" ], @@ -183,11 +174,20 @@ ] }, "metadata": {}, - "execution_count": 20 + "execution_count": 11 } ], "metadata": {} }, + { + "cell_type": "code", + "execution_count": 12, + "source": [ + "my_queue.applicant_df.to_pickle('./test/fixtures/queueing_processes/empty_applicant_df.pickle', protocol=4)" + ], + "outputs": [], + "metadata": {} + }, { "cell_type": "markdown", "source": [ @@ -197,7 +197,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 13, "source": [ "my_queue.queue_df" ], @@ -399,14 +399,14 @@ ] }, "metadata": {}, - "execution_count": 21 + "execution_count": 13 } ], "metadata": {} }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 14, "source": [ "pd.read_pickle('./test/fixtures/queueing_processes/empty_queue_df.pickle')" ], @@ -608,16 +608,16 @@ ] }, "metadata": {}, - "execution_count": 22 + "execution_count": 14 } ], "metadata": {} }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 15, "source": [ - "#my_queue.queue_df.to_pickle('./test/fixtures/queueing_processes/empty_queue_df.pickle')" + "my_queue.queue_df.to_pickle('./test/fixtures/queueing_processes/empty_queue_df.pickle', protocol=4)" ], "outputs": [], "metadata": {} @@ -631,7 +631,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 16, "source": [ "my_queue.add_new_applicants(\r\n", " ids = ['A', 'B', 'C'],\r\n", @@ -644,7 +644,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 17, "source": [ "my_queue.applicant_df" ], @@ -741,14 +741,14 @@ ] }, "metadata": {}, - "execution_count": 25 + "execution_count": 17 } ], "metadata": {} }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 18, "source": [ "pd.read_pickle('./test/fixtures/queueing_processes/Queue_new_applicants_fixture.pickle')" ], @@ -757,20 +757,20 @@ "output_type": "execute_result", "data": { "text/plain": [ - " id swabbed waiting_to_be_swabbed left_queue_not_swabbed time_symptom_onset \\\n", - "0 A False True \n", - "1 B False True \n", - "2 C False True \n", + " id processed waiting_to_be_processed left_queue_not_processed \\\n", + "0 A False True \n", + "1 B False True \n", + "2 C False True \n", "\n", - " time_joined_queue time_swabbed time_received_result time_will_leave_queue \\\n", - "0 7 \n", - "1 8 \n", - "2 9 \n", + " time_symptom_onset time_joined_queue time_processed time_received_result \\\n", + "0 6 1 \n", + "1 6 2 \n", + "2 6 3 \n", "\n", - " symptom_onset \n", - "0 1.0 \n", - "1 2.0 \n", - "2 3.0 " + " time_will_leave_queue \n", + "0 11 \n", + "1 12 \n", + "2 13 " ], "text/html": [ "
\n", @@ -792,15 +792,14 @@ " \n", " \n", " id\n", - " swabbed\n", - " waiting_to_be_swabbed\n", - " left_queue_not_swabbed\n", + " processed\n", + " waiting_to_be_processed\n", + " left_queue_not_processed\n", " time_symptom_onset\n", " time_joined_queue\n", - " time_swabbed\n", + " time_processed\n", " time_received_result\n", " time_will_leave_queue\n", - " symptom_onset\n", " \n", " \n", " \n", @@ -810,12 +809,11 @@ " False\n", " True\n", " \n", + " 6\n", + " 1\n", " \n", " \n", - " \n", - " \n", - " 7\n", - " 1.0\n", + " 11\n", " \n", " \n", " 1\n", @@ -823,12 +821,11 @@ " False\n", " True\n", " \n", + " 6\n", + " 2\n", " \n", " \n", - " \n", - " \n", - " 8\n", - " 2.0\n", + " 12\n", " \n", " \n", " 2\n", @@ -836,12 +833,11 @@ " False\n", " True\n", " \n", + " 6\n", + " 3\n", " \n", " \n", - " \n", - " \n", - " 9\n", - " 3.0\n", + " 13\n", " \n", " \n", "\n", @@ -849,16 +845,16 @@ ] }, "metadata": {}, - "execution_count": 26 + "execution_count": 18 } ], "metadata": {} }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 20, "source": [ - "# my_queue.applicant_df.to_pickle('./test/fixtures/queueing_processes/Queue_new_applicants_fixture.pickle')" + "# my_queue.applicant_df.to_pickle('./test/fixtures/queueing_processes/Queue_new_applicants_fixture.pickle', protocol=4)" ], "outputs": [], "metadata": {} @@ -872,7 +868,7 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 21, "source": [ "my_queue = Queue(days_to_simulate=10, capacity=[10]*10)\r\n", "my_queue.add_new_applicants(\r\n", @@ -889,7 +885,7 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 22, "source": [ "my_queue.applicant_df" ], @@ -986,14 +982,14 @@ ] }, "metadata": {}, - "execution_count": 30 + "execution_count": 22 } ], "metadata": {} }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 23, "source": [ "pd.read_pickle('./test/fixtures/queueing_processes/Queue_swab_applicants.pickle')" ], @@ -1002,20 +998,20 @@ "output_type": "execute_result", "data": { "text/plain": [ - " id swabbed waiting_to_be_swabbed left_queue_not_swabbed time_symptom_onset \\\n", - "0 A False True \n", - "1 B True False False \n", - "2 C True False False \n", + " id processed waiting_to_be_processed left_queue_not_processed \\\n", + "0 A False True \n", + "1 B True False False \n", + "2 C True False False \n", "\n", - " time_joined_queue time_swabbed time_received_result time_will_leave_queue \\\n", - "0 7 \n", - "1 0 1 8 \n", - "2 0 2 9 \n", + " time_symptom_onset time_joined_queue time_processed time_received_result \\\n", + "0 6 1 \n", + "1 6 2 0 1 \n", + "2 6 3 0 2 \n", "\n", - " symptom_onset \n", - "0 1.0 \n", - "1 2.0 \n", - "2 3.0 " + " time_will_leave_queue \n", + "0 11 \n", + "1 12 \n", + "2 13 " ], "text/html": [ "
\n", @@ -1037,15 +1033,14 @@ " \n", " \n", " id\n", - " swabbed\n", - " waiting_to_be_swabbed\n", - " left_queue_not_swabbed\n", + " processed\n", + " waiting_to_be_processed\n", + " left_queue_not_processed\n", " time_symptom_onset\n", " time_joined_queue\n", - " time_swabbed\n", + " time_processed\n", " time_received_result\n", " time_will_leave_queue\n", - " symptom_onset\n", " \n", " \n", " \n", @@ -1055,12 +1050,11 @@ " False\n", " True\n", " \n", + " 6\n", + " 1\n", " \n", " \n", - " \n", - " \n", - " 7\n", - " 1.0\n", + " 11\n", " \n", " \n", " 1\n", @@ -1068,12 +1062,11 @@ " True\n", " False\n", " False\n", - " \n", - " \n", + " 6\n", + " 2\n", " 0\n", " 1\n", - " 8\n", - " 2.0\n", + " 12\n", " \n", " \n", " 2\n", @@ -1081,12 +1074,11 @@ " True\n", " False\n", " False\n", - " \n", - " \n", + " 6\n", + " 3\n", " 0\n", " 2\n", - " 9\n", - " 3.0\n", + " 13\n", " \n", " \n", "\n", @@ -1094,16 +1086,16 @@ ] }, "metadata": {}, - "execution_count": 31 + "execution_count": 23 } ], "metadata": {} }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 24, "source": [ - "# my_queue.applicant_df.to_pickle('./test/fixtures/queueing_processes/Queue_swab_applicants.pickle')" + "my_queue.applicant_df.to_pickle('./test/fixtures/queueing_processes/Queue_swab_applicants.pickle', protocol=4)" ], "outputs": [], "metadata": {} @@ -1116,17 +1108,15 @@ "metadata": {} }, { - "cell_type": "code", - "execution_count": 29, + "cell_type": "markdown", "source": [ "### Add new test seekers" ], - "outputs": [], "metadata": {} }, { "cell_type": "code", - "execution_count": 34, + "execution_count": 25, "source": [ "def processing_delay_dist():\r\n", " return 1\r\n", @@ -1149,7 +1139,7 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": 26, "source": [ "my_det_queue.add_new_test_seekers()" ], @@ -1158,7 +1148,7 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": 27, "source": [ "my_det_queue.queue.applicant_df" ], @@ -1360,16 +1350,16 @@ ] }, "metadata": {}, - "execution_count": 36 + "execution_count": 27 } ], "metadata": {} }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 28, "source": [ - "my_det_queue.queue.applicant_df.to_pickle('./test/fixtures/queueing_processes/DeterministicQueue_add_new_test_seekers.pickle')" + "my_det_queue.queue.applicant_df.to_pickle('./test/fixtures/queueing_processes/DeterministicQueue_add_new_test_seekers.pickle', protocol=4)" ], "outputs": [], "metadata": {} From c8708904b933462ebfadc15d8d5d6b053f687f7c Mon Sep 17 00:00:00 2001 From: annannfryingpan Date: Mon, 2 Aug 2021 14:04:14 +0100 Subject: [PATCH 40/70] Implemented node attribute parameters for remaining 2 attribute categories --- .../household_level_contact_tracing.ipynb | 8 +- examples/run_testing_contact_model.py | 4 +- .../infection/contact_rate_reduction.py | 8 +- .../behaviours/infection/new_infection.py | 74 ++++++---- .../intervention/increment_tracing.py | 110 +++++++------- .../behaviours/intervention/isolation.py | 24 +-- household_contact_tracing/infection.py | 14 +- household_contact_tracing/intervention.py | 24 +-- household_contact_tracing/network.py | 139 +++++++----------- household_contact_tracing/node_attributes.py | 109 ++++++++++++-- test/test_BranchingProcessSimulation.py | 12 +- test/test_TestingContactModel.py | 22 +-- test/test_integration.py | 4 +- 13 files changed, 309 insertions(+), 243 deletions(-) diff --git a/examples/household_level_contact_tracing.ipynb b/examples/household_level_contact_tracing.ipynb index e8ebe7e..68daed7 100644 --- a/examples/household_level_contact_tracing.ipynb +++ b/examples/household_level_contact_tracing.ipynb @@ -129,7 +129,7 @@ "metadata": {}, "outputs": [], "source": [ - "[node for node in controller.model.network.all_nodes() if node.isolated]" + "[node for node in controller.model.network.all_nodes() if node.infection.isolated]" ] }, { @@ -211,7 +211,7 @@ "metadata": {}, "outputs": [], "source": [ - "controller.model.network.node(1).testing_delay" + "controller.model.network.node(1).tracing.testing_delay" ] }, { @@ -339,7 +339,7 @@ " node \n", " for node \n", " in controller.model.network.all_nodes()\n", - " if node.propensity_imperfect_isolation\n", + " if node.tracing_adherence.propensity_imperfect_isolation\n", "]" ] }, @@ -353,7 +353,7 @@ " controller.model.infection.contact_rate_reduction.get_contact_rate_reduction(node)\n", " for node \n", " in controller.model.network.all_nodes()\n", - " if node.propensity_imperfect_isolation\n", + " if node.tracing_adherence.propensity_imperfect_isolation\n", "]" ] }, diff --git a/examples/run_testing_contact_model.py b/examples/run_testing_contact_model.py index f40c626..06fe0e1 100644 --- a/examples/run_testing_contact_model.py +++ b/examples/run_testing_contact_model.py @@ -6,8 +6,8 @@ def main(): - #example_1() - #example_2() + example_1() + example_2() example_3() example_4() diff --git a/household_contact_tracing/behaviours/infection/contact_rate_reduction.py b/household_contact_tracing/behaviours/infection/contact_rate_reduction.py index 5399cf0..1f09a46 100644 --- a/household_contact_tracing/behaviours/infection/contact_rate_reduction.py +++ b/household_contact_tracing/behaviours/infection/contact_rate_reduction.py @@ -58,9 +58,9 @@ def get_contact_rate_reduction(self, node: Node) -> int: intervention parameters """ - if node.isolated and node.tracing_adherence.propensity_imperfect_isolation: + if node.infection.isolated and node.tracing_adherence.propensity_imperfect_isolation: return self.global_contact_reduction_imperfect_quarantine - elif node.isolated and not node.tracing_adherence.propensity_imperfect_isolation: + elif node.infection.isolated and not node.tracing_adherence.propensity_imperfect_isolation: # return 1 means 100% of contacts are stopped return 1 else: @@ -81,11 +81,11 @@ def get_contact_rate_reduction(self, node: Node) -> int: """ # the isolated status should never apply to an individual who will not uptake intervention - if node.isolated and not node.tracing_adherence.propensity_imperfect_isolation: + if node.infection.isolated and not node.tracing_adherence.propensity_imperfect_isolation: # perfect intervention return 1 - elif node.isolated and node.tracing_adherence.propensity_imperfect_isolation: + elif node.infection.isolated and node.tracing_adherence.propensity_imperfect_isolation: # imperfect intervention return self.global_contact_reduction_imperfect_quarantine diff --git a/household_contact_tracing/behaviours/infection/new_infection.py b/household_contact_tracing/behaviours/infection/new_infection.py index 45c77de..ac80391 100644 --- a/household_contact_tracing/behaviours/infection/new_infection.py +++ b/household_contact_tracing/behaviours/infection/new_infection.py @@ -174,20 +174,30 @@ def new_infection(self, time: int, household: Household, infecting_node: Optiona } returning_travellers_attributes = {'pseudo_symptom_onset_time': pseudo_symptom_onset_time} - new_node = self.network.add_node(time_infected=time, + tracing_attributes = { + 'contact_traced': household.contact_traced, + 'has_contact_tracing_app': has_trace_app, + 'symptom_onset_time': symptom_onset_time, + 'testing_delay': self.testing_delay(), + 'time_of_reporting': time_of_reporting, + 'will_report_infection': will_report_infection, + } + + infection_attributes = { + 'time_infected': time, + 'asymptomatic': asymptomatic, + 'infecting_node': infecting_node, + 'isolated': node_is_isolated, + 'recovery_time': recovery_time, + } + + new_node = self.network.add_node( household_id=household.id, - isolated=node_is_isolated, + infection_attributes=infection_attributes, tracing_adherence_attributes=tracing_adherence_attributes, returning_travellers_attributes=returning_travellers_attributes, - asymptomatic=asymptomatic, - contact_traced=household.contact_traced, - symptom_onset_time=symptom_onset_time, - recovery_time=recovery_time, - will_report_infection=will_report_infection, - time_of_reporting=time_of_reporting, - has_contact_tracing_app=has_trace_app, - testing_delay=self.testing_delay(), - infecting_node=infecting_node) + tracing_attributes=tracing_attributes + ) # Each house now stores the ID's of which nodes are stored inside the house, # so that quarantining can be done at the household level @@ -223,11 +233,6 @@ def new_infection(self, time: int, household: Household, infecting_node: Optiona node_being_lateral_flow_tested = False time_started_lfa_testing = float('Inf') - additional_attributes = { - 'received_positive_test_result': False, - 'received_result': None, - } - asymptomatic = self.is_asymptomatic_infection() # Symptom onset time @@ -274,7 +279,9 @@ def new_infection(self, time: int, household: Household, infecting_node: Optiona 'will_uptake_isolation': isolation_uptake, 'propensity_imperfect_isolation': self.get_propensity_imperfect_isolation() } + returning_travellers_attributes = {'pseudo_symptom_onset_time': pseudo_symptom_onset_time} + lfd_testing_attributes = { 'avenue_of_testing': None, 'being_lateral_flow_tested': node_being_lateral_flow_tested, @@ -287,23 +294,34 @@ def new_infection(self, time: int, household: Household, infecting_node: Optiona 'propensity_to_miss_lfa_tests': self.propensity_to_miss_lfa_tests() } - new_node = self.network.add_node(time_infected=time, + tracing_attributes = { + 'received_positive_test_result': False, + 'received_result': None, + 'contact_traced': household.contact_traced, + 'has_contact_tracing_app': has_trace_app, + 'symptom_onset_time': symptom_onset_time, + 'testing_delay': self.testing_delay(), + 'time_of_reporting': time_of_reporting, + 'will_report_infection': will_report_infection, + } + + infection_attributes = { + 'time_infected': time, + 'asymptomatic': asymptomatic, + 'infecting_node': infecting_node, + 'isolated': node_is_isolated, + 'recovery_time': recovery_time, + } + + new_node = self.network.add_node( household_id=household.id, - isolated=node_is_isolated, + infection_attributes=infection_attributes, tracing_adherence_attributes=tracing_adherence_attributes, returning_travellers_attributes=returning_travellers_attributes, lfd_testing_attributes=lfd_testing_attributes, lfd_testing_adherence_attributes=lfd_testing_adherence_attributes, - asymptomatic=asymptomatic, - contact_traced=household.contact_traced, - symptom_onset_time=symptom_onset_time, - recovery_time=recovery_time, - will_report_infection=will_report_infection, - time_of_reporting=time_of_reporting, - has_contact_tracing_app=has_trace_app, - testing_delay=self.testing_delay(), - additional_attributes=additional_attributes, - infecting_node=infecting_node) + tracing_attributes=tracing_attributes + ) # Each house now stores the ID's of which nodes are stored inside the house, # so that quarantining can be done at the household level diff --git a/household_contact_tracing/behaviours/intervention/increment_tracing.py b/household_contact_tracing/behaviours/intervention/increment_tracing.py index d9028e8..d64c6fa 100644 --- a/household_contact_tracing/behaviours/intervention/increment_tracing.py +++ b/household_contact_tracing/behaviours/intervention/increment_tracing.py @@ -66,25 +66,25 @@ def increment_contact_tracing(self, time: int): # Isolate all households under observation that now display symptoms (excludes those # who will not take up intervention if prob <1) for node in self.network.all_nodes(): - if node.symptom_onset_time <= time: - if node.contact_traced: - if not node.isolated: - if not node.completed_isolation: + if node.tracing.symptom_onset_time <= time: + if node.tracing.contact_traced: + if not node.infection.isolated: + if not node.tracing.completed_isolation: node.household.isolate_household(time) # Propagate the contact tracing for all households that self-reported and have had their # test results come back for node in self.network.all_nodes(): - if node.time_of_reporting + node.testing_delay == time: + if node.tracing.time_of_reporting + node.tracing.testing_delay == time: if not node.household.propagated_contact_tracing: self.propagate_contact_tracing(node.household, time) # Propagate the contact tracing for all households that are isolated due to exposure, # have developed symptoms and had a test come back for node in self.network.all_nodes(): - if node.symptom_onset_time <= time: + if node.tracing.symptom_onset_time <= time: if not node.household.propagated_contact_tracing: - if node.household.isolated_time + node.testing_delay <= time: + if node.household.isolated_time + node.tracing.testing_delay <= time: self.propagate_contact_tracing(node.household, time) # Update the contact tracing index of households @@ -171,9 +171,9 @@ def update_contact_tracing_index(self, time: int): # The testing delay must be passed # The testing delay starts when the house have been isolated and symptoms have # onset - critical_time = max(node.symptom_onset_time, household.isolated_time) + critical_time = max(node.tracing.symptom_onset_time, household.isolated_time) - if critical_time + node.testing_delay <= time: + if critical_time + node.tracing.testing_delay <= time: household.contact_tracing_index = 0 for index_1_hh in household.contact_traced_households: @@ -196,30 +196,30 @@ def prob_pcr_positive(self, fn: Callable[[int], float]): self._prob_pcr_positive = fn def pcr_test_node(self, node: Node, time: int): - node.received_result = True - infectious_age_when_tested = time - node.testing_delay - node.time_infected + node.tracing.received_result = True + infectious_age_when_tested = time - node.tracing.testing_delay - node.infection.time_infected prob_positive_result = self.prob_pcr_positive(infectious_age_when_tested) node.lfd_testing.avenue_of_testing = TestType.pcr if np.random.binomial(1, prob_positive_result) == 1: - node.received_positive_test_result = True + node.tracing.received_positive_test_result = True node.lfd_testing.positive_test_time = time else: - node.received_positive_test_result = False + node.tracing.received_positive_test_result = False def receive_pcr_test_results(self, time: int): # self reporting infections for node in self.network.all_nodes(): - if node.time_of_reporting + node.testing_delay == time: - if not node.contact_traced: - if not node.received_result: + if node.tracing.time_of_reporting + node.tracing.testing_delay == time: + if not node.tracing.contact_traced: + if not node.tracing.received_result: self.pcr_test_node(node, time) # contact traced nodes for node in self.network.all_nodes(): - if node.symptom_onset_time + node.testing_delay == time: - if node.contact_traced: - if not node.received_result: + if node.tracing.symptom_onset_time + node.tracing.testing_delay == time: + if node.tracing.contact_traced: + if not node.tracing.received_result: self.pcr_test_node(node, time) def increment_contact_tracing(self, time: int): @@ -243,15 +243,15 @@ def increment_contact_tracing(self, time: int): self.receive_pcr_test_results(time) for node in self.network.all_nodes(): - if node.symptom_onset_time <= time: - if node.received_positive_test_result: - if not node.isolated: - if not node.completed_isolation: + if node.tracing.symptom_onset_time <= time: + if node.tracing.received_positive_test_result: + if not node.infection.isolated: + if not node.tracing.completed_isolation: node.household.isolate_household(time) for node in self.network.all_nodes(): - if node.received_result: - if not node.propagated_contact_tracing: + if node.tracing.received_result: + if not node.tracing.propagated_contact_tracing: self.propagate_contact_tracing(node, time) def propagate_contact_tracing(self, node: Node, time: int): @@ -260,11 +260,11 @@ def propagate_contact_tracing(self, node: Node, time: int): when a household that is under surveillance develops symptoms + gets tested. """ # update the propagation data - node.propagated_contact_tracing = True + node.tracing.propagated_contact_tracing = True # Contact tracing attempted for the household that infected the household currently # propagating the infection - infected_by_node = node.infecting_node + infected_by_node = node.infection.infecting_node # If the node was globally infected, we are backwards tracing and the infecting node is # not None @@ -272,8 +272,8 @@ def propagate_contact_tracing(self, node: Node, time: int): # if the infector is not already isolated and the time the node was infected captured # by going backwards - # the node.time_infected is when they had a contact with their infector. - if not infected_by_node.isolated and node.time_infected >= node.symptom_onset_time - \ + # the node.infection.time_infected is when they had a contact with their infector. + if not infected_by_node.isolated and node.infection.time_infected >= node.tracing.symptom_onset_time - \ self.number_of_days_to_trace_backwards: # Then attempt to contact trace the household of the node that infected you @@ -281,13 +281,13 @@ def propagate_contact_tracing(self, node: Node, time: int): house_to=infected_by_node.household, house_from=node.household, time=time, - days_since_contact_occurred=time - node.time_infected + days_since_contact_occurred=time - node.infection.time_infected ) # spread_to_global_node_time_tuples stores a list of tuples, where the first element is # the node_id of a node who was globally infected by the node, and the second element is # the time of transmission - for global_infection in node.spread_to_global_node_time_tuples: + for global_infection in node.infection.spread_to_global_node_time_tuples: # Get the child node_id and the time of transmission/time of contact child_node_id, time_t = global_infection @@ -295,9 +295,9 @@ def propagate_contact_tracing(self, node: Node, time: int): child_node = self.network.node(child_node_id) # If the node was infected 2 days prior to symptom onset, or 7 days post and is not already isolated - if time_t >= node.symptom_onset_time - self.number_of_days_to_trace_backwards and \ - time_t <= node.symptom_onset_time + self.number_of_days_to_trace_forwards and \ - not child_node.isolated: + if time_t >= node.tracing.symptom_onset_time - self.number_of_days_to_trace_backwards and \ + time_t <= node.tracing.symptom_onset_time + self.number_of_days_to_trace_forwards and \ + not child_node.infection.isolated: self.attempt_contact_trace_of_household( house_to=child_node.household, @@ -358,24 +358,24 @@ def receive_pcr_test_results(self, time: int): super().receive_pcr_test_results(time) else: for node in self.network.all_nodes(): - if node.time_of_reporting + node.testing_delay == time: - if not node.contact_traced: - if not node.received_result: - if not node.being_lateral_flow_tested: + if node.tracing.time_of_reporting + node.tracing.testing_delay == time: + if not node.tracing.contact_traced: + if not node.tracing.received_result: + if not node.lfd_testing.being_lateral_flow_tested: self.pcr_test_node(node, time) def increment_contact_tracing(self, time: int): for node in self.network.all_nodes(): - if node.received_positive_test_result: + if node.tracing.received_positive_test_result: if node.lfd_testing.avenue_of_testing == TestType.pcr: - if not node.propagated_contact_tracing: + if not node.tracing.propagated_contact_tracing: self.propagate_contact_tracing(node, time) if not self.LFA_testing_requires_confirmatory_PCR: for node in self.network.all_nodes(): - if node.received_positive_test_result: + if node.tracing.received_positive_test_result: if node.lfd_testing.avenue_of_testing == TestType.lfa: - if not node.propagated_contact_tracing: + if not node.tracing.propagated_contact_tracing: self.propagate_contact_tracing(node, time) elif self.LFA_testing_requires_confirmatory_PCR: @@ -383,7 +383,7 @@ def increment_contact_tracing(self, time: int): if node.lfd_testing.confirmatory_PCR_test_result_time == time: if node.lfd_testing_adherence.confirmatory_PCR_result_was_positive: if node.lfd_testing.avenue_of_testing == TestType.lfa: - if not node.propagated_contact_tracing: + if not node.tracing.propagated_contact_tracing: self.propagate_contact_tracing(node, time) def propagate_contact_tracing(self, node: Node, time: int): @@ -397,50 +397,50 @@ def propagate_contact_tracing(self, node: Node, time: int): # 2) Trace on confirmatory PCR result # update the propagation data - node.propagated_contact_tracing = True + node.tracing.propagated_contact_tracing = True # Contact tracing attempted for the household that infected the household currently # propagating the infection - infected_by_node = node.infecting_node + infected_by_node = node.infection.infecting_node # If the node was globally infected, we are backwards tracing and the infecting node # is not None if not node.locally_infected() and infected_by_node: # if the infector is not already isolated and the time the node was infected captured - # by going backwards the node.time_infected is when they had a contact with their + # by going backwards the node.infection.time_infected is when they had a contact with their # infector. if node.lfd_testing.avenue_of_testing == TestType.pcr: - if not infected_by_node.isolated and \ - node.time_infected >= node.symptom_onset_time - \ + if not infected_by_node.infection.isolated and \ + node.infection.time_infected >= node.tracing.symptom_onset_time - \ self.number_of_days_to_trace_backwards: # Then attempt to contact trace the household of the node that infected you self.attempt_contact_trace_of_household( house_to=infected_by_node.household, house_from=node.household, - days_since_contact_occurred=time - node.time_infected, + days_since_contact_occurred=time - node.infection.time_infected, time=time) elif node.lfd_testing.avenue_of_testing == TestType.lfa: if not self.LFA_testing_requires_confirmatory_PCR: - if not infected_by_node.isolated and node.time_infected >= \ + if not infected_by_node.isolated and node.infection.time_infected >= \ node.lfd_testing.positive_test_time - self.number_of_days_prior_to_LFA_result_to_trace: # Then attempt to contact trace the household of the node that infected you self.attempt_contact_trace_of_household( house_to=infected_by_node.household, house_from=node.household, - days_since_contact_occurred=time - node.time_infected, + days_since_contact_occurred=time - node.infection.time_infected, time=time ) # spread_to_global_node_time_tuples stores a list of tuples, where the first element is # the node_id of a node who was globally infected by the node, and the second element is # the time of transmission - for global_infection in node.spread_to_global_node_time_tuples: + for global_infection in node.infection.spread_to_global_node_time_tuples: # Get the child node_id and the time of transmission/time of contact child_node_id, time_t = global_infection @@ -451,9 +451,9 @@ def propagate_contact_tracing(self, node: Node, time: int): # If the node was infected 2 days prior to symptom onset, or 7 days post and is # not already isolated - if time_t >= node.symptom_onset_time - self.number_of_days_to_trace_backwards: - if time_t <= node.symptom_onset_time + self.number_of_days_to_trace_forwards: - if not child_node.isolated: + if time_t >= node.tracing.symptom_onset_time - self.number_of_days_to_trace_backwards: + if time_t <= node.tracing.symptom_onset_time + self.number_of_days_to_trace_forwards: + if not child_node.infection.isolated: self.attempt_contact_trace_of_household( house_to=child_node.household, diff --git a/household_contact_tracing/behaviours/intervention/isolation.py b/household_contact_tracing/behaviours/intervention/isolation.py index 6a29957..b731b04 100644 --- a/household_contact_tracing/behaviours/intervention/isolation.py +++ b/household_contact_tracing/behaviours/intervention/isolation.py @@ -90,8 +90,8 @@ def isolate_self_reporting_cases(self, time: int): """ for node in self.network.all_nodes(): if node.tracing_adherence.will_uptake_isolation: - if node.time_of_reporting == time: - node.isolated = True + if node.tracing.time_of_reporting == time: + node.infection.isolated = True def update_households_contact_traced(self, time: int): """Update the contact traced status for all households that have had the @@ -105,7 +105,7 @@ def update_households_contact_traced(self, time: int): def update_isolation(self, time: int): for node in self.network.all_nodes(): - if node.time_of_reporting + node.testing_delay == time: + if node.tracing.time_of_reporting + node.tracing.testing_delay == time: if not node.household.isolated: if not node.household.contact_traced: node.household.isolate_household(time) @@ -123,8 +123,8 @@ def update_households_contact_traced(self, time: int): def update_isolation(self, time: int): for node in self.network.all_nodes(): - if node.time_of_reporting + node.testing_delay == time: - if node.received_positive_test_result: + if node.tracing.time_of_reporting + node.tracing.testing_delay == time: + if node.tracing.received_positive_test_result: if not node.household.isolated: if not node.household.contact_traced: node.household.isolate_household(time) @@ -154,15 +154,15 @@ def update_households_contact_traced(self, time: int): traced_node = household.find_traced_node() # the traced node is now being lateral flow tested if traced_node.lfd_testing_adherence.node_will_take_up_lfa_testing: - if not traced_node.received_positive_test_result: - traced_node.being_lateral_flow_tested = True + if not traced_node.tracing.received_positive_test_result: + traced_node.lfd_testing.being_lateral_flow_tested = True traced_node.lfd_testing.time_started_lfa_testing = time def update_isolation(self, time: int): for node in self.network.all_nodes(): if node.lfd_testing.positive_test_time == time: if node.lfd_testing.avenue_of_testing == TestType.pcr: - if node.received_positive_test_result: + if node.tracing.received_positive_test_result: if not node.household.applied_household_positive_policy: node.household.apply_positive_policy(time, self.household_positive_policy) @@ -185,14 +185,14 @@ def isolate_positive_lateral_flow_tests(self, time: int, positive_nodes: List[No """ for node in positive_nodes: - node.received_positive_test_result = True + node.tracing.received_positive_test_result = True if node.tracing_adherence.will_uptake_isolation: - node.isolated = True + node.infection.isolated = True - node.avenue_of_testing = TestType.lfa + node.lfd_testing.avenue_of_testing = TestType.lfa node.lfd_testing.positive_test_time = time - node.being_lateral_flow_tested = False + node.lfd_testing.being_lateral_flow_tested = False if not node.household.applied_household_positive_policy and \ not self.LFA_testing_requires_confirmatory_PCR: diff --git a/household_contact_tracing/infection.py b/household_contact_tracing/infection.py index 5090c92..665ba67 100644 --- a/household_contact_tracing/infection.py +++ b/household_contact_tracing/infection.py @@ -95,7 +95,7 @@ def increment(self, time): household = node.household # Extracting useful parameters from the node - days_since_infected = time - node.time_infected + days_since_infected = time - node.infection.time_infected outside_household_contacts = -1 local_contacts = -1 @@ -122,7 +122,7 @@ def increment(self, time): # will be thinned again local_infection_probs = self.get_infection_prob(local=True, infectious_age=days_since_infected, - asymptomatic=node.asymptomatic) + asymptomatic=node.infection.asymptomatic) local_infective_contacts = npr.binomial(local_contacts, local_infection_probs) @@ -145,12 +145,12 @@ def increment(self, time): self.new_within_household_infection(time=time, infecting_node=node) # Update how many contacts the node made - node.outside_house_contacts_made += outside_household_contacts + node.infection.outside_house_contacts_made += outside_household_contacts # How many outside household contacts cause new infections global_infection_probs = self.get_infection_prob(local=False, infectious_age=days_since_infected, - asymptomatic=node.asymptomatic) + asymptomatic=node.infection.asymptomatic) outside_household_new_infections = npr.binomial( outside_household_contacts, global_infection_probs @@ -160,7 +160,7 @@ def increment(self, time): self.new_outside_household_infection(time=time, infecting_node=node) node_time_tuple = (self.network.node_count, time) - node.spread_to_global_node_time_tuples.append(node_time_tuple) + node.infection.spread_to_global_node_time_tuples.append(node_time_tuple) def contacts_made_today(self, household_size) -> int: """Generates the number of contacts made today by a node, given the house size of the node. @@ -269,5 +269,5 @@ def perform_recoveries(self, time: int): recovered state """ for node in self.network.all_nodes(): - if node.recovery_time == time: - node.recovered = True + if node.infection.recovery_time == time: + node.infection.recovered = True diff --git a/household_contact_tracing/intervention.py b/household_contact_tracing/intervention.py index 282f8f7..8b6a38b 100644 --- a/household_contact_tracing/intervention.py +++ b/household_contact_tracing/intervention.py @@ -56,7 +56,7 @@ def lft_nodes(self, time: int, prob_lfa_positive: Callable) -> List[Node]: for node in self.network.all_nodes(): if node.lfd_testing.being_lateral_flow_tested: if node.will_lfa_test_today(self.node_daily_prob_lfa_test): - if not node.received_positive_test_result: + if not node.tracing.received_positive_test_result: if node.lfa_test_node(time, prob_lfa_positive): positive_nodes.append(node) return positive_nodes @@ -77,20 +77,20 @@ def completed_quarantine(self, time: int): # (if they do not self-report they will not isolate; if contact traced, they will be # quarantining for the quarantine duration) # if node.household_id == node.infected_by_node().household_id: - if node.infecting_node: - if (node.infection_status(time) == InfectionStatus.unknown_infection) & node.isolated: + if node.infection.infecting_node: + if (node.infection_status(time) == InfectionStatus.unknown_infection) & node.infection.isolated: if node.locally_infected(): if time >= (node.household.earliest_recognised_symptom_onset(model_time=time) + self.quarantine_duration): node.isolated = False - node.completed_isolation = True + node.tracing.completed_isolation = True # For nodes who do not self-report, and are not in the same household as # their infector (if they do not self-report they will not isolate; if contact # traced, they will be quarantining for the quarantine duration) - elif node.contact_traced & (time >= node.time_infected + self.quarantine_duration): - node.isolated = False - node.completed_isolation = True + elif node.tracing.contact_traced & (time >= node.infection.time_infected + self.quarantine_duration): + node.infection.isolated = False + node.tracing.completed_isolation = True def completed_isolation(self, time: int): """ @@ -106,20 +106,20 @@ def completed_isolation(self, time: int): """ for node in self.network.all_nodes(): - if node.isolated: + if node.infection.isolated: infection_status = node.infection_status(time) if infection_status in [InfectionStatus.known_infection, InfectionStatus.self_recognised_infection]: if node.lfd_testing.avenue_of_testing == TestType.lfa: if time >= node.lfd_testing.positive_test_time + self.self_isolation_duration: - node.isolated = False - node.completed_isolation = True + node.infection.isolated = False + node.tracing.completed_isolation = True else: - if time >= node.symptom_onset_time + self.self_isolation_duration: + if time >= node.tracing.symptom_onset_time + self.self_isolation_duration: # this won't include nodes who tested positive due to LF tests who do not # have symptoms node.isolated = False - node.completed_isolation = True + node.tracing.completed_isolation = True def completed_lateral_flow_testing(self, time: int): """If a node is currently in lateral flow testing, and has completed this period then we diff --git a/household_contact_tracing/network.py b/household_contact_tracing/network.py index a70ee81..20d8a3f 100644 --- a/household_contact_tracing/network.py +++ b/household_contact_tracing/network.py @@ -7,7 +7,7 @@ from household_contact_tracing.parameterised import Parameterised from household_contact_tracing.node_attributes import LFDTestingAdherenceAttributes, TracingAdherenceAttributes, \ - ReturningTravellerAttributes, LFDTestingAttributes + ReturningTravellerAttributes, LFDTestingAttributes, TracingAttributes, InfectionAttributes class EdgeType(Enum): @@ -139,7 +139,7 @@ def active_infections(self): Returns: list: list of nodes able to infect """ - return [node for node in self.all_nodes() if not node.recovered] + return [node for node in self.all_nodes() if not node.infection.recovered] def is_isomorphic(self, network: Network) -> bool: """ Determine whether graphs have identical network structures.""" @@ -179,7 +179,7 @@ def household(self, house_id: int) -> Household: def count_non_recovered_nodes(self) -> int: """Returns the number of nodes not in the recovered state.""" - return len([node for node in self.all_nodes() if not node.recovered]) + return len([node for node in self.all_nodes() if not node.infection.recovered]) def get_edge_between_household(self, house1: Household, house2: Household) -> Tuple[int, int]: """Get the id's of the two nodes that connect households.""" @@ -190,16 +190,14 @@ def get_edge_between_household(self, house1: Household, house2: Household) -> Tu def is_edge_app_traced(self, edge: Tuple[int, int]) -> bool: """Returns whether two nodes have the contract tracing app.""" - node_1_app = self.node(edge[0]).has_contact_tracing_app - node_2_app = self.node(edge[1]).has_contact_tracing_app + node_1_app = self.node(edge[0]).tracing.has_contact_tracing_app + node_2_app = self.node(edge[1]).tracing.has_contact_tracing_app return node_1_app and node_2_app - def add_node(self, time_infected, household_id, isolated, asymptomatic, symptom_onset_time, - recovery_time, will_report_infection, - time_of_reporting, has_contact_tracing_app, contact_traced, testing_delay=0, - additional_attributes: Optional[dict] = None, - infecting_node: Optional[Node] = None, completed_isolation=False, + def add_node(self, household_id, + infection_attributes: Optional[dict] = None, lfd_testing_adherence_attributes: Optional[dict] = None, + tracing_attributes: Optional[dict] = None, tracing_adherence_attributes: Optional[dict] = None, returning_travellers_attributes: Optional[dict] = None, lfd_testing_attributes: Optional[dict] = None @@ -208,24 +206,13 @@ def add_node(self, time_infected, household_id, isolated, asymptomatic, symptom_ self.graph.add_node(new_node_id) new_node_household = self.household(household_id) node = Node(node_id=new_node_id, - time_infected=time_infected, household=new_node_household, - isolated=isolated, - asymptomatic=asymptomatic, - symptom_onset_time=symptom_onset_time, - recovery_time=recovery_time, - will_report_infection=will_report_infection, - time_of_reporting=time_of_reporting, - has_contact_tracing_app=has_contact_tracing_app, - contact_traced=contact_traced, - testing_delay=testing_delay, - additional_attributes=additional_attributes, + infection_attributes=infection_attributes, lfd_testing_adherence_attributes=lfd_testing_adherence_attributes, + tracing_attributes=tracing_attributes, tracing_adherence_attributes=tracing_adherence_attributes, returning_travellers_attributes=returning_travellers_attributes, - lfd_testing_attributes=lfd_testing_attributes, - infecting_node=infecting_node, - completed_isolation=completed_isolation) + lfd_testing_attributes=lfd_testing_attributes) self.graph.nodes[new_node_id]['node_obj'] = node return node @@ -266,40 +253,24 @@ class Node(Parameterised): """ - def __init__(self, node_id: int, household: Household, **attributes): + def __init__(self, node_id: int, household: Household, + infection_attributes=None, + lfd_testing_adherence_attributes=None, + tracing_attributes=None, + tracing_adherence_attributes=None, + returning_travellers_attributes=None, + lfd_testing_attributes=None): self.id = node_id self.household = household - self.time_infected = None - self.isolated = None - self.asymptomatic = None - self.symptom_onset_time = None - self.recovery_time = None - self.will_report_infection = None - self.time_of_reporting = None - self.has_contact_tracing_app = None - self.testing_delay = 0 - self.contact_traced = None - self.outside_house_contacts_made = 0 - self.spread_to_global_node_time_tuples = [] - self.recovered = False - self.propagated_contact_tracing = False - self.infecting_node = None - self.completed_isolation = False - self.received_result = False - self.received_positive_test_result = False - - # Update instance variables with anything in `additional_attributes` - self.update_params(attributes) - # Update node attribute classes - self.lfd_testing_adherence = LFDTestingAdherenceAttributes(attributes['lfd_testing_adherence_attributes']) - self.tracing_adherence = TracingAdherenceAttributes(attributes['tracing_adherence_attributes']) - self.returning_travellers = ReturningTravellerAttributes(attributes['returning_travellers_attributes']) - self.lfd_testing = LFDTestingAttributes(attributes['lfd_testing_attributes']) - - self.update_params(attributes['additional_attributes']) + self.infection = InfectionAttributes(infection_attributes) + self.lfd_testing_adherence = LFDTestingAdherenceAttributes(lfd_testing_adherence_attributes) + self.tracing = TracingAttributes(tracing_attributes) + self.tracing_adherence = TracingAdherenceAttributes(tracing_adherence_attributes) + self.returning_travellers = ReturningTravellerAttributes(returning_travellers_attributes) + self.lfd_testing = LFDTestingAttributes(lfd_testing_attributes) def time_relative_to_symptom_onset(self, time: int) -> int: # asymptomatics do not have a symptom onset time @@ -308,22 +279,22 @@ def time_relative_to_symptom_onset(self, time: int) -> int: return time - self.returning_travellers.pseudo_symptom_onset_time def locally_infected(self) -> bool: - if self.infecting_node: - return self.infecting_node.household == self.household + if self.infection.infecting_node: + return self.infection.infecting_node.household == self.household else: return False def infection_status(self, time_now: int) -> InfectionStatus: - if self.contact_traced: - if self.symptom_onset_time + self.testing_delay <= time_now: + if self.tracing.contact_traced: + if self.tracing.symptom_onset_time + self.tracing.testing_delay <= time_now: return InfectionStatus.known_infection - if self.symptom_onset_time <= time_now: + if self.tracing.symptom_onset_time <= time_now: return InfectionStatus.self_recognised_infection else: - if self.will_report_infection: - if self.time_of_reporting + self.testing_delay <= time_now: + if self.tracing.will_report_infection: + if self.tracing.time_of_reporting + self.tracing.testing_delay <= time_now: return InfectionStatus.known_infection - if self.time_of_reporting <= time_now: + if self.tracing.time_of_reporting <= time_now: return InfectionStatus.self_recognised_infection return InfectionStatus.unknown_infection @@ -334,23 +305,23 @@ def node_type(self, time=None) -> NodeType: time (int): The current increment / step number (e.g. day number) of the simulation """ if self.lfd_testing.being_lateral_flow_tested: - if self.isolated: + if self.infection.isolated: return NodeType.being_lateral_flow_tested_isolated else: return NodeType.being_lateral_flow_tested_not_isolated - elif self.isolated: + elif self.infection.isolated: return NodeType.isolated - elif not self.asymptomatic: - if self.will_report_infection: + elif not self.infection.asymptomatic: + if self.tracing.will_report_infection: return NodeType.symptomatic_will_report_infection else: return NodeType.symptomatic_will_not_report_infection - elif self.received_positive_test_result: - if self.avenue_of_testing == TestType.pcr: + elif self.tracing.received_positive_test_result: + if self.lfd_testing.avenue_of_testing == TestType.pcr: return NodeType.received_pos_test_pcr else: return NodeType.received_pos_test_lfa - elif self.received_result and self.avenue_of_testing == TestType.pcr: + elif self.tracing.received_result and self.lfd_testing.avenue_of_testing == TestType.pcr: return NodeType.received_neg_test_pcr elif self.lfd_testing.taken_confirmatory_PCR_test: if time and time >= self.confirmatory_PCR_test_result_time: @@ -358,7 +329,7 @@ def node_type(self, time=None) -> NodeType: return NodeType.confirmatory_pos_pcr_test else: return NodeType.confirmatory_neg_pcr_test - elif self.asymptomatic: + elif self.infection.asymptomatic: return NodeType.asymptomatic else: return NodeType.default @@ -366,9 +337,9 @@ def node_type(self, time=None) -> NodeType: def take_confirmatory_pcr_test(self, time: int, prob_pcr_positive: Callable): """Given a the time relative to a nodes symptom onset, will that node test positive.""" - infectious_age_when_tested = time - self.time_infected + infectious_age_when_tested = time - self.infection.time_infected - self.confirmatory_PCR_test_result_time = time + self.testing_delay + self.confirmatory_PCR_test_result_time = time + self.tracing.testing_delay self.lfd_testing.taken_confirmatory_PCR_test = True if numpy.random.binomial(1, prob_pcr_positive(infectious_age_when_tested)) == 1: @@ -389,7 +360,7 @@ def will_lfa_test_today(self, daily_prob_lfa_test: float) -> bool: def lfa_test_node(self, time: int, prob_lfa_positive: Callable): """Given a the time relative to a nodes symptom onset, will that node test positive""" - infectious_age = time - self.time_infected + infectious_age = time - self.infection.time_infected prob_positive_result = prob_lfa_positive(infectious_age) @@ -467,7 +438,7 @@ def get_recognised_symptom_onsets(self, model_time: int): infection_status = household_node.infection_status(model_time) if infection_status in [InfectionStatus.known_infection, InfectionStatus.self_recognised_infection]: - recognised_symptom_onsets.append(household_node.symptom_onset_time) + recognised_symptom_onsets.append(household_node.tracing.symptom_onset_time) return recognised_symptom_onsets def get_positive_test_times(self, model_time: int) -> List[int]: @@ -475,7 +446,7 @@ def get_positive_test_times(self, model_time: int) -> List[int]: for node in self.nodes: if node.infection_status(model_time) == InfectionStatus.known_infection: - if node.received_positive_test_result: + if node.tracing.received_positive_test_result: positive_test_times.append(node.lfd_testing.positive_test_time) return positive_test_times @@ -514,9 +485,9 @@ def isolate_household(self, time: int): # Update isolated and contact traced status for Nodes in Household for node in self.nodes: - node.contact_traced = True + node.tracing.contact_traced = True if node.tracing_adherence.will_uptake_isolation: - node.isolated = True + node.infection.isolated = True self._update_edges_on_isolation() @@ -548,7 +519,7 @@ def start_lateral_flow_testing_household(self, time: int): for node in self.nodes: if node.lfd_testing_adherence.node_will_take_up_lfa_testing: - if not node.received_positive_test_result: + if not node.tracing.received_positive_test_result: if not node.lfd_testing.being_lateral_flow_tested: node.lfd_testing.being_lateral_flow_tested = True node.lfd_testing.time_started_lfa_testing = time @@ -566,13 +537,13 @@ def start_lateral_flow_testing_household_and_quarantine(self, time): for node in self.nodes: if node.lfd_testing_adherence.node_will_take_up_lfa_testing: - if not node.received_positive_test_result: - if not node.being_lateral_flow_tested: + if not node.tracing.received_positive_test_result: + if not node.lfd_testing.being_lateral_flow_tested: node.lfd_testing.being_lateral_flow_tested = True node.lfd_testing.time_started_lfa_testing = time if node.tracing_adherence.will_uptake_isolation: - node.isolated = True + node.infection.isolated = True def apply_positive_policy(self, time: int, household_positive_policy: str): """Depending on the positive policy, different interventions are made to the household @@ -613,7 +584,7 @@ def update_network(self): # Update the nodes to the contact traced status for node in self.nodes: - node.contact_traced = True + node.tracing.contact_traced = True # Colour the edges within household self.network.label_edges_inside_household(self, EdgeType.within_house) @@ -621,7 +592,7 @@ def update_network(self): def isolate_if_symptomatic_nodes(self, time: int): """If there are any symptomatic nodes in the household then isolate the household.""" for node in self.nodes: - if node.symptom_onset_time <= time and not node.completed_isolation: + if node.tracing.symptom_onset_time <= time and not node.tracing.completed_isolation: self.isolate_household(time) break @@ -629,5 +600,5 @@ def quarantine_traced_node(self): traced_node = self.find_traced_node() # the traced node should go into quarantine - if not traced_node.isolated and traced_node.tracing_adherence.will_uptake_isolation: - traced_node.isolated = True + if not traced_node.infection.isolated and traced_node.tracing_adherence.will_uptake_isolation: + traced_node.infection.isolated = True diff --git a/household_contact_tracing/node_attributes.py b/household_contact_tracing/node_attributes.py index a48b1a6..36ffa59 100644 --- a/household_contact_tracing/node_attributes.py +++ b/household_contact_tracing/node_attributes.py @@ -1,17 +1,28 @@ -from typing import Optional, Iterator, List, Tuple, Dict, Callable +from typing import Optional from household_contact_tracing.parameterised import Parameterised +# Todo: @Martyn Please check -class NodeAttributes(Parameterised): - """ - - """ - pass - -class InfectionAttributes(NodeAttributes): +class InfectionAttributes(Parameterised): """ + A class used to store Node attributes relating to infection + Inherits from Parameterised to handle validation and updating of large number of parameters + + Attributes + ---------- + asymptomatic (boolean) + infecting_node (Node) + isolated (boolean) + outside_house_contacts_made (int) + recovered (boolean) + recovery_time (float) + spread_to_global_node_time_tuples (list) + time_infected (int) + + Methods + ------- """ @@ -20,7 +31,7 @@ def __init__(self, attributes): self.infecting_node = None self.isolated = None self.outside_house_contacts_made = 0 - self.recovered = None + self.recovered = False self.recovery_time = None self.spread_to_global_node_time_tuples = [] self.time_infected = None @@ -29,8 +40,28 @@ def __init__(self, attributes): self.update_params(attributes) -class LFDTestingAttributes(NodeAttributes): +class LFDTestingAttributes(Parameterised): """ + A class used to store Node attributes relating to LFD Testing + Inherits from Parameterised to handle validation and updating of large number of parameters + + Attributes + ---------- + avenue_of_testing (int) + being_lateral_flow_tested (boolean) + positive_test_time (int) + taken_confirmatory_PCR_test (boolean) + time_started_lfa_testing (int) + + # Todo @Martyn: Ann estimated placing these here - CHECK + propensity_risky_behaviour_lfa_testing (float) + propensity_to_miss_lfa_tests (float) + confirmatory_PCR_test_result_time (float) + completed_lateral_flow_testing_time (boolean) + lateral_flow_testing_duration (float) + + Methods + ------- """ @@ -41,7 +72,7 @@ def __init__(self, attributes): self.taken_confirmatory_PCR_test = None self.time_started_lfa_testing = None - # Todo Ann estimated location - CHECK + # Todo Ann estimated placing these here - CHECK self.propensity_risky_behaviour_lfa_testing = None self.propensity_to_miss_lfa_tests = None self.confirmatory_PCR_test_result_time = None @@ -52,8 +83,19 @@ def __init__(self, attributes): self.update_params(attributes) -class LFDTestingAdherenceAttributes(NodeAttributes): +class LFDTestingAdherenceAttributes(Parameterised): """ + A class used to store Node attributes relating to LFD Testing Adherence + Inherits from Parameterised to handle validation and updating of large number of parameters + + Attributes + ---------- + confirmatory_PCR_result_was_positive (boolean) + node_will_take_up_lfa_testing (boolean) + + + Methods + ------- """ @@ -65,9 +107,17 @@ def __init__(self, attributes): self.update_params(attributes) -class ReturningTravellerAttributes(NodeAttributes): +class ReturningTravellerAttributes(Parameterised): """ + A class used to store Node attributes relating to Returning Travellers + Inherits from Parameterised to handle validation and updating of large number of parameters + + Attributes + ---------- + pseudo_symptom_onset_time (float) + Methods + ------- """ def __init__(self, attributes): @@ -77,9 +127,26 @@ def __init__(self, attributes): self.update_params(attributes) -class TracingAttributes(NodeAttributes): +class TracingAttributes(Parameterised): """ - + A class used to store Node attributes relating to Contact Tracing + Inherits from Parameterised to handle validation and updating of large number of parameters + + Attributes + ---------- + contact_traced (boolean) + has_contact_tracing_app (boolean) + propagated_contact_tracing (boolean) + received_positive_test_result (boolean) + received_result (boolean) + symptom_onset_time (float) + testing_delay (float) + time_of_reporting (int) + will_report_infection (boolean) + completed_isolation (boolean) + + Methods + ------- """ def __init__(self, attributes): @@ -98,8 +165,18 @@ def __init__(self, attributes): self.update_params(attributes) -class TracingAdherenceAttributes(NodeAttributes): +class TracingAdherenceAttributes(Parameterised): """ + A class used to store Node attributes relating to Contact Tracing Adherence + Inherits from Parameterised to handle validation and updating of large number of parameters + + Attributes + ---------- + propensity_imperfect_isolation (float) + will_uptake_isolation (boolean) + + Methods + ------- """ diff --git a/test/test_BranchingProcessSimulation.py b/test/test_BranchingProcessSimulation.py index c1bcf6f..4efedda 100644 --- a/test/test_BranchingProcessSimulation.py +++ b/test/test_BranchingProcessSimulation.py @@ -25,10 +25,10 @@ def test_asymptomatic_nodes_attributes(): lfa_test_node = test_model.network.node(1) - assert lfa_test_node.asymptomatic is True + assert lfa_test_node.infection.asymptomatic is True # Symptom onset time is infinite - assert lfa_test_node.symptom_onset_time > 10000 - assert lfa_test_node.will_report_infection is False + assert lfa_test_node.tracing.symptom_onset_time > 10000 + assert lfa_test_node.tracing.will_report_infection is False def test_symptomatic_nodes_attributes(): @@ -42,9 +42,9 @@ def test_symptomatic_nodes_attributes(): lfa_test_node = test_model.network.node(1) - assert lfa_test_node.asymptomatic is False - assert lfa_test_node.symptom_onset_time == 5 - assert lfa_test_node.will_report_infection is True + assert lfa_test_node.infection.asymptomatic is False + assert lfa_test_node.tracing.symptom_onset_time == 5 + assert lfa_test_node.tracing.will_report_infection is True @pytest.fixture diff --git a/test/test_TestingContactModel.py b/test/test_TestingContactModel.py index 73fbcb2..252bebc 100644 --- a/test/test_TestingContactModel.py +++ b/test/test_TestingContactModel.py @@ -267,10 +267,10 @@ def test_isolate_positive_lateral_flow_tests(simple_model_high_test_prob: Indivi # but they are lfa testing model.infection.new_within_household_infection(time=model.time, infecting_node=model.network.node(1)) - assert model.network.node(1).isolated + assert model.network.node(1).infection.isolated assert model.network.household(1).applied_household_positive_policy - assert model.network.node(1).received_positive_test_result - assert not model.network.node(2).isolated + assert model.network.node(1).tracing.received_positive_test_result + assert not model.network.node(2).infection.isolated assert model.network.node(2).lfd_testing.being_lateral_flow_tested @@ -325,10 +325,10 @@ def test_start_lateral_flow_testing_household_and_quarantine( model.infection.new_within_household_infection(time=model.time, infecting_node=model.network.node(1)) - assert model.network.node(1).isolated + assert model.network.node(1).infection.isolated assert model.network.household(1).applied_household_positive_policy - assert model.network.node(1).received_positive_test_result - assert model.network.node(2).isolated + assert model.network.node(1).tracing.received_positive_test_result + assert model.network.node(2).infection.isolated assert model.network.node(2).lfd_testing.being_lateral_flow_tested @@ -385,10 +385,10 @@ def test_household_contacts_quarantine_only( model.infection.new_within_household_infection(time=model.time, infecting_node=model.network.node(1)) - assert model.network.node(1).isolated + assert model.network.node(1).infection.isolated assert model.network.household(1).applied_household_positive_policy - assert model.network.node(1).received_positive_test_result - assert model.network.node(2).isolated + assert model.network.node(1).tracing.received_positive_test_result + assert model.network.node(2).infection.isolated assert model.network.node(2).lfd_testing.being_lateral_flow_tested @@ -475,5 +475,5 @@ def test_lfa_tested_nodes_make_more_contacts_if_risky( model.simulate_one_step() # node 1 does not engage in risky behaviour and should not make any global contacts - assert model.network.node(1).outside_house_contacts_made == 0 - assert model.network.node(2).outside_house_contacts_made != 0 + assert model.network.node(1).infection.outside_house_contacts_made == 0 + assert model.network.node(2).infection.outside_house_contacts_made != 0 diff --git a/test/test_integration.py b/test/test_integration.py index 7c8a15f..a80799d 100644 --- a/test/test_integration.py +++ b/test/test_integration.py @@ -61,7 +61,7 @@ def nodes_isolating_correctly(network: Network) -> List[bool]: isolating_correctly = [] for node in network.all_nodes(): if node.household.isolated: - if node.isolated: + if node.infection.isolated: isolating_correctly.append(True) else: isolating_correctly.append(False) @@ -171,7 +171,7 @@ def test_simple_testing(self, household_params): numpy.random.seed(42) network = self.run_simulation(household_params).network - assert network.node(1).testing_delay != 0 + assert network.node(1).tracing.testing_delay != 0 # No intervention should expire by day 10 so all whose household is isolated should # be isolating. From 51350e10abbd8e8e52e92d07d186588fb36ebbed Mon Sep 17 00:00:00 2001 From: annannfryingpan Date: Mon, 2 Aug 2021 14:12:27 +0100 Subject: [PATCH 41/70] Changed name of infecting_node to infecting_node_id --- .../behaviours/intervention/increment_tracing.py | 4 ++-- household_contact_tracing/intervention.py | 2 +- household_contact_tracing/network.py | 6 +++--- household_contact_tracing/node_attributes.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/household_contact_tracing/behaviours/intervention/increment_tracing.py b/household_contact_tracing/behaviours/intervention/increment_tracing.py index d64c6fa..961b998 100644 --- a/household_contact_tracing/behaviours/intervention/increment_tracing.py +++ b/household_contact_tracing/behaviours/intervention/increment_tracing.py @@ -264,7 +264,7 @@ def propagate_contact_tracing(self, node: Node, time: int): # Contact tracing attempted for the household that infected the household currently # propagating the infection - infected_by_node = node.infection.infecting_node + infected_by_node = node.infection.infecting_node_id # If the node was globally infected, we are backwards tracing and the infecting node is # not None @@ -401,7 +401,7 @@ def propagate_contact_tracing(self, node: Node, time: int): # Contact tracing attempted for the household that infected the household currently # propagating the infection - infected_by_node = node.infection.infecting_node + infected_by_node = node.infection.infecting_node_id # If the node was globally infected, we are backwards tracing and the infecting node # is not None diff --git a/household_contact_tracing/intervention.py b/household_contact_tracing/intervention.py index 8b6a38b..5d56f27 100644 --- a/household_contact_tracing/intervention.py +++ b/household_contact_tracing/intervention.py @@ -77,7 +77,7 @@ def completed_quarantine(self, time: int): # (if they do not self-report they will not isolate; if contact traced, they will be # quarantining for the quarantine duration) # if node.household_id == node.infected_by_node().household_id: - if node.infection.infecting_node: + if node.infection.infecting_node_id: if (node.infection_status(time) == InfectionStatus.unknown_infection) & node.infection.isolated: if node.locally_infected(): diff --git a/household_contact_tracing/network.py b/household_contact_tracing/network.py index 20d8a3f..cae81a4 100644 --- a/household_contact_tracing/network.py +++ b/household_contact_tracing/network.py @@ -134,7 +134,7 @@ def all_households(self) -> Iterator[Household]: def active_infections(self): """Returns a list of nodes who have not yet recovered. - Nodes can still infect unless they have been isolated. + Nodes can still infect unless they have been isolated. Returns: list: list of nodes able to infect @@ -279,8 +279,8 @@ def time_relative_to_symptom_onset(self, time: int) -> int: return time - self.returning_travellers.pseudo_symptom_onset_time def locally_infected(self) -> bool: - if self.infection.infecting_node: - return self.infection.infecting_node.household == self.household + if self.infection.infecting_node_id: + return self.infection.infecting_node_id.household == self.household else: return False diff --git a/household_contact_tracing/node_attributes.py b/household_contact_tracing/node_attributes.py index 36ffa59..fcd6939 100644 --- a/household_contact_tracing/node_attributes.py +++ b/household_contact_tracing/node_attributes.py @@ -28,7 +28,7 @@ class InfectionAttributes(Parameterised): def __init__(self, attributes): self.asymptomatic = None - self.infecting_node = None + self.infecting_node_id = None self.isolated = None self.outside_house_contacts_made = 0 self.recovered = False From cb42019530f4a57beeb868618ded4cc4393fc352 Mon Sep 17 00:00:00 2001 From: annannfryingpan Date: Mon, 2 Aug 2021 14:49:53 +0100 Subject: [PATCH 42/70] changed InfectionAttribute.infecting_node to be an id (int) rather than a Node type --- .../behaviours/infection/new_infection.py | 11 +++++++++-- .../behaviours/intervention/increment_tracing.py | 9 +++++++-- household_contact_tracing/network.py | 3 ++- household_contact_tracing/node_attributes.py | 2 +- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/household_contact_tracing/behaviours/infection/new_infection.py b/household_contact_tracing/behaviours/infection/new_infection.py index ac80391..eab3493 100644 --- a/household_contact_tracing/behaviours/infection/new_infection.py +++ b/household_contact_tracing/behaviours/infection/new_infection.py @@ -183,10 +183,13 @@ def new_infection(self, time: int, household: Household, infecting_node: Optiona 'will_report_infection': will_report_infection, } + infecting_node_id = None + if infecting_node: + infecting_node_id = infecting_node.id infection_attributes = { 'time_infected': time, 'asymptomatic': asymptomatic, - 'infecting_node': infecting_node, + 'infecting_node': infecting_node_id, 'isolated': node_is_isolated, 'recovery_time': recovery_time, } @@ -305,10 +308,14 @@ def new_infection(self, time: int, household: Household, infecting_node: Optiona 'will_report_infection': will_report_infection, } + infecting_node_id = None + if infecting_node: + infecting_node_id = infecting_node.id + infection_attributes = { 'time_infected': time, 'asymptomatic': asymptomatic, - 'infecting_node': infecting_node, + 'infecting_node': infecting_node_id, 'isolated': node_is_isolated, 'recovery_time': recovery_time, } diff --git a/household_contact_tracing/behaviours/intervention/increment_tracing.py b/household_contact_tracing/behaviours/intervention/increment_tracing.py index 961b998..8fba3bc 100644 --- a/household_contact_tracing/behaviours/intervention/increment_tracing.py +++ b/household_contact_tracing/behaviours/intervention/increment_tracing.py @@ -264,7 +264,9 @@ def propagate_contact_tracing(self, node: Node, time: int): # Contact tracing attempted for the household that infected the household currently # propagating the infection - infected_by_node = node.infection.infecting_node_id + infected_by_node = None + if node.infection.infecting_node_id: + infected_by_node = node.household.network.node(node.infection.infecting_node_id) # If the node was globally infected, we are backwards tracing and the infecting node is # not None @@ -401,7 +403,10 @@ def propagate_contact_tracing(self, node: Node, time: int): # Contact tracing attempted for the household that infected the household currently # propagating the infection - infected_by_node = node.infection.infecting_node_id + + infected_by_node = None + if node.infection.infecting_node_id: + infected_by_node = node.household.network.node(node.infection.infecting_node_id) # If the node was globally infected, we are backwards tracing and the infecting node # is not None diff --git a/household_contact_tracing/network.py b/household_contact_tracing/network.py index cae81a4..1a8f678 100644 --- a/household_contact_tracing/network.py +++ b/household_contact_tracing/network.py @@ -280,7 +280,8 @@ def time_relative_to_symptom_onset(self, time: int) -> int: def locally_infected(self) -> bool: if self.infection.infecting_node_id: - return self.infection.infecting_node_id.household == self.household + #return self.infection.infecting_node_id.household == self.household + return self.household.network.node(self.infection.infecting_node_id).household == self.household else: return False diff --git a/household_contact_tracing/node_attributes.py b/household_contact_tracing/node_attributes.py index fcd6939..eefd381 100644 --- a/household_contact_tracing/node_attributes.py +++ b/household_contact_tracing/node_attributes.py @@ -13,7 +13,7 @@ class InfectionAttributes(Parameterised): Attributes ---------- asymptomatic (boolean) - infecting_node (Node) + infecting_node_id (int) isolated (boolean) outside_house_contacts_made (int) recovered (boolean) From 7f64451009a54bab639d5fa9cbd1e11b1a24a637 Mon Sep 17 00:00:00 2001 From: annannfryingpan Date: Mon, 2 Aug 2021 14:52:14 +0100 Subject: [PATCH 43/70] added node attribute schemas, not used, but could be?? --- .../schemas/node_attributes/infection.json | 58 ++++++++++++++++++ .../schemas/node_attributes/lfd_testing.json | 36 +++++++++++ .../lfd_testing_adherence.json | 21 +++++++ .../node_attributes/returning_travellers.json | 16 +++++ .../schemas/node_attributes/tracing.json | 61 +++++++++++++++++++ .../node_attributes/tracing_adherence.json | 20 ++++++ 6 files changed, 212 insertions(+) create mode 100644 household_contact_tracing/schemas/node_attributes/infection.json create mode 100644 household_contact_tracing/schemas/node_attributes/lfd_testing.json create mode 100644 household_contact_tracing/schemas/node_attributes/lfd_testing_adherence.json create mode 100644 household_contact_tracing/schemas/node_attributes/returning_travellers.json create mode 100644 household_contact_tracing/schemas/node_attributes/tracing.json create mode 100644 household_contact_tracing/schemas/node_attributes/tracing_adherence.json diff --git a/household_contact_tracing/schemas/node_attributes/infection.json b/household_contact_tracing/schemas/node_attributes/infection.json new file mode 100644 index 0000000..1b2259f --- /dev/null +++ b/household_contact_tracing/schemas/node_attributes/infection.json @@ -0,0 +1,58 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Node infection attribute schema", + "description": "A schema for validating the infection attributes of nodes in a household branching process model", + "type": "object", + "anyOf": [ + {"required": ["asymptomatic"]}, + {"required": ["infecting_node"]}, + {"required": ["isolated"]}, + {"required": ["outside_house_contacts_made"]}, + {"required": ["recovered"]}, + {"required": ["recovery_time"]}, + {"required": ["spread_to_global_node_time_tuples"]}, + {"required": ["time_infected"]} + ], + "properties": { + "asymptomatic": { + "description": "Is the node asymptomatic?", + "type": "boolean" + }, + "infecting_node": { + "description": "The node that infected this node [CHECK]", + "type": ["integer", "null"] + }, + "isolated": { + "description": "Has the node isolated?", + "type": "boolean" + }, + "outside_house_contacts_made": { + "description": "Has the node made outside contacts?", + "type": "boolean" + }, + "recovered": { + "description": "Has the node recovered?", + "type": "boolean" + }, + "recovery_time": { + "description": "The time taken for the node to recover", + "type": ["number", "null"] + }, + "spread_to_global_node_time_tuples": { + "description": "List of node/times where spread to global.", + "type": "array", + "items": { + "type": "array", + "items": { + "type": "integer", + "type": "number" + } + } + }, + "time_infected": { + "description": "The time that the node was infected.", + "type": ["number", "null"] + } + } +} + diff --git a/household_contact_tracing/schemas/node_attributes/lfd_testing.json b/household_contact_tracing/schemas/node_attributes/lfd_testing.json new file mode 100644 index 0000000..980fa90 --- /dev/null +++ b/household_contact_tracing/schemas/node_attributes/lfd_testing.json @@ -0,0 +1,36 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Node infection attribute schema", + "description": "A schema for validating the attributes of nodes in a household branching process model", + "type": "object", + "anyOf": [ + {"required": ["avenue_of_testing"]}, + {"required": ["being_lateral_flow_tested"]}, + {"required": ["positive_test_time"]}, + {"required": ["taken_confirmatory_pcr_test"]}, + {"required": ["time_started_LFA_testing"]} + ], + "properties": { + "avenue_of_testing": { + "description": "What was the avenue of testing? 0=PCR, 1=LFA", + "type": ["integer", "null"] + }, + "being_lateral_flow_tested": { + "description": "Is the node being lateral flow tested?", + "type": "boolean" + }, + "positive_test_time": { + "description": "The time taken to receive a positive test result", + "type": ["number", "null"] + }, + "taken_confirmatory_pcr_test": { + "description": "Has the node taken a confirmatory PCR test?", + "type": "boolean" + }, + "time_started_LFA_testing": { + "description": "The time that the node started LFA testing", + "type": ["number", "null"] + } + } +} + diff --git a/household_contact_tracing/schemas/node_attributes/lfd_testing_adherence.json b/household_contact_tracing/schemas/node_attributes/lfd_testing_adherence.json new file mode 100644 index 0000000..dc17c15 --- /dev/null +++ b/household_contact_tracing/schemas/node_attributes/lfd_testing_adherence.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Node infection attribute schema", + "description": "A schema for validating the attributes of nodes in a household branching process model", + "type": "object", + "anyOf": [ + {"required": ["confirmatory_PCR_result_was_positive"]}, + {"required": ["node_will_take_up_LFA_testing"]} + ], + "properties": { + "confirmatory_PCR_result_was_positive": { + "description": "Was the confirmatory PCR test positive?", + "type": "boolean" + }, + "node_will_take_up_LFA_testing": { + "description": "Will the node take up LFA testing?", + "type": "boolean" + } + } +} + diff --git a/household_contact_tracing/schemas/node_attributes/returning_travellers.json b/household_contact_tracing/schemas/node_attributes/returning_travellers.json new file mode 100644 index 0000000..693bcc3 --- /dev/null +++ b/household_contact_tracing/schemas/node_attributes/returning_travellers.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Node returning travellers attribute schema", + "description": "A schema for validating the attributes of returning traveller nodes in a household branching process model", + "type": "object", + "anyOf": [ + {"required": ["pseudo_symptom_onset_time"]} + ], + "properties": { + "pseudo_symptom_onset_time": { + "description": "Pseudo time of onset of symptoms", + "type": ["number", "null"] + } + } +} + diff --git a/household_contact_tracing/schemas/node_attributes/tracing.json b/household_contact_tracing/schemas/node_attributes/tracing.json new file mode 100644 index 0000000..546906a --- /dev/null +++ b/household_contact_tracing/schemas/node_attributes/tracing.json @@ -0,0 +1,61 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Node infection attribute schema", + "description": "A schema for validating the contact tracing attributes of nodes in a household branching process model", + "type": "object", + "anyOf": [ + {"required": ["contact_traced"]}, + {"required": ["has_contact_tracing_app"]}, + {"required": ["propagated_contact_tracing"]}, + {"required": ["received_positive_test_result"]}, + {"required": ["received_result"]}, + {"required": ["symptom_onset_time"]}, + {"required": ["testing_delay"]}, + {"required": ["time_of_reporting"]}, + {"required": ["will_report_infection"]}, + {"required": ["completed_isolation"]} + ], + "properties": { + "contact_traced": { + "description": "Has the node been contact traced?", + "type": "boolean" + }, + "has_contact_tracing_app": { + "description": "Does this node have the contact tracing app?", + "type": "boolean" + }, + "propagated_contact_tracing": { + "description": "Has the node propagated contact tracing?", + "type": "boolean" + }, + "received_positive_test_result": { + "description": "Has the node received a positive test result?", + "type": "boolean" + }, + "received_result": { + "description": "Has the node received any test result?", + "type": "boolean" + }, + "symptom_onset_time": { + "description": "The time taken for symptom onset for this node", + "type": ["number", "null"] + }, + "testing_delay": { + "description": "The delay time before node was tested.", + "type": ["number", "null"] + }, + "time_of_reporting": { + "description": "The time that the node reported symptoms [CHECK].", + "type": ["number", "null"] + }, + "will_report_infection": { + "description": "Will the node report an infection?", + "type": "boolean" + }, + "completed_isolation": { + "description": "Has the node completed isolation?", + "type": "boolean" + } + } +} + diff --git a/household_contact_tracing/schemas/node_attributes/tracing_adherence.json b/household_contact_tracing/schemas/node_attributes/tracing_adherence.json new file mode 100644 index 0000000..2d8677a --- /dev/null +++ b/household_contact_tracing/schemas/node_attributes/tracing_adherence.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Node tracing adherence attribute schema", + "description": "A schema for validating the tracing adherence attributes of nodes in a household branching process model", + "type": "object", + "anyOf": [ + {"required": ["will_uptake_isolation"]}, + {"required": ["propensity_imperfect_isolation"]} + ], + "properties": { + "will_uptake_isolation": { + "description": "Will this node uptake isolation?", + "type": "boolean" + }, + "propensity_imperfect_isolation": { + "description": "The propensity of this node to perform imperfect isolation?", + "type": ["boolean", "null"] + } + } +} \ No newline at end of file From 011fd2b6d0f7dcd5578ca3951266485470e7a2b5 Mon Sep 17 00:00:00 2001 From: Martyn Fyles Date: Thu, 5 Aug 2021 13:00:05 +0100 Subject: [PATCH 44/70] Renamed GrowthRateView to StatisticsView --- .../branching_process_controller.py | 6 +++--- household_contact_tracing/calibration.py | 9 ++++++++- household_contact_tracing/views/growth_rate_view.py | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/household_contact_tracing/branching_process_controller.py b/household_contact_tracing/branching_process_controller.py index e9613ea..f601c27 100644 --- a/household_contact_tracing/branching_process_controller.py +++ b/household_contact_tracing/branching_process_controller.py @@ -1,11 +1,11 @@ -from household_contact_tracing.views.growth_rate_view import GrowthRateView +from household_contact_tracing.views.growth_rate_view import StatisticsView from household_contact_tracing.branching_process_model import BranchingProcessModel from household_contact_tracing.views.shell_view import ShellView from household_contact_tracing.views.csv_file_view import CSVFileView from household_contact_tracing.views.graph_view import GraphView from household_contact_tracing.views.graph_pyvis_view import GraphPyvisView from household_contact_tracing.views.timeline_graph_view import TimelineGraphView -from household_contact_tracing.views.growth_rate_view import GrowthRateView +from household_contact_tracing.views.growth_rate_view import StatisticsView class BranchingProcessController: @@ -49,7 +49,7 @@ def __init__(self, model: BranchingProcessModel): self.timeline_view = TimelineGraphView(model) self.shell_view = ShellView(model) self.csv_view = CSVFileView(model) - self.growth_rate_view = GrowthRateView(model) + self.growth_rate_view = StatisticsView(model) self.set_graphic_displays(False) diff --git a/household_contact_tracing/calibration.py b/household_contact_tracing/calibration.py index a1b762a..5b3d5f1 100644 --- a/household_contact_tracing/calibration.py +++ b/household_contact_tracing/calibration.py @@ -90,7 +90,7 @@ def __init__( self.optimisation_complete = False - def eval_growth_rate( + def eval_metrics( self, outside_household_infectivity_scaling: float, max_time: int = 20, @@ -113,6 +113,13 @@ def eval_growth_rate( return controller.growth_rate_view.get_growth_rate() + def evaluate_household_secondary_attack_rate(self) -> float: + """[summary] + + Returns: + float: [description] + """ + def evaluate_fit(self, outside_household_infectivity_scaling) -> float: return abs(self.desired_growth_rate - self.eval_growth_rate(outside_household_infectivity_scaling)) diff --git a/household_contact_tracing/views/growth_rate_view.py b/household_contact_tracing/views/growth_rate_view.py index 58cee8c..691d1a5 100644 --- a/household_contact_tracing/views/growth_rate_view.py +++ b/household_contact_tracing/views/growth_rate_view.py @@ -6,7 +6,7 @@ import statsmodels.api as sm import numpy as np -class GrowthRateView(BranchingProcessView): +class StatisticsView(BranchingProcessView): """ View that estimates the growth rate of a completed simulation From a1d8f1397df8baab212dba73591e15880314760e Mon Sep 17 00:00:00 2001 From: Martyn Fyles Date: Thu, 5 Aug 2021 13:02:42 +0100 Subject: [PATCH 45/70] growth_rate_view renaming --- household_contact_tracing/branching_process_controller.py | 6 +++--- .../views/{growth_rate_view.py => statistics_view.py} | 0 2 files changed, 3 insertions(+), 3 deletions(-) rename household_contact_tracing/views/{growth_rate_view.py => statistics_view.py} (100%) diff --git a/household_contact_tracing/branching_process_controller.py b/household_contact_tracing/branching_process_controller.py index f601c27..490da18 100644 --- a/household_contact_tracing/branching_process_controller.py +++ b/household_contact_tracing/branching_process_controller.py @@ -1,11 +1,11 @@ -from household_contact_tracing.views.growth_rate_view import StatisticsView +from household_contact_tracing.views.statistics_view import StatisticsView from household_contact_tracing.branching_process_model import BranchingProcessModel from household_contact_tracing.views.shell_view import ShellView from household_contact_tracing.views.csv_file_view import CSVFileView from household_contact_tracing.views.graph_view import GraphView from household_contact_tracing.views.graph_pyvis_view import GraphPyvisView from household_contact_tracing.views.timeline_graph_view import TimelineGraphView -from household_contact_tracing.views.growth_rate_view import StatisticsView +from household_contact_tracing.views.statistics_view import StatisticsView class BranchingProcessController: @@ -49,7 +49,7 @@ def __init__(self, model: BranchingProcessModel): self.timeline_view = TimelineGraphView(model) self.shell_view = ShellView(model) self.csv_view = CSVFileView(model) - self.growth_rate_view = StatisticsView(model) + self.statistics_view = StatisticsView(model) self.set_graphic_displays(False) diff --git a/household_contact_tracing/views/growth_rate_view.py b/household_contact_tracing/views/statistics_view.py similarity index 100% rename from household_contact_tracing/views/growth_rate_view.py rename to household_contact_tracing/views/statistics_view.py From 8185e8f2ad88e1d4de4453e091e08e37af81c4ad Mon Sep 17 00:00:00 2001 From: Martyn Fyles Date: Thu, 5 Aug 2021 13:44:13 +0100 Subject: [PATCH 46/70] added a local_epidemic_completed var to households --- household_contact_tracing/network.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/household_contact_tracing/network.py b/household_contact_tracing/network.py index a927a2f..168a624 100644 --- a/household_contact_tracing/network.py +++ b/household_contact_tracing/network.py @@ -634,3 +634,11 @@ def quarantine_traced_node(self): # the traced node should go into quarantine if not traced_node.isolated and traced_node.will_uptake_isolation: traced_node.isolated = True + + @property + def local_epidemic_completed(self): + """ + Returns true if all infections in the household have recovered, + which is defined as being 10 + """ + return all([node.recovered for node in self.nodes]) From f37c8c59677dac84c2cc8cee415a0ae48c4086e1 Mon Sep 17 00:00:00 2001 From: Martyn Fyles Date: Thu, 5 Aug 2021 14:45:05 +0100 Subject: [PATCH 47/70] Added simulation method needed for calibration --- .../branching_process_controller.py | 14 +++++ .../branching_process_model.py | 18 +++++++ .../branching_process_models.py | 52 +++++++++++++++++++ household_contact_tracing/infection.py | 3 ++ 4 files changed, 87 insertions(+) diff --git a/household_contact_tracing/branching_process_controller.py b/household_contact_tracing/branching_process_controller.py index 490da18..b983644 100644 --- a/household_contact_tracing/branching_process_controller.py +++ b/household_contact_tracing/branching_process_controller.py @@ -94,3 +94,17 @@ def run_simulation(self, max_time: int = 20, max_active_infections: int = 5000): None """ self._model.run_simulation(max_time, max_active_infections) + + def run_hh_sar_simulation(self): + """ + This simulation method with only simulate the infection process for households in the first + generation of the epidemic, and will continue until all nodes in the initial households of the + epidemic are recovered. This is primarily useful when we are estimating the household secondary + attack rate. If we simulated onwards transmission, and examined households where the local + epidemic was completed, we would end up with a biased sample - the longer local epidemics would + be less likely to be included in the sample. + + Returns: + None + """ + self._model.run_hh_sar_simulation() diff --git a/household_contact_tracing/branching_process_model.py b/household_contact_tracing/branching_process_model.py index 463777a..4f7b37e 100644 --- a/household_contact_tracing/branching_process_model.py +++ b/household_contact_tracing/branching_process_model.py @@ -86,6 +86,24 @@ def run_simulation(self, max_time: int, max_active_infections: int) -> None: None """ + @abstractmethod + def run_hh_sar_simulation(self) -> None: + """ + This simulation method with only simulate the infection process for households in the first + generation of the epidemic, and will continue until all nodes in the initial households of the + epidemic are recovered. This is primarily useful when we are estimating the household secondary + attack rate. If we simulated onwards transmission, and examined households where the local + epidemic was completed, we would end up with a biased sample - the longer local epidemics would + be less likely to be included in the sample. + + Parameters: + max_time (int): The maximum number of iterations (eg. days) to be run (simulation stops if reached) + max_active_infections (int): The maximum number of infectious nodes (simulation stops if reached) + + Returns: + None + """ + def copy_observers(self, model: BranchingProcessModel): self._observers_graph_change = list(model._observers_graph_change) self._observers_step_increment = list(model._observers_step_increment) diff --git a/household_contact_tracing/branching_process_models.py b/household_contact_tracing/branching_process_models.py index 1e98fee..70df473 100644 --- a/household_contact_tracing/branching_process_models.py +++ b/household_contact_tracing/branching_process_models.py @@ -155,6 +155,58 @@ def run_simulation(self, max_time: int, max_active_infections: int = 1000) -> No super()._simulation_stopped() + def run_hh_sar_simulation(self) -> None: + """ Runs the simulation only for the first generation of the household epidemic: + Sets model state, + Announces start/stopped and step increments to observers + + This simulation method with only simulate the infection process for households in the first + generation of the epidemic, and will continue until all nodes in the initial households of the + epidemic are recovered. This is primarily useful when we are estimating the household secondary + attack rate. If we simulated onwards transmission, and examined households where the local + epidemic was completed, we would end up with a biased sample - the longer local epidemics would + be less likely to be included in the sample. + + Returns: + None + """ + + # Switch model to RunningState + self._state.switch(RunningState) + + while type(self.state) is RunningState: + prev_network = deepcopy(self.network) + + # This chunk of code executes a days worth of infections and recoveries, but no tracing + self.infection.increment(self.time) + self.infection.perform_recoveries(self.time) + + # if an infection is in a second generation household, set them to recovered so that + # they do not infect. This is mainly for computational ease + for node in self.network.all_nodes(): + if node.household.id not in self.infection.starting_households: + node.recovered = True + + self.time += 1 + + # If graph changed, tell parent + if not prev_network == self.network: + BranchingProcessModel.graph_changed(self) + + # Call parent completed step + super()._completed_step_increment() + + # the simulation ends when all nodes in the initial generation have recovered + if self.network.count_non_recovered_nodes() == 0: + # Simulation ends if no more infectious nodes + self.state.switch(ExtinctState, + total_increments=self.time, + non_recovered_nodes=0, + total_nodes=self.network.node_count) + + # Tell parent simulation stopped + super()._simulation_stopped() + class IndividualLevelTracing(HouseholdLevelTracing): """ A class used to represent a simulation of contact tracing of households along with diff --git a/household_contact_tracing/infection.py b/household_contact_tracing/infection.py index 5090c92..91fd969 100644 --- a/household_contact_tracing/infection.py +++ b/household_contact_tracing/infection.py @@ -89,6 +89,9 @@ def initialise(self): new_household = self.new_household.new_household(0, None) self.new_infection.new_infection(0, new_household) + # store the set of starting households + self.starting_households = [household.id for household in self.network.all_households] + def increment(self, time): """Create a new days worth of infections.""" for node in self.network.active_infections: From a66aeab5bbe21b6eac04aa8c0532cbff29b749c1 Mon Sep 17 00:00:00 2001 From: Martyn Fyles Date: Thu, 5 Aug 2021 14:45:19 +0100 Subject: [PATCH 48/70] added useful household attribute --- household_contact_tracing/network.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/household_contact_tracing/network.py b/household_contact_tracing/network.py index 168a624..31e8560 100644 --- a/household_contact_tracing/network.py +++ b/household_contact_tracing/network.py @@ -642,3 +642,10 @@ def local_epidemic_completed(self): which is defined as being 10 """ return all([node.recovered for node in self.nodes]) + + @property + def household_epidemic_size(self): + """Returns the current size of the household epidemic, i.e: the number of household members + that are, or were, infected. + """ + return self.size - self.susceptibles From eb6c9830c806a96820d85e94150812357c0b095e Mon Sep 17 00:00:00 2001 From: Martyn Fyles Date: Thu, 5 Aug 2021 16:11:15 +0100 Subject: [PATCH 49/70] Added code to estimate the household SAR --- .../views/statistics_view.py | 58 ++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/household_contact_tracing/views/statistics_view.py b/household_contact_tracing/views/statistics_view.py index 691d1a5..c76eb2b 100644 --- a/household_contact_tracing/views/statistics_view.py +++ b/household_contact_tracing/views/statistics_view.py @@ -3,6 +3,7 @@ from household_contact_tracing.branching_process_model import BranchingProcessModel from household_contact_tracing.branching_process_state import MaxNodesInfectiousState, ReadyState, RunningState, ExtinctState from household_contact_tracing.exceptions import Error, ModelStateError +import scipy.stats as ss import statsmodels.api as sm import numpy as np @@ -144,7 +145,6 @@ def _estimate_growth_rate(self, discard_first_n_days: int = 10, verbose = True): glm_poisson = sm.GLM(y, X, family=sm.families.Poisson()) self.glm_poisson = glm_poisson.fit() - def get_growth_rate(self, discard_first_n_days: int = 10, verbose: bool = True): """Returns the growth rate of the simulated epidemic, estimated using poisson regression. @@ -181,5 +181,61 @@ def growth_rate_summary(self, discard_first_n_days: int = 10, alpha: float = 0.0 print(f'{num_eligible_dates} time periods were used to estimate the growth rate.') print(f'The estimated growth rate was {round(growth_rate*100, 2)}% ({100*(1-alpha)}% CI: {round(growth_rate_ci[0]*100,2)}-{round(growth_rate_ci[1]*100,2)}%) per day.') print(f'The estimated doubling time is {round(doubling_time, 2)} ({100*(1-alpha)}% CI: {round(doubling_time_ci[1],2)}-{round(doubling_time_ci[0],2)}) days.') + + def _estimate_household_secondary_attack_rate(self, use_first_generation_only: bool = False) -> None: + if isinstance(self._model.state, ReadyState): + raise ModelStateError(self._model.state, 'Simulation has not started yet. Cannot estimate growth rate.') + + if use_first_generation_only: + households_with_completed_local_epidemics = [ + household + for household + in self._model.network.all_households + if household.local_epidemic_completed + and household.id in self._model.infection.starting_households + ] + else: + households_with_completed_local_epidemics = [ + household + for household + in self._model.network.all_households + if household.local_epidemic_completed + ] + + # size of household - number of remaining susceptibles = final size. + # we subtract 1, to work out the number of non-index secondary infections + self.total_infected = sum([ + household.size - household.susceptibles - 1 + for household + in households_with_completed_local_epidemics + ]) + + # we subtract 1, to work out the number of non-index exposed individuals + self.total_exposed = sum([ + household.size - 1 for household in households_with_completed_local_epidemics + ]) + + self.n_households_with_completed_local_epidemics = len(households_with_completed_local_epidemics) + + self.household_sar = self.total_infected / self.total_exposed + # calculating some confidence intervals using the good ol' Jefferys interval + self.household_sar_ci = ss.beta.interval(alpha = 0.95, a = self.total_infected + 0.5, b = self.total_exposed - self.total_infected + 0.5) + + def household_secondary_attack_rate_summary(self, use_first_generation_only: bool = False, alpha: float = 0.95) -> None: + """Estimates the household secondary attack rate, and prints and interpretable output. + + Args: + use_first_generation_only (bool, optional): Use the first generation of the household epidemic only to estimate the household secondary attack. Defaults to False. + """ + self._estimate_household_secondary_attack_rate(use_first_generation_only) + + print('Household secondary attack rate summary:') + print(f'{self.n_households_with_completed_local_epidemics} were eligible to be included.') + if use_first_generation_only: + print('Only the first generation of the household epidemic was included in this calculation.') + else: + print('All households with completed local epidemics were included. This may lead to a biased sample, as it is possible that local epidemics with a long duration were not included.') + print(f'There were {self.total_exposed} non-index susceptible individuals exposed, of which {self.total_infected} were infected.') + print(f'This yields a household secondary attack rate of {round(self.household_sar*100)}% ({alpha * 100}% CI: ({round(self.household_sar_ci[0]*100)}, {round(self.household_sar_ci[1]*100)})') From 1018d03c839553eb75d24897d8e2b00a9220e4c5 Mon Sep 17 00:00:00 2001 From: Martyn Fyles Date: Thu, 5 Aug 2021 16:39:24 +0100 Subject: [PATCH 50/70] calibration of hh sar added --- household_contact_tracing/calibration.py | 57 ++++++++++++++++++------ 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/household_contact_tracing/calibration.py b/household_contact_tracing/calibration.py index 5b3d5f1..5a0e82b 100644 --- a/household_contact_tracing/calibration.py +++ b/household_contact_tracing/calibration.py @@ -47,11 +47,13 @@ def __init__( self, #household_pairwise_survival_prob: float, desired_growth_rate: float, + desired_hh_sar: float, asymptomatic_prob: float, asymptomatic_relative_infectivity: float, infection_reporting_prob: float, reduce_contacts_by: float, - starting_infections: int = 100 + starting_infections: int = 100, + starting_infections_hh_sar: int = 1000 ): # initialise non-infection parameters that are held constant between simulations @@ -87,11 +89,14 @@ def __init__( self.fixed_params['starting_infections'] = starting_infections self.desired_growth_rate = desired_growth_rate + self.desired_hh_sar = desired_hh_sar + self.starting_infections_hh_sar = starting_infections_hh_sar self.optimisation_complete = False def eval_metrics( - self, + self, + household_pairwise_survival_prob: float, outside_household_infectivity_scaling: float, max_time: int = 20, max_active_infections: int = 1e5) -> float: @@ -105,27 +110,40 @@ def eval_metrics( params = copy(self.fixed_params) params['outside_household_infectivity_scaling'] = outside_household_infectivity_scaling + params['household_pairwise_survival_prob'] = household_pairwise_survival_prob + # run a simulation to get the growth rate of the epidemic controller = BranchingProcessController(HouseholdLevelTracing(params)) - controller.csv_view.set_display(False) controller.run_simulation(max_time, max_active_infections) - return controller.growth_rate_view.get_growth_rate() + # use a different simulation method to get the household secondary attack rate of the epidemic + params['starting_infections'] = self.starting_infections_hh_sar # use a higher number of starting infections + controller_hh_sar = BranchingProcessController(HouseholdLevelTracing(params)) + controller_hh_sar.csv_view.set_display(False) + controller_hh_sar.run_hh_sar_simulation() - def evaluate_household_secondary_attack_rate(self) -> float: - """[summary] + return { + 'growth_rate': controller.statistics_view.get_growth_rate(), + 'hh_sar': controller_hh_sar.statistics_view.get_hh_sar() + } - Returns: - float: [description] - """ - def evaluate_fit(self, outside_household_infectivity_scaling) -> float: + def evaluate_fit( + self, + household_pairwise_survival_prob, + outside_household_infectivity_scaling) -> float: - return abs(self.desired_growth_rate - self.eval_growth_rate(outside_household_infectivity_scaling)) + metrics = self.eval_metrics( + household_pairwise_survival_prob, + outside_household_infectivity_scaling + ) + + return abs(self.desired_growth_rate - metrics['growth_rate']) + abs(self.desired_hh_sar - metrics['hh_sar']) def optimise(self, outside_household_infectivity_scaling_range: list[float], + household_pairwise_survival_prob_range: list[float], total_trials: int = 20): """Performs the hyperparameter optimization step with proposals from the specified ranges. @@ -141,9 +159,18 @@ def optimise(self, "type": "range", "bounds": outside_household_infectivity_scaling_range, "value_type": "float" + }, + { + "name": "household_pairwise_survival_prob", + "type": "range", + "bounds": household_pairwise_survival_prob_range, + "value_type": "float" } ], - evaluation_function = lambda p: self.evaluate_fit(p["outside_household_infectivity_scaling"]), + evaluation_function = lambda pars: self.evaluate_fit( + household_pairwise_survival_prob = pars["household_pairwise_survival_prob"], + outside_household_infectivity_scaling = pars["outside_household_infectivity_scaling"], + ), minimize = True, total_trials = total_trials ) @@ -152,7 +179,7 @@ def optimise(self, return self.best_parameters, self.values - def get_fitted_growth_rate_samples( + def get_fitted_model_metric_samples( self, n_obs: int = 10) -> list[float]: """If optimisation has been completed, this method generates sample of the growth rate using @@ -169,7 +196,9 @@ def get_fitted_growth_rate_samples( if self.optimisation_complete: return [ - self.eval_growth_rate(self.best_parameters['outside_household_infectivity_scaling']) + self.eval_metrics( + household_pairwise_survival_prob = self.best_parameters['household_pairwise_survival_prob'], + outside_household_infectivity_scaling = self.best_parameters['outside_household_infectivity_scaling']) for _ in range(20) ] From 989a53c7c47457d5cb6387cd337f628240c00899 Mon Sep 17 00:00:00 2001 From: Martyn Fyles Date: Thu, 5 Aug 2021 16:39:42 +0100 Subject: [PATCH 51/70] ease of access method added --- household_contact_tracing/views/statistics_view.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/household_contact_tracing/views/statistics_view.py b/household_contact_tracing/views/statistics_view.py index c76eb2b..e58154a 100644 --- a/household_contact_tracing/views/statistics_view.py +++ b/household_contact_tracing/views/statistics_view.py @@ -222,6 +222,10 @@ def _estimate_household_secondary_attack_rate(self, use_first_generation_only: b # calculating some confidence intervals using the good ol' Jefferys interval self.household_sar_ci = ss.beta.interval(alpha = 0.95, a = self.total_infected + 0.5, b = self.total_exposed - self.total_infected + 0.5) + def get_hh_sar(self): + self._estimate_household_secondary_attack_rate() + return self.household_sar + def household_secondary_attack_rate_summary(self, use_first_generation_only: bool = False, alpha: float = 0.95) -> None: """Estimates the household secondary attack rate, and prints and interpretable output. From cc460aca90c97edd190357668b656650ca7c1e72 Mon Sep 17 00:00:00 2001 From: Martyn Fyles Date: Thu, 5 Aug 2021 19:41:36 +0100 Subject: [PATCH 52/70] Added calibration tutorial --- examples/calibration.ipynb | 861 +++++++++++++++++++++++++++++++++++++ 1 file changed, 861 insertions(+) create mode 100644 examples/calibration.ipynb diff --git a/examples/calibration.ipynb b/examples/calibration.ipynb new file mode 100644 index 0000000..a5b3e33 --- /dev/null +++ b/examples/calibration.ipynb @@ -0,0 +1,861 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 23, + "source": [ + "import household_contact_tracing.branching_process_models as bpm\r\n", + "from household_contact_tracing.branching_process_controller import BranchingProcessController\r\n", + "from household_contact_tracing.calibration import StandardCalibrationHouseholdLevelTracing\r\n", + "import seaborn as sns\r\n", + "import matplotlib.pyplot as plt" + ], + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "# Calibration tutorial" + ], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "This is a quick tutorial on how to calibrate the epidemic models. Household structured branching processes are the underlying mathematical framework, and these have several key metrics that are commonly calibrated:\r\n", + "* The household secondary attack rate (probability that a non-index case in a household gets infected)\r\n", + "* The growth rate of the epidemic\r\n", + "\r\n", + "The household secondary attack rate typically only depends upon the parameter 'household_pairwise_survival_prob', however this could always be varied. The growth rate of the epidemic is more complicated, and this could depends on every parameter in the model. Typically, we calibrate the epidemic model to a growth rate when there is no contact tracing - an unconstrained epidemic model. Without contact tracing, the epidemic growth rate will depend only upon the household secondary attack rate, the outside household infectivity scaling, and the probability that a case reports their infection and isolates (which depends on the symptomatic proportion of cases)." + ], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## Evaluating metrics\r\n", + "\r\n", + "By default, models will be initialised with a statistics view, which is able to estimate several metrics in a model, inclduing the growth rate and the household secondary attack rate.\r\n", + "\r\n", + "We configure some model parameters, set up a model, run the simulation for 25 days, and then use the statistics view to print some summaries of the simulated epidemic." + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 2, + "source": [ + "params = {'outside_household_infectivity_scaling': 0.1,\r\n", + " 'contact_tracing_success_prob': 0.0, # doesn't matter, no tracing\r\n", + " 'overdispersion': 0.32,\r\n", + " 'asymptomatic_prob': 0.2,\r\n", + " 'asymptomatic_relative_infectivity': 0.35,\r\n", + " 'infection_reporting_prob': 0,\r\n", + " 'contact_trace': False,\r\n", + " 'test_delay': 2,\r\n", + " 'contact_trace_delay': 1,\r\n", + " 'incubation_period_delay': 5,\r\n", + " 'symptom_reporting_delay': 1,\r\n", + " 'household_pairwise_survival_prob': 0.2,\r\n", + " 'do_2_step': False, # doesn't matter, no tracing\r\n", + " 'reduce_contacts_by': 0.3,\r\n", + " 'prob_has_trace_app': 0, # doesn't matter, no tracing\r\n", + " 'hh_propensity_to_use_trace_app': 1, # doesn't matter, no tracing\r\n", + " 'test_before_propagate_tracing': True, # doesn't matter, no tracing\r\n", + " 'starting_infections': 100, \r\n", + " 'node_will_uptake_isolation_prob': 1, # doesn't matter, no tracing\r\n", + " 'self_isolation_duration': 0, # doesn't matter, no tracing\r\n", + " 'quarantine_duration': 0, # doesn't matter, no tracing\r\n", + " 'transmission_probability_multiplier': 1,\r\n", + " 'propensity_imperfect_quarantine': 0, # doesn't matter no tracing\r\n", + " 'global_contact_reduction_imperfect_quarantine': 0, # doesn't matter, no tracing\r\n", + "\r\n", + " }" + ], + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 3, + "source": [ + "controller = BranchingProcessController(bpm.HouseholdLevelTracing(params))\r\n", + "controller.graph_view.set_display(False) # don't draw the network\r\n", + "controller.csv_view.set_display(False) # don't save csv info\r\n", + "controller.run_simulation(25, max_active_infections=10000)" + ], + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "When estimating the growth rate, it is good practice to discard the first 10 or so days. It can take some time for the distribution of infectious ages to mix properly, and for contact tracing to be initialised." + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 4, + "source": [ + "controller.statistics_view.growth_rate_summary(discard_first_n_days=10)" + ], + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "15 time periods were used to estimate the growth rate.\n", + "The estimated growth rate was 8.49% (95.0% CI: 7.26-9.72%) per day.\n", + "The estimated doubling time is 8.5 (95.0% CI: 7.47-9.88) days.\n" + ] + } + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 5, + "source": [ + "controller.statistics_view.household_secondary_attack_rate_summary(use_first_generation_only=False)" + ], + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Household secondary attack rate summary:\n", + "151 local household epidemics were eligible to be included.\n", + "All households with completed local epidemics were included. This may lead to a biased sample, as it is possible that local epidemics with a long duration were not included.\n", + "There were 282 non-index susceptible individuals exposed, of which 183 were infected.\n", + "This yields a household secondary attack rate of 65% (95% CI: 59-70%).\n" + ] + } + ], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## Household secondary attack rate simulations" + ], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "Only households with completed local epidemics are included when estimating the household secondary attack rate. This however can lead to biased samples as households with long lasting epidemics might be excluded, and these may be likely to have larger final sizes.\r\n", + "\r\n", + "To get around this a second simulation has been implemented that simulates only the local epidemics, and does not simulate outside household transmission. The simulation is run until all the starting households have completed their local epidemics. This will yield a sample that can be used to obtain an unbiased estiamted of the household secondary attack rate. It is necessary to use a larger number of starting infections to obtain the household secondary attack rate." + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 6, + "source": [ + "params['starting_infections'] = 500 # increasing the number of starting infections will increase the sample size\r\n", + "controller = BranchingProcessController(bpm.HouseholdLevelTracing(params))\r\n", + "controller.graph_view.set_display(False) # don't draw the network\r\n", + "controller.csv_view.set_display(False) # don't save csv info\r\n", + "controller.run_hh_sar_simulation()" + ], + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 7, + "source": [ + "controller.statistics_view.household_secondary_attack_rate_summary(use_first_generation_only=True)" + ], + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Household secondary attack rate summary:\n", + "500 local household epidemics were eligible to be included.\n", + "Only the first generation of the household epidemic was included in this calculation.\n", + "There were 1075 non-index susceptible individuals exposed, of which 833 were infected.\n", + "This yields a household secondary attack rate of 77% (95% CI: 75-80%).\n" + ] + } + ], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "The previous estimate of the household secondary attack rate was around 60%, however using this alternative simulation method we find that the secondary attack rate is estimated to be much closer to 80%. This is why it is important to use the correct method when estimating the household secondary attack rate." + ], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## Calibration" + ], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "We have seen that it is necessary to run two different types of simulations in our to compute two metrics required for calibrating the model. To make our lives easier, we have written several configuration classes. These classes contain methods that will run the model, evaluate the fit of the model, and find the optimal combination of parameters." + ], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "Suppose we want a model with a household secondary attack rate of 25%, and a growth rate of 10% per day." + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 8, + "source": [ + "calibrator = StandardCalibrationHouseholdLevelTracing(\r\n", + " desired_growth_rate=0.1, \r\n", + " desired_hh_sar=0.2, \r\n", + " asymptomatic_prob=0.2, \r\n", + " asymptomatic_relative_infectivity=0.35,\r\n", + " infection_reporting_prob=0.2,\r\n", + " reduce_contacts_by=0.5)" + ], + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "We first perform a trial for two proposed parameters." + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 9, + "source": [ + "calibrator.eval_metrics(\r\n", + " household_pairwise_survival_prob = 0.9,\r\n", + " outside_household_infectivity_scaling = 0.2\r\n", + ")" + ], + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Estimating growth rate using 10 time periods\n" + ] + }, + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "{'growth_rate': 0.026147572887874303, 'hh_sar': 0.11070615034168566}" + ] + }, + "metadata": {}, + "execution_count": 9 + } + ], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "So, we were quite far away. The calibrator class will evaluate the performance of these parameters." + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 10, + "source": [ + "calibrator.evaluate_fit(\r\n", + " household_pairwise_survival_prob = 0.9,\r\n", + " outside_household_infectivity_scaling = 0.2\r\n", + ")" + ], + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Estimating growth rate using 10 time periods\n" + ] + }, + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "0.19315146129441682" + ] + }, + "metadata": {}, + "execution_count": 10 + } + ], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "The household secondary attack rate is too low, we could decrease the household pairwise survival probability to increase the household secondary attack rate." + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 11, + "source": [ + "calibrator.eval_metrics(\r\n", + " household_pairwise_survival_prob = 0.8,\r\n", + " outside_household_infectivity_scaling = 0.2\r\n", + ")" + ], + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Estimating growth rate using 10 time periods\n" + ] + }, + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "{'growth_rate': 0.05384812846666602, 'hh_sar': 0.22884160756501182}" + ] + }, + "metadata": {}, + "execution_count": 11 + } + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 12, + "source": [ + "calibrator.evaluate_fit(\r\n", + " household_pairwise_survival_prob = 0.8,\r\n", + " outside_household_infectivity_scaling = 0.2\r\n", + ")" + ], + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Estimating growth rate using 10 time periods\n" + ] + }, + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "0.07805426370102453" + ] + }, + "metadata": {}, + "execution_count": 12 + } + ], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "The fit has improved, as the return value is smaller. Moving on, let's use the optimisation routine to find the optimal combination of parameters. To do this, we need to supply a range of parameters. These could be set fairly wide, as the expense of requiring more simulation runs to find the optimal combination of parameters. A machine learning hyperparameter optimisation framework will handle the rest. Some of the runs could take a while, particularly if they overshoot the growth rate, as the computation time is related to the size of the epidemic." + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 13, + "source": [ + "calibrator.optimise(\r\n", + " household_pairwise_survival_prob_range = [0.75, 0.95],\r\n", + " outside_household_infectivity_scaling_range = [0.2, 0.4]\r\n", + ")" + ], + "outputs": [ + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 08-05 19:29:29] ax.modelbridge.dispatch_utils: Using GPEI (Bayesian optimization) since there are more continuous parameters than there are categories for the unordered categorical parameters.\n", + "[INFO 08-05 19:29:29] ax.modelbridge.dispatch_utils: Using Bayesian Optimization generation strategy: GenerationStrategy(name='Sobol+GPEI', steps=[Sobol for 5 trials, GPEI for subsequent trials]). Iterations after 5 will take longer to generate due to model-fitting.\n", + "[INFO 08-05 19:29:29] ax.service.managed_loop: Started full optimization with 20 steps.\n", + "[INFO 08-05 19:29:29] ax.service.managed_loop: Running optimization trial 1...\n", + "[INFO 08-05 19:29:33] ax.service.managed_loop: Running optimization trial 2...\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Estimating growth rate using 10 time periods\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 08-05 19:29:37] ax.service.managed_loop: Running optimization trial 3...\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Estimating growth rate using 10 time periods\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 08-05 19:29:42] ax.service.managed_loop: Running optimization trial 4...\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Estimating growth rate using 10 time periods\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 08-05 19:29:47] ax.service.managed_loop: Running optimization trial 5...\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Estimating growth rate using 10 time periods\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 08-05 19:29:52] ax.service.managed_loop: Running optimization trial 6...\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Estimating growth rate using 10 time periods\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 08-05 19:29:58] ax.service.managed_loop: Running optimization trial 7...\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Estimating growth rate using 10 time periods\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 08-05 19:30:03] ax.service.managed_loop: Running optimization trial 8...\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Estimating growth rate using 10 time periods\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 08-05 19:30:08] ax.service.managed_loop: Running optimization trial 9...\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Estimating growth rate using 10 time periods\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 08-05 19:30:12] ax.service.managed_loop: Running optimization trial 10...\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Estimating growth rate using 10 time periods\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 08-05 19:30:15] ax.service.managed_loop: Running optimization trial 11...\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Estimating growth rate using 10 time periods\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 08-05 19:30:20] ax.service.managed_loop: Running optimization trial 12...\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Estimating growth rate using 10 time periods\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 08-05 19:30:24] ax.service.managed_loop: Running optimization trial 13...\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Estimating growth rate using 10 time periods\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 08-05 19:30:29] ax.service.managed_loop: Running optimization trial 14...\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Estimating growth rate using 10 time periods\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 08-05 19:30:34] ax.service.managed_loop: Running optimization trial 15...\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Estimating growth rate using 10 time periods\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 08-05 19:30:39] ax.service.managed_loop: Running optimization trial 16...\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Estimating growth rate using 10 time periods\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 08-05 19:30:44] ax.service.managed_loop: Running optimization trial 17...\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Estimating growth rate using 10 time periods\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 08-05 19:30:50] ax.service.managed_loop: Running optimization trial 18...\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Estimating growth rate using 10 time periods\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 08-05 19:30:53] ax.service.managed_loop: Running optimization trial 19...\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Estimating growth rate using 10 time periods\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[INFO 08-05 19:31:00] ax.service.managed_loop: Running optimization trial 20...\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Estimating growth rate using 10 time periods\n", + "Estimating growth rate using 10 time periods\n" + ] + }, + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "({'outside_household_infectivity_scaling': 0.25710476035578406,\n", + " 'household_pairwise_survival_prob': 0.810841293427738},\n", + " ({'objective': 0.012664750904135401},\n", + " {'objective': {'objective': 1.000745344768751e-06}}))" + ] + }, + "metadata": {}, + "execution_count": 13 + } + ], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "The optimal parameter combinations can be recovered using:" + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 14, + "source": [ + "calibrator.best_parameters" + ], + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "{'outside_household_infectivity_scaling': 0.25710476035578406,\n", + " 'household_pairwise_survival_prob': 0.810841293427738}" + ] + }, + "metadata": {}, + "execution_count": 14 + } + ], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "Finally, we can easily sample the fitting metrics to check that the fit looks good." + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 17, + "source": [ + "outputs = calibrator.get_fitted_model_metric_samples(n_obs = 20)" + ], + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 18, + "source": [ + "growth_rates = [\r\n", + " output['growth_rate'] for output in outputs\r\n", + "]\r\n", + "\r\n", + "hh_sars = [\r\n", + " output['hh_sar'] for output in outputs\r\n", + "]\r\n", + "\r\n", + "outputs" + ], + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "[{'growth_rate': 0.11082584992572188, 'hh_sar': 0.18533157663592445},\n", + " {'growth_rate': 0.10738751955334369, 'hh_sar': 0.21236059479553904},\n", + " {'growth_rate': 0.09084650192266462, 'hh_sar': 0.19239274657231314},\n", + " {'growth_rate': 0.10075203747153494, 'hh_sar': 0.2336119665640123},\n", + " {'growth_rate': 0.10735735237940272, 'hh_sar': 0.2076707202993452},\n", + " {'growth_rate': 0.08313646738902405, 'hh_sar': 0.23312331233123312},\n", + " {'growth_rate': 0.08981749442949848, 'hh_sar': 0.2202781516375056},\n", + " {'growth_rate': 0.09322273991873418, 'hh_sar': 0.21407121407121407},\n", + " {'growth_rate': 0.09289832274649205, 'hh_sar': 0.21107580571947346},\n", + " {'growth_rate': 0.09344663727571464, 'hh_sar': 0.20498614958448755},\n", + " {'growth_rate': 0.096859550802151, 'hh_sar': 0.21289151157512484},\n", + " {'growth_rate': 0.10469928901617155, 'hh_sar': 0.2064975522919448},\n", + " {'growth_rate': 0.10857606282994096, 'hh_sar': 0.20827710301394511},\n", + " {'growth_rate': 0.09836023406359318, 'hh_sar': 0.20940959409594095},\n", + " {'growth_rate': 0.08472873842505456, 'hh_sar': 0.20318181818181819},\n", + " {'growth_rate': 0.0941299371383913, 'hh_sar': 0.2197851387645479},\n", + " {'growth_rate': 0.09565612682725907, 'hh_sar': 0.20922795797167656},\n", + " {'growth_rate': 0.07646034952842615, 'hh_sar': 0.20435967302452315},\n", + " {'growth_rate': 0.09383057360324933, 'hh_sar': 0.17884702678166137},\n", + " {'growth_rate': 0.08110175647926467, 'hh_sar': 0.19276556776556777}]" + ] + }, + "metadata": {}, + "execution_count": 18 + } + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 26, + "source": [ + "sns.displot(growth_rates)" + ], + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "" + ] + }, + "metadata": {}, + "execution_count": 26 + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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" + }, + "metadata": { + "needs_background": "light" + } + } + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 27, + "source": [ + "sns.displot(hh_sars)" + ], + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "" + ] + }, + "metadata": {}, + "execution_count": 27 + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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" + }, + "metadata": { + "needs_background": "light" + } + } + ], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [], + "metadata": {} + } + ], + "metadata": { + "orig_nbformat": 4, + "language_info": { + "name": "python", + "version": "3.9.4", + "mimetype": "text/x-python", + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "pygments_lexer": "ipython3", + "nbconvert_exporter": "python", + "file_extension": ".py" + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3.9.4 64-bit (conda)" + }, + "interpreter": { + "hash": "5e088ab515b9ff01afc6092114ac7786286eca1d23e3587660ba5a8d6e06cf28" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} \ No newline at end of file From bf6f12d2809614816e84794c3a7ce6e89a113615 Mon Sep 17 00:00:00 2001 From: Martyn Fyles Date: Thu, 5 Aug 2021 19:42:16 +0100 Subject: [PATCH 53/70] disabled some code that slowed down calibration --- .../branching_process_models.py | 6 +++--- household_contact_tracing/calibration.py | 17 +++++++++++------ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/household_contact_tracing/branching_process_models.py b/household_contact_tracing/branching_process_models.py index 70df473..72adc18 100644 --- a/household_contact_tracing/branching_process_models.py +++ b/household_contact_tracing/branching_process_models.py @@ -175,7 +175,7 @@ def run_hh_sar_simulation(self) -> None: self._state.switch(RunningState) while type(self.state) is RunningState: - prev_network = deepcopy(self.network) + #prev_network = deepcopy(self.network) # This chunk of code executes a days worth of infections and recoveries, but no tracing self.infection.increment(self.time) @@ -190,8 +190,8 @@ def run_hh_sar_simulation(self) -> None: self.time += 1 # If graph changed, tell parent - if not prev_network == self.network: - BranchingProcessModel.graph_changed(self) + #if not prev_network == self.network: + # BranchingProcessModel.graph_changed(self) # Call parent completed step super()._completed_step_increment() diff --git a/household_contact_tracing/calibration.py b/household_contact_tracing/calibration.py index 5a0e82b..78ca169 100644 --- a/household_contact_tracing/calibration.py +++ b/household_contact_tracing/calibration.py @@ -99,7 +99,8 @@ def eval_metrics( household_pairwise_survival_prob: float, outside_household_infectivity_scaling: float, max_time: int = 20, - max_active_infections: int = 1e5) -> float: + max_active_infections: int = 1e5, + verbose: bool = True) -> float: """Sets up a model, runs it, and returns the evaluated growth rate. Args: @@ -124,7 +125,7 @@ def eval_metrics( controller_hh_sar.run_hh_sar_simulation() return { - 'growth_rate': controller.statistics_view.get_growth_rate(), + 'growth_rate': controller.statistics_view.get_growth_rate(verbose = verbose), 'hh_sar': controller_hh_sar.statistics_view.get_hh_sar() } @@ -132,11 +133,13 @@ def eval_metrics( def evaluate_fit( self, household_pairwise_survival_prob, - outside_household_infectivity_scaling) -> float: + outside_household_infectivity_scaling, + verbose: bool = True) -> float: metrics = self.eval_metrics( household_pairwise_survival_prob, - outside_household_infectivity_scaling + outside_household_infectivity_scaling, + verbose=verbose ) return abs(self.desired_growth_rate - metrics['growth_rate']) + abs(self.desired_hh_sar - metrics['hh_sar']) @@ -170,6 +173,7 @@ def optimise(self, evaluation_function = lambda pars: self.evaluate_fit( household_pairwise_survival_prob = pars["household_pairwise_survival_prob"], outside_household_infectivity_scaling = pars["outside_household_infectivity_scaling"], + verbose = False ), minimize = True, total_trials = total_trials @@ -198,8 +202,9 @@ def get_fitted_model_metric_samples( return [ self.eval_metrics( household_pairwise_survival_prob = self.best_parameters['household_pairwise_survival_prob'], - outside_household_infectivity_scaling = self.best_parameters['outside_household_infectivity_scaling']) - for _ in range(20) + outside_household_infectivity_scaling = self.best_parameters['outside_household_infectivity_scaling'], + verbose = False) + for _ in range(n_obs) ] else: From c7b292d1256d7a913c34cdd8c3086c2cde723f55 Mon Sep 17 00:00:00 2001 From: Martyn Fyles Date: Thu, 5 Aug 2021 19:42:50 +0100 Subject: [PATCH 54/70] minor tweaks to print summaries --- household_contact_tracing/views/statistics_view.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/household_contact_tracing/views/statistics_view.py b/household_contact_tracing/views/statistics_view.py index e58154a..5a276b5 100644 --- a/household_contact_tracing/views/statistics_view.py +++ b/household_contact_tracing/views/statistics_view.py @@ -176,8 +176,8 @@ def growth_rate_summary(self, discard_first_n_days: int = 10, alpha: float = 0.0 doubling_time = np.log(2) / np.log(1 + growth_rate) doubling_time_ci = np.log(2) / np.log(1 + np.array(growth_rate_ci)) - print('GLM regression summary:') - print(self.glm_poisson.summary()) + #print('GLM regression summary:') + #print(self.glm_poisson.summary()) print(f'{num_eligible_dates} time periods were used to estimate the growth rate.') print(f'The estimated growth rate was {round(growth_rate*100, 2)}% ({100*(1-alpha)}% CI: {round(growth_rate_ci[0]*100,2)}-{round(growth_rate_ci[1]*100,2)}%) per day.') print(f'The estimated doubling time is {round(doubling_time, 2)} ({100*(1-alpha)}% CI: {round(doubling_time_ci[1],2)}-{round(doubling_time_ci[0],2)}) days.') @@ -223,7 +223,7 @@ def _estimate_household_secondary_attack_rate(self, use_first_generation_only: b self.household_sar_ci = ss.beta.interval(alpha = 0.95, a = self.total_infected + 0.5, b = self.total_exposed - self.total_infected + 0.5) def get_hh_sar(self): - self._estimate_household_secondary_attack_rate() + self._estimate_household_secondary_attack_rate(use_first_generation_only=True) return self.household_sar def household_secondary_attack_rate_summary(self, use_first_generation_only: bool = False, alpha: float = 0.95) -> None: @@ -236,10 +236,10 @@ def household_secondary_attack_rate_summary(self, use_first_generation_only: boo print('Household secondary attack rate summary:') - print(f'{self.n_households_with_completed_local_epidemics} were eligible to be included.') + print(f'{self.n_households_with_completed_local_epidemics} local household epidemics were eligible to be included.') if use_first_generation_only: print('Only the first generation of the household epidemic was included in this calculation.') else: print('All households with completed local epidemics were included. This may lead to a biased sample, as it is possible that local epidemics with a long duration were not included.') print(f'There were {self.total_exposed} non-index susceptible individuals exposed, of which {self.total_infected} were infected.') - print(f'This yields a household secondary attack rate of {round(self.household_sar*100)}% ({alpha * 100}% CI: ({round(self.household_sar_ci[0]*100)}, {round(self.household_sar_ci[1]*100)})') + print(f'This yields a household secondary attack rate of {round(self.household_sar*100)}% ({int(alpha * 100)}% CI: {round(self.household_sar_ci[0]*100)}-{round(self.household_sar_ci[1]*100)}%).') From 6ba321e0ca1e9d95aa7ce6fbbc56b01cf5af2a87 Mon Sep 17 00:00:00 2001 From: Martyn Fyles Date: Fri, 6 Aug 2021 09:51:12 +0100 Subject: [PATCH 55/70] Changing default parameter value --- household_contact_tracing/branching_process_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/household_contact_tracing/branching_process_models.py b/household_contact_tracing/branching_process_models.py index 322a8cc..d233954 100644 --- a/household_contact_tracing/branching_process_models.py +++ b/household_contact_tracing/branching_process_models.py @@ -172,7 +172,7 @@ def set_default_state_criteria(self): self.state_criteria["infection_threshold"] = 10000 if "max_time" not in self.state_criteria: - self.state_criteria["max_time"] = math.inf + self.state_criteria["max_time"] = 40 if "min_non_recovered_nodes" not in self.state_criteria: self.state_criteria["min_non_recovered_nodes"] = 0 From 060458140dc91c2d34aa31cf2f281d76662b5ce8 Mon Sep 17 00:00:00 2001 From: Martyn Fyles Date: Fri, 6 Aug 2021 10:38:12 +0100 Subject: [PATCH 56/70] reorganised + merge fix --- .../branching_process_models.py | 91 +++++++++---------- 1 file changed, 44 insertions(+), 47 deletions(-) diff --git a/household_contact_tracing/branching_process_models.py b/household_contact_tracing/branching_process_models.py index d233954..ece956d 100644 --- a/household_contact_tracing/branching_process_models.py +++ b/household_contact_tracing/branching_process_models.py @@ -80,6 +80,44 @@ def _initialise_intervention(self): increment.IncrementTracingHouseholdLevel, self.params) + def evaluate_model_state(self, ): + """Determine whether the state of the model has changed by evaluating the data from the last simulation step + against criteria which trigger a change of state.""" + + if self.time >= self.state_criteria["max_time"]: + # Simulation ends if max_time is reached + self.state.switch(TimedOutState, {"total_increments": self.time, + "non_recovered_nodes": self.network.count_non_recovered_nodes(), + "total_nodes": self.network.node_count}) + elif self.network.count_non_recovered_nodes() == self.state_criteria["min_non_recovered_nodes"]: + # Simulation ends if no more infectious nodes + self.state.switch(ExtinctState, {"total_increments": self.time, + "non_recovered_nodes": self.network.count_non_recovered_nodes(), + "total_nodes": self.network.node_count}) + elif self.network.count_non_recovered_nodes() > self.state_criteria["infection_threshold"]: + # Simulation ends if number of infectious nodes > threshold + self.state.switch(MaxNodesInfectiousState, {"total_increments": self.time, + "non_recovered_nodes": 0, + "total_nodes": self.network.node_count}) + + def set_default_state_criteria(self): + """Set default values for the state criteria if they have not yet been set.""" + valid_state_criteria = ["max_time", "min_non_recovered_nodes", "infection_threshold"] + + for criterion in self.state_criteria: + if criterion not in valid_state_criteria: + raise ParameterError(f"Criterion '{criterion}', is not a valid state criterion.\n" + f"Valid state criteria are: {valid_state_criteria}.") + + if "infection_threshold" not in self.state_criteria: + self.state_criteria["infection_threshold"] = 10000 + + if "max_time" not in self.state_criteria: + self.state_criteria["max_time"] = 40 + + if "min_non_recovered_nodes" not in self.state_criteria: + self.state_criteria["min_non_recovered_nodes"] = 0 + def simulate_one_step(self): """Simulates one day of the infection and contact tracing.""" @@ -139,46 +177,7 @@ def run_simulation(self, state_criteria: dict) -> None: # Tell parent simulation stopped super()._simulation_stopped() - def evaluate_model_state(self, ): - """Determine whether the state of the model has changed by evaluating the data from the last simulation step - against criteria which trigger a change of state.""" - - if self.time >= self.state_criteria["max_time"]: - # Simulation ends if max_time is reached - self.state.switch(TimedOutState, {"total_increments": self.time, - "non_recovered_nodes": self.network.count_non_recovered_nodes(), - "total_nodes": self.network.node_count}) - elif self.network.count_non_recovered_nodes() == self.state_criteria["min_non_recovered_nodes"]: - # Simulation ends if no more infectious nodes - self.state.switch(ExtinctState, {"total_increments": self.time, - "non_recovered_nodes": self.network.count_non_recovered_nodes(), - "total_nodes": self.network.node_count}) - elif self.network.count_non_recovered_nodes() > self.state_criteria["infection_threshold"]: - # Simulation ends if number of infectious nodes > threshold - self.state.switch(MaxNodesInfectiousState, {"total_increments": self.time, - "non_recovered_nodes": 0, - "total_nodes": self.network.node_count}) - - def set_default_state_criteria(self): - """Set default values for the state criteria if they have not yet been set.""" - valid_state_criteria = ["max_time", "min_non_recovered_nodes", "infection_threshold"] - - for criterion in self.state_criteria: - if criterion not in valid_state_criteria: - raise ParameterError(f"Criterion '{criterion}', is not a valid state criterion.\n" - f"Valid state criteria are: {valid_state_criteria}.") - - if "infection_threshold" not in self.state_criteria: - self.state_criteria["infection_threshold"] = 10000 - - if "max_time" not in self.state_criteria: - self.state_criteria["max_time"] = 40 - - if "min_non_recovered_nodes" not in self.state_criteria: - self.state_criteria["min_non_recovered_nodes"] = 0 - - - def run_hh_sar_simulation(self) -> None: + def run_hh_sar_simulation(self, state_criteria: dict) -> None: """ Runs the simulation only for the first generation of the household epidemic: Sets model state, Announces start/stopped and step increments to observers @@ -193,9 +192,12 @@ def run_hh_sar_simulation(self) -> None: Returns: None """ + self.state_criteria = state_criteria + + self.set_default_state_criteria() # Switch model to RunningState - self._state.switch(RunningState) + self._state.switch(RunningState, self.state_criteria) while type(self.state) is RunningState: #prev_network = deepcopy(self.network) @@ -220,12 +222,7 @@ def run_hh_sar_simulation(self) -> None: super()._completed_step_increment() # the simulation ends when all nodes in the initial generation have recovered - if self.network.count_non_recovered_nodes() == 0: - # Simulation ends if no more infectious nodes - self.state.switch(ExtinctState, - total_increments=self.time, - non_recovered_nodes=0, - total_nodes=self.network.node_count) + self.evaluate_model_state() # Tell parent simulation stopped super()._simulation_stopped() From bfaea8bffb24c887e46b9b656a91e0b0c12b7a08 Mon Sep 17 00:00:00 2001 From: Martyn Fyles Date: Fri, 6 Aug 2021 10:38:44 +0100 Subject: [PATCH 57/70] Merge fixes and tweaks --- examples/calibration.ipynb | 400 ++++-------------- .../branching_process_controller.py | 4 +- household_contact_tracing/calibration.py | 30 +- 3 files changed, 109 insertions(+), 325 deletions(-) diff --git a/examples/calibration.ipynb b/examples/calibration.ipynb index a5b3e33..0b07397 100644 --- a/examples/calibration.ipynb +++ b/examples/calibration.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 23, + "execution_count": 2, "source": [ "import household_contact_tracing.branching_process_models as bpm\r\n", "from household_contact_tracing.branching_process_controller import BranchingProcessController\r\n", @@ -44,7 +44,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "source": [ "params = {'outside_household_infectivity_scaling': 0.1,\r\n", " 'contact_tracing_success_prob': 0.0, # doesn't matter, no tracing\r\n", @@ -78,7 +78,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "source": [ "controller = BranchingProcessController(bpm.HouseholdLevelTracing(params))\r\n", "controller.graph_view.set_display(False) # don't draw the network\r\n", @@ -97,7 +97,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "source": [ "controller.statistics_view.growth_rate_summary(discard_first_n_days=10)" ], @@ -107,8 +107,8 @@ "name": "stdout", "text": [ "15 time periods were used to estimate the growth rate.\n", - "The estimated growth rate was 8.49% (95.0% CI: 7.26-9.72%) per day.\n", - "The estimated doubling time is 8.5 (95.0% CI: 7.47-9.88) days.\n" + "The estimated growth rate was 8.57% (95.0% CI: 7.5-9.64%) per day.\n", + "The estimated doubling time is 8.43 (95.0% CI: 7.53-9.59) days.\n" ] } ], @@ -116,7 +116,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "source": [ "controller.statistics_view.household_secondary_attack_rate_summary(use_first_generation_only=False)" ], @@ -126,10 +126,10 @@ "name": "stdout", "text": [ "Household secondary attack rate summary:\n", - "151 local household epidemics were eligible to be included.\n", + "173 local household epidemics were eligible to be included.\n", "All households with completed local epidemics were included. This may lead to a biased sample, as it is possible that local epidemics with a long duration were not included.\n", - "There were 282 non-index susceptible individuals exposed, of which 183 were infected.\n", - "This yields a household secondary attack rate of 65% (95% CI: 59-70%).\n" + "There were 312 non-index susceptible individuals exposed, of which 193 were infected.\n", + "This yields a household secondary attack rate of 62% (95% CI: 56-67%).\n" ] } ], @@ -153,7 +153,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "source": [ "params['starting_infections'] = 500 # increasing the number of starting infections will increase the sample size\r\n", "controller = BranchingProcessController(bpm.HouseholdLevelTracing(params))\r\n", @@ -166,7 +166,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "source": [ "controller.statistics_view.household_secondary_attack_rate_summary(use_first_generation_only=True)" ], @@ -176,10 +176,10 @@ "name": "stdout", "text": [ "Household secondary attack rate summary:\n", - "500 local household epidemics were eligible to be included.\n", + "499 local household epidemics were eligible to be included.\n", "Only the first generation of the household epidemic was included in this calculation.\n", - "There were 1075 non-index susceptible individuals exposed, of which 833 were infected.\n", - "This yields a household secondary attack rate of 77% (95% CI: 75-80%).\n" + "There were 1053 non-index susceptible individuals exposed, of which 809 were infected.\n", + "This yields a household secondary attack rate of 77% (95% CI: 74-79%).\n" ] } ], @@ -215,7 +215,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "source": [ "calibrator = StandardCalibrationHouseholdLevelTracing(\r\n", " desired_growth_rate=0.1, \r\n", @@ -237,7 +237,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 10, "source": [ "calibrator.eval_metrics(\r\n", " household_pairwise_survival_prob = 0.9,\r\n", @@ -249,18 +249,18 @@ "output_type": "stream", "name": "stdout", "text": [ - "Estimating growth rate using 10 time periods\n" + "Estimating growth rate using 30 time periods\n" ] }, { "output_type": "execute_result", "data": { "text/plain": [ - "{'growth_rate': 0.026147572887874303, 'hh_sar': 0.11070615034168566}" + "{'growth_rate': 0.0034215244104689423, 'hh_sar': 0.10606060606060606}" ] }, "metadata": {}, - "execution_count": 9 + "execution_count": 10 } ], "metadata": {} @@ -274,7 +274,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 11, "source": [ "calibrator.evaluate_fit(\r\n", " household_pairwise_survival_prob = 0.9,\r\n", @@ -286,18 +286,18 @@ "output_type": "stream", "name": "stdout", "text": [ - "Estimating growth rate using 10 time periods\n" + "Estimating growth rate using 30 time periods\n" ] }, { "output_type": "execute_result", "data": { "text/plain": [ - "0.19315146129441682" + "0.18708774795757915" ] }, "metadata": {}, - "execution_count": 10 + "execution_count": 11 } ], "metadata": {} @@ -311,7 +311,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 12, "source": [ "calibrator.eval_metrics(\r\n", " household_pairwise_survival_prob = 0.8,\r\n", @@ -323,25 +323,25 @@ "output_type": "stream", "name": "stdout", "text": [ - "Estimating growth rate using 10 time periods\n" + "Estimating growth rate using 30 time periods\n" ] }, { "output_type": "execute_result", "data": { "text/plain": [ - "{'growth_rate': 0.05384812846666602, 'hh_sar': 0.22884160756501182}" + "{'growth_rate': 0.0653517347589822, 'hh_sar': 0.22484134179510426}" ] }, "metadata": {}, - "execution_count": 11 + "execution_count": 12 } ], "metadata": {} }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 13, "source": [ "calibrator.evaluate_fit(\r\n", " household_pairwise_survival_prob = 0.8,\r\n", @@ -353,18 +353,18 @@ "output_type": "stream", "name": "stdout", "text": [ - "Estimating growth rate using 10 time periods\n" + "Estimating growth rate using 30 time periods\n" ] }, { "output_type": "execute_result", "data": { "text/plain": [ - "0.07805426370102453" + "0.066385683081691" ] }, "metadata": {}, - "execution_count": 12 + "execution_count": 13 } ], "metadata": {} @@ -378,11 +378,15 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 14, "source": [ "calibrator.optimise(\r\n", " household_pairwise_survival_prob_range = [0.75, 0.95],\r\n", - " outside_household_infectivity_scaling_range = [0.2, 0.4]\r\n", + " outside_household_infectivity_scaling_range = [0.2, 0.4],\r\n", + " state_criteria={\r\n", + " 'max_time': 25,\r\n", + " 'infection_threshold': 1e5\r\n", + " }\r\n", ")" ], "outputs": [ @@ -390,285 +394,43 @@ "output_type": "stream", "name": "stderr", "text": [ - "[INFO 08-05 19:29:29] ax.modelbridge.dispatch_utils: Using GPEI (Bayesian optimization) since there are more continuous parameters than there are categories for the unordered categorical parameters.\n", - "[INFO 08-05 19:29:29] ax.modelbridge.dispatch_utils: Using Bayesian Optimization generation strategy: GenerationStrategy(name='Sobol+GPEI', steps=[Sobol for 5 trials, GPEI for subsequent trials]). Iterations after 5 will take longer to generate due to model-fitting.\n", - "[INFO 08-05 19:29:29] ax.service.managed_loop: Started full optimization with 20 steps.\n", - "[INFO 08-05 19:29:29] ax.service.managed_loop: Running optimization trial 1...\n", - "[INFO 08-05 19:29:33] ax.service.managed_loop: Running optimization trial 2...\n" - ] - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Estimating growth rate using 10 time periods\n" - ] - }, - { - "output_type": "stream", - "name": "stderr", - "text": [ - "[INFO 08-05 19:29:37] ax.service.managed_loop: Running optimization trial 3...\n" - ] - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Estimating growth rate using 10 time periods\n" - ] - }, - { - "output_type": "stream", - "name": "stderr", - "text": [ - "[INFO 08-05 19:29:42] ax.service.managed_loop: Running optimization trial 4...\n" - ] - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Estimating growth rate using 10 time periods\n" - ] - }, - { - "output_type": "stream", - "name": "stderr", - "text": [ - "[INFO 08-05 19:29:47] ax.service.managed_loop: Running optimization trial 5...\n" - ] - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Estimating growth rate using 10 time periods\n" - ] - }, - { - "output_type": "stream", - "name": "stderr", - "text": [ - "[INFO 08-05 19:29:52] ax.service.managed_loop: Running optimization trial 6...\n" - ] - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Estimating growth rate using 10 time periods\n" - ] - }, - { - "output_type": "stream", - "name": "stderr", - "text": [ - "[INFO 08-05 19:29:58] ax.service.managed_loop: Running optimization trial 7...\n" - ] - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Estimating growth rate using 10 time periods\n" - ] - }, - { - "output_type": "stream", - "name": "stderr", - "text": [ - "[INFO 08-05 19:30:03] ax.service.managed_loop: Running optimization trial 8...\n" - ] - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Estimating growth rate using 10 time periods\n" - ] - }, - { - "output_type": "stream", - "name": "stderr", - "text": [ - "[INFO 08-05 19:30:08] ax.service.managed_loop: Running optimization trial 9...\n" - ] - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Estimating growth rate using 10 time periods\n" - ] - }, - { - "output_type": "stream", - "name": "stderr", - "text": [ - "[INFO 08-05 19:30:12] ax.service.managed_loop: Running optimization trial 10...\n" - ] - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Estimating growth rate using 10 time periods\n" - ] - }, - { - "output_type": "stream", - "name": "stderr", - "text": [ - "[INFO 08-05 19:30:15] ax.service.managed_loop: Running optimization trial 11...\n" - ] - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Estimating growth rate using 10 time periods\n" - ] - }, - { - "output_type": "stream", - "name": "stderr", - "text": [ - "[INFO 08-05 19:30:20] ax.service.managed_loop: Running optimization trial 12...\n" - ] - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Estimating growth rate using 10 time periods\n" - ] - }, - { - "output_type": "stream", - "name": "stderr", - "text": [ - "[INFO 08-05 19:30:24] ax.service.managed_loop: Running optimization trial 13...\n" - ] - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Estimating growth rate using 10 time periods\n" - ] - }, - { - "output_type": "stream", - "name": "stderr", - "text": [ - "[INFO 08-05 19:30:29] ax.service.managed_loop: Running optimization trial 14...\n" - ] - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Estimating growth rate using 10 time periods\n" - ] - }, - { - "output_type": "stream", - "name": "stderr", - "text": [ - "[INFO 08-05 19:30:34] ax.service.managed_loop: Running optimization trial 15...\n" - ] - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Estimating growth rate using 10 time periods\n" - ] - }, - { - "output_type": "stream", - "name": "stderr", - "text": [ - "[INFO 08-05 19:30:39] ax.service.managed_loop: Running optimization trial 16...\n" - ] - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Estimating growth rate using 10 time periods\n" - ] - }, - { - "output_type": "stream", - "name": "stderr", - "text": [ - "[INFO 08-05 19:30:44] ax.service.managed_loop: Running optimization trial 17...\n" - ] - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Estimating growth rate using 10 time periods\n" - ] - }, - { - "output_type": "stream", - "name": "stderr", - "text": [ - "[INFO 08-05 19:30:50] ax.service.managed_loop: Running optimization trial 18...\n" - ] - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Estimating growth rate using 10 time periods\n" - ] - }, - { - "output_type": "stream", - "name": "stderr", - "text": [ - "[INFO 08-05 19:30:53] ax.service.managed_loop: Running optimization trial 19...\n" - ] - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Estimating growth rate using 10 time periods\n" - ] - }, - { - "output_type": "stream", - "name": "stderr", - "text": [ - "[INFO 08-05 19:31:00] ax.service.managed_loop: Running optimization trial 20...\n" - ] - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Estimating growth rate using 10 time periods\n", - "Estimating growth rate using 10 time periods\n" + "[INFO 08-06 10:14:42] ax.modelbridge.dispatch_utils: Using GPEI (Bayesian optimization) since there are more continuous parameters than there are categories for the unordered categorical parameters.\n", + "[INFO 08-06 10:14:42] ax.modelbridge.dispatch_utils: Using Bayesian Optimization generation strategy: GenerationStrategy(name='Sobol+GPEI', steps=[Sobol for 5 trials, GPEI for subsequent trials]). Iterations after 5 will take longer to generate due to model-fitting.\n", + "[INFO 08-06 10:14:42] ax.service.managed_loop: Started full optimization with 20 steps.\n", + "[INFO 08-06 10:14:42] ax.service.managed_loop: Running optimization trial 1...\n", + "[INFO 08-06 10:14:49] ax.service.managed_loop: Running optimization trial 2...\n", + "[INFO 08-06 10:14:56] ax.service.managed_loop: Running optimization trial 3...\n", + "[INFO 08-06 10:15:05] ax.service.managed_loop: Running optimization trial 4...\n", + "[INFO 08-06 10:15:10] ax.service.managed_loop: Running optimization trial 5...\n", + "[INFO 08-06 10:15:16] ax.service.managed_loop: Running optimization trial 6...\n", + "[INFO 08-06 10:15:24] ax.service.managed_loop: Running optimization trial 7...\n", + "[INFO 08-06 10:15:29] ax.service.managed_loop: Running optimization trial 8...\n", + "[INFO 08-06 10:15:32] ax.service.managed_loop: Running optimization trial 9...\n", + "[INFO 08-06 10:15:37] ax.service.managed_loop: Running optimization trial 10...\n", + "[INFO 08-06 10:15:41] ax.service.managed_loop: Running optimization trial 11...\n", + "[INFO 08-06 10:15:47] ax.service.managed_loop: Running optimization trial 12...\n", + "[INFO 08-06 10:15:52] ax.service.managed_loop: Running optimization trial 13...\n", + "[INFO 08-06 10:16:02] ax.service.managed_loop: Running optimization trial 14...\n", + "[INFO 08-06 10:16:06] ax.service.managed_loop: Running optimization trial 15...\n", + "[INFO 08-06 10:16:10] ax.service.managed_loop: Running optimization trial 16...\n", + "[INFO 08-06 10:16:15] ax.service.managed_loop: Running optimization trial 17...\n", + "[INFO 08-06 10:16:21] ax.service.managed_loop: Running optimization trial 18...\n", + "[INFO 08-06 10:16:27] ax.service.managed_loop: Running optimization trial 19...\n", + "[INFO 08-06 10:16:31] ax.service.managed_loop: Running optimization trial 20...\n" ] }, { "output_type": "execute_result", "data": { "text/plain": [ - "({'outside_household_infectivity_scaling': 0.25710476035578406,\n", - " 'household_pairwise_survival_prob': 0.810841293427738},\n", - " ({'objective': 0.012664750904135401},\n", - " {'objective': {'objective': 1.000745344768751e-06}}))" + "({'outside_household_infectivity_scaling': 0.2369218112772867,\n", + " 'household_pairwise_survival_prob': 0.8104007705085332},\n", + " ({'objective': 0.026804953615459474},\n", + " {'objective': {'objective': 7.980087959078822e-06}}))" ] }, "metadata": {}, - "execution_count": 13 + "execution_count": 14 } ], "metadata": {} @@ -682,7 +444,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 15, "source": [ "calibrator.best_parameters" ], @@ -691,12 +453,12 @@ "output_type": "execute_result", "data": { "text/plain": [ - "{'outside_household_infectivity_scaling': 0.25710476035578406,\n", - " 'household_pairwise_survival_prob': 0.810841293427738}" + "{'outside_household_infectivity_scaling': 0.2369218112772867,\n", + " 'household_pairwise_survival_prob': 0.8104007705085332}" ] }, "metadata": {}, - "execution_count": 14 + "execution_count": 15 } ], "metadata": {} @@ -712,14 +474,26 @@ "cell_type": "code", "execution_count": 17, "source": [ - "outputs = calibrator.get_fitted_model_metric_samples(n_obs = 20)" + "outputs = calibrator.get_fitted_model_metric_samples(n_obs = 20, state_criteria={'max_time': 25})" + ], + "outputs": [ + { + "output_type": "error", + "ename": "TypeError", + "evalue": "get_fitted_model_metric_samples() got an unexpected keyword argument 'state_criteria'", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mTypeError\u001b[0m Traceback (most recent call last)", + "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[0moutputs\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mcalibrator\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mget_fitted_model_metric_samples\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mn_obs\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;36m20\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mstate_criteria\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;33m{\u001b[0m\u001b[1;34m'max_time'\u001b[0m\u001b[1;33m:\u001b[0m \u001b[1;36m25\u001b[0m\u001b[1;33m}\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[1;31mTypeError\u001b[0m: get_fitted_model_metric_samples() got an unexpected keyword argument 'state_criteria'" + ] + } ], - "outputs": [], "metadata": {} }, { "cell_type": "code", - "execution_count": 18, + "execution_count": null, "source": [ "growth_rates = [\r\n", " output['growth_rate'] for output in outputs\r\n", @@ -766,7 +540,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": null, "source": [ "sns.displot(growth_rates)" ], @@ -798,7 +572,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": null, "source": [ "sns.displot(hh_sars)" ], diff --git a/household_contact_tracing/branching_process_controller.py b/household_contact_tracing/branching_process_controller.py index c7b5545..11b5c8a 100644 --- a/household_contact_tracing/branching_process_controller.py +++ b/household_contact_tracing/branching_process_controller.py @@ -104,7 +104,7 @@ def run_simulation(self, state_criteria: dict): self._model.run_simulation(state_criteria) - def run_hh_sar_simulation(self): + def run_hh_sar_simulation(self, state_criteria: dict = {}): """ This simulation method with only simulate the infection process for households in the first generation of the epidemic, and will continue until all nodes in the initial households of the @@ -116,6 +116,6 @@ def run_hh_sar_simulation(self): Returns: None """ - self._model.run_hh_sar_simulation() + self._model.run_hh_sar_simulation(state_criteria = state_criteria) diff --git a/household_contact_tracing/calibration.py b/household_contact_tracing/calibration.py index 78ca169..5bd1b33 100644 --- a/household_contact_tracing/calibration.py +++ b/household_contact_tracing/calibration.py @@ -9,7 +9,7 @@ from household_contact_tracing.branching_process_controller import BranchingProcessController from ax import optimize from copy import Error, copy - +import math class Calibration(ABC): """ @@ -98,8 +98,7 @@ def eval_metrics( self, household_pairwise_survival_prob: float, outside_household_infectivity_scaling: float, - max_time: int = 20, - max_active_infections: int = 1e5, + state_criteria: dict = {}, verbose: bool = True) -> float: """Sets up a model, runs it, and returns the evaluated growth rate. @@ -116,13 +115,18 @@ def eval_metrics( # run a simulation to get the growth rate of the epidemic controller = BranchingProcessController(HouseholdLevelTracing(params)) controller.csv_view.set_display(False) - controller.run_simulation(max_time, max_active_infections) + controller.run_simulation(state_criteria) # use a different simulation method to get the household secondary attack rate of the epidemic params['starting_infections'] = self.starting_infections_hh_sar # use a higher number of starting infections controller_hh_sar = BranchingProcessController(HouseholdLevelTracing(params)) controller_hh_sar.csv_view.set_display(False) - controller_hh_sar.run_hh_sar_simulation() + controller_hh_sar.run_hh_sar_simulation( + state_criteria = { + 'infection_threshold': math.inf, + 'max_time': math.inf + } + ) return { 'growth_rate': controller.statistics_view.get_growth_rate(verbose = verbose), @@ -134,12 +138,14 @@ def evaluate_fit( self, household_pairwise_survival_prob, outside_household_infectivity_scaling, - verbose: bool = True) -> float: + verbose: bool = True, + state_criteria: dict = {}) -> float: metrics = self.eval_metrics( household_pairwise_survival_prob, outside_household_infectivity_scaling, - verbose=verbose + state_criteria, + verbose ) return abs(self.desired_growth_rate - metrics['growth_rate']) + abs(self.desired_hh_sar - metrics['hh_sar']) @@ -147,7 +153,8 @@ def evaluate_fit( def optimise(self, outside_household_infectivity_scaling_range: list[float], household_pairwise_survival_prob_range: list[float], - total_trials: int = 20): + total_trials: int = 20, + state_criteria: dict = {}): """Performs the hyperparameter optimization step with proposals from the specified ranges. Args: @@ -173,7 +180,8 @@ def optimise(self, evaluation_function = lambda pars: self.evaluate_fit( household_pairwise_survival_prob = pars["household_pairwise_survival_prob"], outside_household_infectivity_scaling = pars["outside_household_infectivity_scaling"], - verbose = False + verbose = False, + state_criteria = state_criteria ), minimize = True, total_trials = total_trials @@ -185,7 +193,8 @@ def optimise(self, def get_fitted_model_metric_samples( self, - n_obs: int = 10) -> list[float]: + n_obs: int = 10, + state_criteria: dict = {}) -> list[dict]: """If optimisation has been completed, this method generates sample of the growth rate using the results from the optimisation step. @@ -203,6 +212,7 @@ def get_fitted_model_metric_samples( self.eval_metrics( household_pairwise_survival_prob = self.best_parameters['household_pairwise_survival_prob'], outside_household_infectivity_scaling = self.best_parameters['outside_household_infectivity_scaling'], + state_criteria = state_criteria, verbose = False) for _ in range(n_obs) ] From ec720deaa8ed638c0155892854c12ad868b6e682 Mon Sep 17 00:00:00 2001 From: Martyn Fyles Date: Fri, 6 Aug 2021 11:06:58 +0100 Subject: [PATCH 58/70] Moving some attributes around --- household_contact_tracing/node_attributes.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/household_contact_tracing/node_attributes.py b/household_contact_tracing/node_attributes.py index eefd381..3c10e2d 100644 --- a/household_contact_tracing/node_attributes.py +++ b/household_contact_tracing/node_attributes.py @@ -1,10 +1,6 @@ from typing import Optional - from household_contact_tracing.parameterised import Parameterised -# Todo: @Martyn Please check - - class InfectionAttributes(Parameterised): """ A class used to store Node attributes relating to infection @@ -52,10 +48,6 @@ class LFDTestingAttributes(Parameterised): positive_test_time (int) taken_confirmatory_PCR_test (boolean) time_started_lfa_testing (int) - - # Todo @Martyn: Ann estimated placing these here - CHECK - propensity_risky_behaviour_lfa_testing (float) - propensity_to_miss_lfa_tests (float) confirmatory_PCR_test_result_time (float) completed_lateral_flow_testing_time (boolean) lateral_flow_testing_duration (float) @@ -71,10 +63,6 @@ def __init__(self, attributes): self.positive_test_time = None self.taken_confirmatory_PCR_test = None self.time_started_lfa_testing = None - - # Todo Ann estimated placing these here - CHECK - self.propensity_risky_behaviour_lfa_testing = None - self.propensity_to_miss_lfa_tests = None self.confirmatory_PCR_test_result_time = None self.completed_lateral_flow_testing_time = None self.lateral_flow_testing_duration = 0 @@ -102,6 +90,8 @@ class LFDTestingAdherenceAttributes(Parameterised): def __init__(self, attributes): self.confirmatory_PCR_result_was_positive: Optional[bool] = None self.node_will_take_up_lfa_testing = None + self.propensity_risky_behaviour_lfa_testing = None + self.propensity_to_miss_lfa_tests = None # Update instance variables with anything in attributes self.update_params(attributes) From 1f5e2fa4cab1f63064a367d237c10f4903a469e1 Mon Sep 17 00:00:00 2001 From: Martyn Fyles Date: Fri, 6 Aug 2021 11:07:23 +0100 Subject: [PATCH 59/70] Updating to reflect moved parameters --- .../behaviours/infection/contact_rate_reduction.py | 2 +- test/test_TestingContactModel.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/household_contact_tracing/behaviours/infection/contact_rate_reduction.py b/household_contact_tracing/behaviours/infection/contact_rate_reduction.py index 1f09a46..0470c0e 100644 --- a/household_contact_tracing/behaviours/infection/contact_rate_reduction.py +++ b/household_contact_tracing/behaviours/infection/contact_rate_reduction.py @@ -89,7 +89,7 @@ def get_contact_rate_reduction(self, node: Node) -> int: # imperfect intervention return self.global_contact_reduction_imperfect_quarantine - elif node.lfd_testing.being_lateral_flow_tested and node.lfd_testing.propensity_risky_behaviour_lfa_testing: + elif node.lfd_testing.being_lateral_flow_tested and node.lfd_testing_adherence.propensity_risky_behaviour_lfa_testing: # engaging in risky behaviour while testing negative return self.global_contact_reduction_risky_behaviour diff --git a/test/test_TestingContactModel.py b/test/test_TestingContactModel.py index 252bebc..61fb1eb 100644 --- a/test/test_TestingContactModel.py +++ b/test/test_TestingContactModel.py @@ -396,7 +396,7 @@ def test_risky_behaviour_attributes_default(simple_model: simple_model): """Tests that the default behaviour is no more risky behaviour """ - assert not simple_model.network.node(1).lfd_testing.propensity_risky_behaviour_lfa_testing + assert not simple_model.network.node(1).lfd_testing_adherence.propensity_risky_behaviour_lfa_testing def test_risky_behaviour_attributes(simple_model_risky_behaviour: simple_model_risky_behaviour): @@ -404,7 +404,7 @@ def test_risky_behaviour_attributes(simple_model_risky_behaviour: simple_model_r being tested. """ - assert simple_model_risky_behaviour.network.node(1).lfd_testing.propensity_risky_behaviour_lfa_testing + assert simple_model_risky_behaviour.network.node(1).lfd_testing_adherence.propensity_risky_behaviour_lfa_testing @pytest.fixture @@ -458,7 +458,7 @@ def test_lfa_tested_nodes_make_more_contacts_if_risky( model = simple_model_risky_behaviour_2_infections - model.network.node(1).lfd_testing.propensity_risky_behaviour_lfa_testing = False + model.network.node(1).lfd_testing_adherence.propensity_risky_behaviour_lfa_testing = False # stop there being any within household infections # not sure if this is strictly necessary From 9f3a26f05ca21b0dd0bb588aa8dc579bad6404ce Mon Sep 17 00:00:00 2001 From: annannfryingpan Date: Fri, 6 Aug 2021 12:22:26 +0100 Subject: [PATCH 60/70] Corrections to move 2 attributes to LFDTestingAdherence, from LFDTesting --- .../behaviours/infection/new_infection.py | 37 ++++++++++--------- household_contact_tracing/network.py | 2 +- .../lfd_testing_adherence.json | 12 +++++- 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/household_contact_tracing/behaviours/infection/new_infection.py b/household_contact_tracing/behaviours/infection/new_infection.py index eab3493..dacfb85 100644 --- a/household_contact_tracing/behaviours/infection/new_infection.py +++ b/household_contact_tracing/behaviours/infection/new_infection.py @@ -169,11 +169,6 @@ def new_infection(self, time: int, household: Household, infecting_node: Optiona else: node_is_isolated = False - tracing_adherence_attributes = {'will_uptake_isolation': isolation_uptake, - 'propensity_imperfect_isolation': self.get_propensity_imperfect_isolation() - } - returning_travellers_attributes = {'pseudo_symptom_onset_time': pseudo_symptom_onset_time} - tracing_attributes = { 'contact_traced': household.contact_traced, 'has_contact_tracing_app': has_trace_app, @@ -183,6 +178,12 @@ def new_infection(self, time: int, household: Household, infecting_node: Optiona 'will_report_infection': will_report_infection, } + tracing_adherence_attributes = {'will_uptake_isolation': isolation_uptake, + 'propensity_imperfect_isolation': self.get_propensity_imperfect_isolation() + } + + returning_travellers_attributes = {'pseudo_symptom_onset_time': pseudo_symptom_onset_time} + infecting_node_id = None if infecting_node: infecting_node_id = infecting_node.id @@ -273,18 +274,6 @@ def new_infection(self, time: int, household: Household, infecting_node: Optiona else: node_is_isolated = False - lfd_testing_adherence_attributes = { - 'node_will_take_up_lfa_testing': node_will_take_up_lfa_testing, - 'confirmatory_PCR_result_was_positive': None, - } - - tracing_adherence_attributes = { - 'will_uptake_isolation': isolation_uptake, - 'propensity_imperfect_isolation': self.get_propensity_imperfect_isolation() - } - - returning_travellers_attributes = {'pseudo_symptom_onset_time': pseudo_symptom_onset_time} - lfd_testing_attributes = { 'avenue_of_testing': None, 'being_lateral_flow_tested': node_being_lateral_flow_tested, @@ -292,7 +281,12 @@ def new_infection(self, time: int, household: Household, infecting_node: Optiona 'positive_test_time': None, 'taken_confirmatory_PCR_test': False, 'confirmatory_PCR_test_time': None, - 'confirmatory_PCR_test_result_time': None, + 'confirmatory_PCR_test_result_time': None + } + + lfd_testing_adherence_attributes = { + 'node_will_take_up_lfa_testing': node_will_take_up_lfa_testing, + 'confirmatory_PCR_result_was_positive': None, 'propensity_risky_behaviour_lfa_testing': self.will_engage_in_risky_behaviour_while_being_lfa_tested(), 'propensity_to_miss_lfa_tests': self.propensity_to_miss_lfa_tests() } @@ -308,6 +302,13 @@ def new_infection(self, time: int, household: Household, infecting_node: Optiona 'will_report_infection': will_report_infection, } + tracing_adherence_attributes = { + 'will_uptake_isolation': isolation_uptake, + 'propensity_imperfect_isolation': self.get_propensity_imperfect_isolation() + } + + returning_travellers_attributes = {'pseudo_symptom_onset_time': pseudo_symptom_onset_time} + infecting_node_id = None if infecting_node: infecting_node_id = infecting_node.id diff --git a/household_contact_tracing/network.py b/household_contact_tracing/network.py index 1a8f678..bbebcf8 100644 --- a/household_contact_tracing/network.py +++ b/household_contact_tracing/network.py @@ -351,7 +351,7 @@ def take_confirmatory_pcr_test(self, time: int, prob_pcr_positive: Callable): def will_lfa_test_today(self, daily_prob_lfa_test: float) -> bool: """Determine whether a node will do an LFT test today.""" - if not self.lfd_testing.propensity_to_miss_lfa_tests: + if not self.lfd_testing_adherence.propensity_to_miss_lfa_tests: return True if numpy.random.binomial(1, daily_prob_lfa_test) == 1: diff --git a/household_contact_tracing/schemas/node_attributes/lfd_testing_adherence.json b/household_contact_tracing/schemas/node_attributes/lfd_testing_adherence.json index dc17c15..7600cb2 100644 --- a/household_contact_tracing/schemas/node_attributes/lfd_testing_adherence.json +++ b/household_contact_tracing/schemas/node_attributes/lfd_testing_adherence.json @@ -5,7 +5,9 @@ "type": "object", "anyOf": [ {"required": ["confirmatory_PCR_result_was_positive"]}, - {"required": ["node_will_take_up_LFA_testing"]} + {"required": ["node_will_take_up_LFA_testing"]}, + {"required": ["propensity_to_miss_lfa_tests"]}, + {"required": ["propensity_risky_behaviour_lfa_testing"]} ], "properties": { "confirmatory_PCR_result_was_positive": { @@ -15,6 +17,14 @@ "node_will_take_up_LFA_testing": { "description": "Will the node take up LFA testing?", "type": "boolean" + }, + "propensity_to_miss_lfa_tests": { + "description": "Propensity of node to miss LFA tests", + "type": ["number", "null"] + }, + "propensity_risky_behaviour_lfa_testing": { + "description": "Propensity of node to engage in risky behaviour during LFA testing", + "type": ["number", "null"] } } } From 8b43dcf030c26c88800e28730672d8b501a04364 Mon Sep 17 00:00:00 2001 From: Peter Crowther <13117069+merrygoat@users.noreply.github.com> Date: Mon, 9 Aug 2021 11:20:28 +0100 Subject: [PATCH 61/70] Fix a Node attribute --- household_contact_tracing/network.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/household_contact_tracing/network.py b/household_contact_tracing/network.py index 8555e85..0eea059 100644 --- a/household_contact_tracing/network.py +++ b/household_contact_tracing/network.py @@ -5,7 +5,6 @@ import numpy from dataclasses import dataclass -from household_contact_tracing.parameterised import Parameterised from household_contact_tracing.node_attributes import LFDTestingAdherenceAttributes, TracingAdherenceAttributes, \ ReturningTravellerAttributes, LFDTestingAttributes, TracingAttributes, InfectionAttributes @@ -235,7 +234,7 @@ def label_edges_inside_household(self, household: Household, new_edge_type: Edge self.graph.edges[edge[0], edge[1]].update({"edge_type": new_edge_type}) -class Node(Parameterised): +class Node: """ A class used to store contact tracing node data. Uses networkx as storage tool. @@ -254,17 +253,17 @@ class Node(Parameterised): """ def __init__(self, node_id: int, household: Household, - infection_attributes=None, - lfd_testing_adherence_attributes=None, - tracing_attributes=None, - tracing_adherence_attributes=None, - returning_travellers_attributes=None, - lfd_testing_attributes=None): + infection_attributes: dict = None, + lfd_testing_adherence_attributes: dict = None, + tracing_attributes: dict = None, + tracing_adherence_attributes: dict = None, + returning_travellers_attributes: dict = None, + lfd_testing_attributes: dict = None): self.id = node_id self.household = household - # Update node attribute classes + # Instantiate attribute classes self.infection = InfectionAttributes(infection_attributes) self.lfd_testing_adherence = LFDTestingAdherenceAttributes(lfd_testing_adherence_attributes) self.tracing = TracingAttributes(tracing_attributes) @@ -280,7 +279,6 @@ def time_relative_to_symptom_onset(self, time: int) -> int: def locally_infected(self) -> bool: if self.infection.infecting_node_id: - #return self.infection.infecting_node_id.household == self.household return self.household.network.node(self.infection.infecting_node_id).household == self.household else: return False @@ -325,7 +323,7 @@ def node_type(self, time=None) -> NodeType: elif self.tracing.received_result and self.lfd_testing.avenue_of_testing == TestType.pcr: return NodeType.received_neg_test_pcr elif self.lfd_testing.taken_confirmatory_PCR_test: - if time and time >= self.confirmatory_PCR_test_result_time: + if time and time >= self.lfd_testing.confirmatory_PCR_test_result_time: if self.lfd_testing_adherence.confirmatory_PCR_result_was_positive: return NodeType.confirmatory_pos_pcr_test else: @@ -340,7 +338,7 @@ def take_confirmatory_pcr_test(self, time: int, prob_pcr_positive: Callable): infectious_age_when_tested = time - self.infection.time_infected - self.confirmatory_PCR_test_result_time = time + self.tracing.testing_delay + self.lfd_testing.confirmatory_PCR_test_result_time = time + self.tracing.testing_delay self.lfd_testing.taken_confirmatory_PCR_test = True if numpy.random.binomial(1, prob_pcr_positive(infectious_age_when_tested)) == 1: From 64215d54ed58679adc83f5871709387e2a0b0992 Mon Sep 17 00:00:00 2001 From: Peter Crowther <13117069+merrygoat@users.noreply.github.com> Date: Mon, 9 Aug 2021 11:48:38 +0100 Subject: [PATCH 62/70] Add node attribute types --- .../behaviours/infection/new_infection.py | 42 ++--- household_contact_tracing/node_attributes.py | 164 ++++++++---------- 2 files changed, 93 insertions(+), 113 deletions(-) diff --git a/household_contact_tracing/behaviours/infection/new_infection.py b/household_contact_tracing/behaviours/infection/new_infection.py index dacfb85..d27bd83 100644 --- a/household_contact_tracing/behaviours/infection/new_infection.py +++ b/household_contact_tracing/behaviours/infection/new_infection.py @@ -137,12 +137,6 @@ def new_infection(self, time: int, household: Household, infecting_node: Optiona # Symptom onset time symptom_onset_time = time + self.incubation_period(asymptomatic) - # If the node is asymptomatic, we need to generate a pseudo symptom onset time - if asymptomatic: - pseudo_symptom_onset_time = self.incubation_period(asymptomatic=False) - else: - pseudo_symptom_onset_time = symptom_onset_time - # When a node reports its infection if not asymptomatic and np.random.binomial(1, self.infection_reporting_prob) == 1: will_report_infection = True @@ -151,10 +145,6 @@ def new_infection(self, time: int, household: Household, infecting_node: Optiona will_report_infection = False time_of_reporting = float('Inf') - # We assign each node a recovery period of 21 days, after 21 days the probability of - # causing a new infections is 0, due to the generation time distribution - recovery_time = time + 14 - # If the household has the propensity to use the contact tracing app, decide # if the node uses the app. if household.propensity_trace_app: @@ -162,13 +152,6 @@ def new_infection(self, time: int, household: Household, infecting_node: Optiona else: has_trace_app = False - isolation_uptake = self.will_uptake_isolation() - - if household.isolated and isolation_uptake: - node_is_isolated = True - else: - node_is_isolated = False - tracing_attributes = { 'contact_traced': household.contact_traced, 'has_contact_tracing_app': has_trace_app, @@ -178,15 +161,33 @@ def new_infection(self, time: int, household: Household, infecting_node: Optiona 'will_report_infection': will_report_infection, } + isolation_uptake = self.will_uptake_isolation() + tracing_adherence_attributes = {'will_uptake_isolation': isolation_uptake, 'propensity_imperfect_isolation': self.get_propensity_imperfect_isolation() } + # If the node is asymptomatic, we need to generate a pseudo symptom onset time + if asymptomatic: + pseudo_symptom_onset_time = self.incubation_period(asymptomatic=False) + else: + pseudo_symptom_onset_time = symptom_onset_time + returning_travellers_attributes = {'pseudo_symptom_onset_time': pseudo_symptom_onset_time} - infecting_node_id = None if infecting_node: infecting_node_id = infecting_node.id + else: + infecting_node_id = None + + if household.isolated and isolation_uptake: + node_is_isolated = True + else: + node_is_isolated = False + + # Each node has a recovery period after which the node recovers and cannot cause a new infection + recovery_time = time + 14 + infection_attributes = { 'time_infected': time, 'asymptomatic': asymptomatic, @@ -195,15 +196,14 @@ def new_infection(self, time: int, household: Household, infecting_node: Optiona 'recovery_time': recovery_time, } - new_node = self.network.add_node( - household_id=household.id, + new_node = self.network.add_node(household_id=household.id, infection_attributes=infection_attributes, tracing_adherence_attributes=tracing_adherence_attributes, returning_travellers_attributes=returning_travellers_attributes, tracing_attributes=tracing_attributes ) - # Each house now stores the ID's of which nodes are stored inside the house, + # Each house stores the IDs of which nodes are stored inside the house, # so that quarantining can be done at the household level household.nodes.append(new_node) diff --git a/household_contact_tracing/node_attributes.py b/household_contact_tracing/node_attributes.py index 3c10e2d..584de03 100644 --- a/household_contact_tracing/node_attributes.py +++ b/household_contact_tracing/node_attributes.py @@ -1,6 +1,7 @@ -from typing import Optional +from typing import Optional, List, Tuple from household_contact_tracing.parameterised import Parameterised + class InfectionAttributes(Parameterised): """ A class used to store Node attributes relating to infection @@ -8,29 +9,25 @@ class InfectionAttributes(Parameterised): Attributes ---------- - asymptomatic (boolean) - infecting_node_id (int) - isolated (boolean) - outside_house_contacts_made (int) - recovered (boolean) - recovery_time (float) - spread_to_global_node_time_tuples (list) - time_infected (int) - - Methods - ------- - + asymptomatic + infecting_node_id + isolated + outside_house_contacts_made + recovered + recovery_time + spread_to_global_node_time_tuples + time_infected """ - def __init__(self, attributes): - self.asymptomatic = None - self.infecting_node_id = None - self.isolated = None - self.outside_house_contacts_made = 0 - self.recovered = False - self.recovery_time = None - self.spread_to_global_node_time_tuples = [] - self.time_infected = None + def __init__(self, attributes: dict): + self.asymptomatic: Optional[bool] = None + self.infecting_node_id: Optional[int] = None + self.isolated: Optional[bool] = None + self.outside_house_contacts_made: int = 0 + self.recovered: bool = False + self.recovery_time: Optional[int] = None + self.spread_to_global_node_time_tuples: List[Tuple[int, int]] = [] + self.time_infected: Optional[int] = None # Update instance variables with anything in attributes self.update_params(attributes) @@ -43,29 +40,25 @@ class LFDTestingAttributes(Parameterised): Attributes ---------- - avenue_of_testing (int) - being_lateral_flow_tested (boolean) - positive_test_time (int) - taken_confirmatory_PCR_test (boolean) - time_started_lfa_testing (int) - confirmatory_PCR_test_result_time (float) - completed_lateral_flow_testing_time (boolean) - lateral_flow_testing_duration (float) - - Methods - ------- - + avenue_of_testing + being_lateral_flow_tested + positive_test_time + taken_confirmatory_PCR_test + time_started_lfa_testing + confirmatory_PCR_test_result_time + completed_lateral_flow_testing_time + lateral_flow_testing_duration """ - def __init__(self, attributes): + def __init__(self, attributes: dict): self.avenue_of_testing: Optional[int] = None - self.being_lateral_flow_tested = None - self.positive_test_time = None - self.taken_confirmatory_PCR_test = None - self.time_started_lfa_testing = None - self.confirmatory_PCR_test_result_time = None - self.completed_lateral_flow_testing_time = None - self.lateral_flow_testing_duration = 0 + self.being_lateral_flow_tested: Optional[bool] = None + self.positive_test_time: Optional[int] = None + self.taken_confirmatory_PCR_test: Optional[bool] = None + self.time_started_lfa_testing: Optional[int] = None + self.confirmatory_PCR_test_result_time: Optional[int] = None + self.completed_lateral_flow_testing_time: Optional[bool] = None + self.lateral_flow_testing_duration: Optional[int] = 0 # Update instance variables with anything in attributes self.update_params(attributes) @@ -78,20 +71,17 @@ class LFDTestingAdherenceAttributes(Parameterised): Attributes ---------- - confirmatory_PCR_result_was_positive (boolean) - node_will_take_up_lfa_testing (boolean) - - - Methods - ------- - + confirmatory_PCR_result_was_positive + node_will_take_up_lfa_testing + propensity_risky_behaviour_lfa_testing + propensity_to_miss_lfa_tests """ - def __init__(self, attributes): + def __init__(self, attributes: dict): self.confirmatory_PCR_result_was_positive: Optional[bool] = None - self.node_will_take_up_lfa_testing = None - self.propensity_risky_behaviour_lfa_testing = None - self.propensity_to_miss_lfa_tests = None + self.node_will_take_up_lfa_testing: Optional[bool] = None + self.propensity_risky_behaviour_lfa_testing: Optional[float] = None + self.propensity_to_miss_lfa_tests: Optional[float] = None # Update instance variables with anything in attributes self.update_params(attributes) @@ -104,14 +94,11 @@ class ReturningTravellerAttributes(Parameterised): Attributes ---------- - pseudo_symptom_onset_time (float) - - Methods - ------- + pseudo_symptom_onset_time """ - def __init__(self, attributes): - self.pseudo_symptom_onset_time = None + def __init__(self, attributes: dict): + self.pseudo_symptom_onset_time: Optional[float] = None # Update instance variables with anything in attributes self.update_params(attributes) @@ -124,32 +111,29 @@ class TracingAttributes(Parameterised): Attributes ---------- - contact_traced (boolean) - has_contact_tracing_app (boolean) - propagated_contact_tracing (boolean) - received_positive_test_result (boolean) - received_result (boolean) - symptom_onset_time (float) - testing_delay (float) - time_of_reporting (int) - will_report_infection (boolean) - completed_isolation (boolean) - - Methods - ------- + contact_traced + has_contact_tracing_app + propagated_contact_tracing + received_positive_test_result + received_result + symptom_onset_time + testing_delay + time_of_reporting + will_report_infection + completed_isolation """ - def __init__(self, attributes): - self.contact_traced = None - self.has_contact_tracing_app = None - self.propagated_contact_tracing = False - self.received_positive_test_result = False - self.received_result = False - self.symptom_onset_time = None - self.testing_delay = None - self.time_of_reporting = None - self.will_report_infection = None - self.completed_isolation = None + def __init__(self, attributes: dict): + self.contact_traced: Optional[bool] = None + self.has_contact_tracing_app: Optional[bool] = None + self.propagated_contact_tracing: bool = False + self.received_positive_test_result: bool = False + self.received_result: bool = False + self.symptom_onset_time: Optional[int] = None + self.testing_delay: Optional[int] = None + self.time_of_reporting: Optional[int] = None + self.will_report_infection: Optional[bool] = None + self.completed_isolation: Optional[bool] = None # Update instance variables with anything in attributes self.update_params(attributes) @@ -162,17 +146,13 @@ class TracingAdherenceAttributes(Parameterised): Attributes ---------- - propensity_imperfect_isolation (float) - will_uptake_isolation (boolean) - - Methods - ------- - + propensity_imperfect_isolation + will_uptake_isolation """ - def __init__(self, attributes): - self.propensity_imperfect_isolation = None - self.will_uptake_isolation = None + def __init__(self, attributes: dict): + self.propensity_imperfect_isolation: Optional[float] = None + self.will_uptake_isolation: Optional[bool] = None # Update instance variables with anything in attributes self.update_params(attributes) From 860c6cfba6bded14048516bafe81e5784339698d Mon Sep 17 00:00:00 2001 From: Peter Crowther <13117069+merrygoat@users.noreply.github.com> Date: Mon, 9 Aug 2021 12:54:45 +0100 Subject: [PATCH 63/70] Rename contact tracing method Child method has different signature to parent method so can not be a subclass. --- examples/run_testing_contact_model.py | 2 +- .../intervention/increment_tracing.py | 24 +++++++++---------- test/test_TestingContactModel.py | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/examples/run_testing_contact_model.py b/examples/run_testing_contact_model.py index a804a80..d4d52c9 100644 --- a/examples/run_testing_contact_model.py +++ b/examples/run_testing_contact_model.py @@ -202,7 +202,7 @@ def prob_testing_positive_pcr_func(infectious_age): model.infection.new_outside_household_infection(time=0, infecting_node=model.network.node(1)) - model.intervention.increment_tracing.attempt_contact_trace_of_household( + model.intervention.increment_tracing.contact_trace_household( house_to=model.network.household(2), house_from=model.network.household(1), days_since_contact_occurred=0, diff --git a/household_contact_tracing/behaviours/intervention/increment_tracing.py b/household_contact_tracing/behaviours/intervention/increment_tracing.py index 8fba3bc..6ebb246 100644 --- a/household_contact_tracing/behaviours/intervention/increment_tracing.py +++ b/household_contact_tracing/behaviours/intervention/increment_tracing.py @@ -279,7 +279,7 @@ def propagate_contact_tracing(self, node: Node, time: int): self.number_of_days_to_trace_backwards: # Then attempt to contact trace the household of the node that infected you - self.attempt_contact_trace_of_household( + self.contact_trace_household( house_to=infected_by_node.household, house_from=node.household, time=time, @@ -301,19 +301,19 @@ def propagate_contact_tracing(self, node: Node, time: int): time_t <= node.tracing.symptom_onset_time + self.number_of_days_to_trace_forwards and \ not child_node.infection.isolated: - self.attempt_contact_trace_of_household( + self.contact_trace_household( house_to=child_node.household, house_from=node.household, days_since_contact_occurred=time - time_t, time=time ) - def attempt_contact_trace_of_household(self, - house_to: Household, - house_from: Household, - days_since_contact_occurred: int, - time: int, - contact_trace_delay: int = 0): + def contact_trace_household(self, + house_to: Household, + house_from: Household, + days_since_contact_occurred: int, + time: int, + contact_trace_delay: int = 0): # Decide if the edge was traced by the app app_traced = self.network.is_edge_app_traced(self.network.get_edge_between_household(house_from, house_to)) @@ -422,7 +422,7 @@ def propagate_contact_tracing(self, node: Node, time: int): self.number_of_days_to_trace_backwards: # Then attempt to contact trace the household of the node that infected you - self.attempt_contact_trace_of_household( + self.contact_trace_household( house_to=infected_by_node.household, house_from=node.household, days_since_contact_occurred=time - node.infection.time_infected, @@ -436,7 +436,7 @@ def propagate_contact_tracing(self, node: Node, time: int): node.lfd_testing.positive_test_time - self.number_of_days_prior_to_LFA_result_to_trace: # Then attempt to contact trace the household of the node that infected you - self.attempt_contact_trace_of_household( + self.contact_trace_household( house_to=infected_by_node.household, house_from=node.household, days_since_contact_occurred=time - node.infection.time_infected, @@ -460,7 +460,7 @@ def propagate_contact_tracing(self, node: Node, time: int): if time_t <= node.tracing.symptom_onset_time + self.number_of_days_to_trace_forwards: if not child_node.infection.isolated: - self.attempt_contact_trace_of_household( + self.contact_trace_household( house_to=child_node.household, house_from=node.household, days_since_contact_occurred=time - time_t, @@ -475,7 +475,7 @@ def propagate_contact_tracing(self, node: Node, time: int): if time_t >= node.lfd_testing.positive_test_time - \ self.number_of_days_prior_to_LFA_result_to_trace: - self.attempt_contact_trace_of_household( + self.contact_trace_household( house_to=child_node.household, house_from=node.household, days_since_contact_occurred=time - time_t, diff --git a/test/test_TestingContactModel.py b/test/test_TestingContactModel.py index 61fb1eb..793a8a6 100644 --- a/test/test_TestingContactModel.py +++ b/test/test_TestingContactModel.py @@ -236,7 +236,7 @@ def prob_testing_positive_pcr_func(infectious_age): model.infection.new_outside_household_infection(time=0, infecting_node=model.network.node(1)) - model.intervention.increment_tracing.attempt_contact_trace_of_household( + model.intervention.increment_tracing.contact_trace_household( house_to=model.network.household(2), house_from=model.network.household(1), days_since_contact_occurred=0, From ac1a94abb2ced728560e6780d3e08ab3eb404a03 Mon Sep 17 00:00:00 2001 From: Peter Crowther <13117069+merrygoat@users.noreply.github.com> Date: Mon, 9 Aug 2021 13:07:49 +0100 Subject: [PATCH 64/70] Fix missing Node attributes. --- .../infection/contact_rate_reduction.py | 3 ++- .../behaviours/infection/new_infection.py | 6 +---- .../intervention/increment_tracing.py | 24 +++++++++---------- .../behaviours/intervention/isolation.py | 6 ++--- .../branching_process_models.py | 2 +- household_contact_tracing/exceptions.py | 7 +++--- household_contact_tracing/infection.py | 7 +++--- household_contact_tracing/network.py | 2 +- household_contact_tracing/node_attributes.py | 2 +- 9 files changed, 29 insertions(+), 30 deletions(-) diff --git a/household_contact_tracing/behaviours/infection/contact_rate_reduction.py b/household_contact_tracing/behaviours/infection/contact_rate_reduction.py index 0470c0e..9bd587e 100644 --- a/household_contact_tracing/behaviours/infection/contact_rate_reduction.py +++ b/household_contact_tracing/behaviours/infection/contact_rate_reduction.py @@ -89,7 +89,8 @@ def get_contact_rate_reduction(self, node: Node) -> int: # imperfect intervention return self.global_contact_reduction_imperfect_quarantine - elif node.lfd_testing.being_lateral_flow_tested and node.lfd_testing_adherence.propensity_risky_behaviour_lfa_testing: + elif (node.lfd_testing.being_lateral_flow_tested and + node.lfd_testing_adherence.propensity_risky_behaviour_lfa_testing): # engaging in risky behaviour while testing negative return self.global_contact_reduction_risky_behaviour diff --git a/household_contact_tracing/behaviours/infection/new_infection.py b/household_contact_tracing/behaviours/infection/new_infection.py index d27bd83..ecb3b27 100644 --- a/household_contact_tracing/behaviours/infection/new_infection.py +++ b/household_contact_tracing/behaviours/infection/new_infection.py @@ -20,7 +20,7 @@ class NewInfection(ABC, Parameterised): Attributes ---------- - Todo: fill in descriptions of each attribute + # Todo: fill in descriptions of each attribute network: ContactTracingNetwork The store of Nodes and households used in the simulation symptom_reporting_delay @@ -42,10 +42,6 @@ class NewInfection(ABC, Parameterised): ------- new_infection(self, time: int, household: Household, infecting_node: Optional[Node] = None) - Add a new infected Node to the model. - :param time: The current simulation time. - :param household: The Household to create the new infection in. - :param infecting_node: The source of the new infection. """ diff --git a/household_contact_tracing/behaviours/intervention/increment_tracing.py b/household_contact_tracing/behaviours/intervention/increment_tracing.py index 6ebb246..143ebd6 100644 --- a/household_contact_tracing/behaviours/intervention/increment_tracing.py +++ b/household_contact_tracing/behaviours/intervention/increment_tracing.py @@ -275,7 +275,7 @@ def propagate_contact_tracing(self, node: Node, time: int): # if the infector is not already isolated and the time the node was infected captured # by going backwards # the node.infection.time_infected is when they had a contact with their infector. - if not infected_by_node.isolated and node.infection.time_infected >= node.tracing.symptom_onset_time - \ + if not infected_by_node.infection.isolated and node.infection.time_infected >= node.tracing.symptom_onset_time - \ self.number_of_days_to_trace_backwards: # Then attempt to contact trace the household of the node that infected you @@ -419,7 +419,7 @@ def propagate_contact_tracing(self, node: Node, time: int): if not infected_by_node.infection.isolated and \ node.infection.time_infected >= node.tracing.symptom_onset_time - \ - self.number_of_days_to_trace_backwards: + self.number_of_days_to_trace_backwards: # Then attempt to contact trace the household of the node that infected you self.contact_trace_household( @@ -432,7 +432,7 @@ def propagate_contact_tracing(self, node: Node, time: int): if not self.LFA_testing_requires_confirmatory_PCR: - if not infected_by_node.isolated and node.infection.time_infected >= \ + if not infected_by_node.tracing.isolated and node.infection.time_infected >= \ node.lfd_testing.positive_test_time - self.number_of_days_prior_to_LFA_result_to_trace: # Then attempt to contact trace the household of the node that infected you @@ -440,7 +440,7 @@ def propagate_contact_tracing(self, node: Node, time: int): house_to=infected_by_node.household, house_from=node.household, days_since_contact_occurred=time - node.infection.time_infected, - time=time ) + time=time) # spread_to_global_node_time_tuples stores a list of tuples, where the first element is # the node_id of a node who was globally infected by the node, and the second element is @@ -457,14 +457,14 @@ def propagate_contact_tracing(self, node: Node, time: int): # If the node was infected 2 days prior to symptom onset, or 7 days post and is # not already isolated if time_t >= node.tracing.symptom_onset_time - self.number_of_days_to_trace_backwards: - if time_t <= node.tracing.symptom_onset_time + self.number_of_days_to_trace_forwards: - if not child_node.infection.isolated: + if time_t <= node.tracing.symptom_onset_time + self.number_of_days_to_trace_forwards: + if not child_node.infection.isolated: - self.contact_trace_household( - house_to=child_node.household, - house_from=node.household, - days_since_contact_occurred=time - time_t, - time=time) + self.contact_trace_household( + house_to=child_node.household, + house_from=node.household, + days_since_contact_occurred=time - time_t, + time=time) elif node.lfd_testing.avenue_of_testing == TestType.lfa: @@ -479,4 +479,4 @@ def propagate_contact_tracing(self, node: Node, time: int): house_to=child_node.household, house_from=node.household, days_since_contact_occurred=time - time_t, - time=time) \ No newline at end of file + time=time) diff --git a/household_contact_tracing/behaviours/intervention/isolation.py b/household_contact_tracing/behaviours/intervention/isolation.py index b731b04..24cf62e 100644 --- a/household_contact_tracing/behaviours/intervention/isolation.py +++ b/household_contact_tracing/behaviours/intervention/isolation.py @@ -198,16 +198,16 @@ def isolate_positive_lateral_flow_tests(self, time: int, positive_nodes: List[No not self.LFA_testing_requires_confirmatory_PCR: node.household.apply_positive_policy(time, self.household_positive_policy) - def act_on_positive_LFA_tests(self, time: int, positive_nodes: List[Node]): + def act_on_positive_lfa_tests(self, time: int, positive_nodes: List[Node]): """For nodes who test positive on their LFA test, take the appropriate action depending on the policy """ self.isolate_positive_lateral_flow_tests(time, positive_nodes) if self.LFA_testing_requires_confirmatory_PCR: - self.confirmatory_pcr_test_LFA_nodes(time, positive_nodes) + self.confirmatory_pcr_test_lfa_nodes(time, positive_nodes) - def confirmatory_pcr_test_LFA_nodes(self, time: int, positive_nodes: List[Node]): + def confirmatory_pcr_test_lfa_nodes(self, time: int, positive_nodes: List[Node]): """Nodes who receive a positive LFA result will be tested using a PCR test.""" for node in positive_nodes: if not node.lfd_testing.taken_confirmatory_PCR_test: diff --git a/household_contact_tracing/branching_process_models.py b/household_contact_tracing/branching_process_models.py index ece956d..8462616 100644 --- a/household_contact_tracing/branching_process_models.py +++ b/household_contact_tracing/branching_process_models.py @@ -344,7 +344,7 @@ def simulate_one_step(self): self.intervention.isolation.update_isolation(self.time) # isolate self reporting nodes positive_nodes = self.intervention.lft_nodes(self.time, self.prob_lfa_positive) - self.intervention.isolation.act_on_positive_LFA_tests(self.time, positive_nodes) + self.intervention.isolation.act_on_positive_lfa_tests(self.time, positive_nodes) # if we require PCR tests, to confirm infection we act on those if self.intervention.increment_tracing.LFA_testing_requires_confirmatory_PCR: self.intervention.increment_tracing.act_on_confirmatory_pcr_results(self.time) diff --git a/household_contact_tracing/exceptions.py b/household_contact_tracing/exceptions.py index 03d86db..775b840 100644 --- a/household_contact_tracing/exceptions.py +++ b/household_contact_tracing/exceptions.py @@ -2,13 +2,14 @@ from household_contact_tracing.branching_process_state import BranchingProcessState + class Error(Exception): """Base class for exceptions in this module""" pass class ModelStateError(Error): - """Exception raised when model is in an inapropriate state + """Exception raised when model is in an inappropriate state when a method or function is called. Args: @@ -17,5 +18,5 @@ class ModelStateError(Error): """ def __init__(self, state: BranchingProcessState, message: str): - self.state = state - self.message = message + self.state = state + self.message = message diff --git a/household_contact_tracing/infection.py b/household_contact_tracing/infection.py index 4bb613a..7800dc3 100644 --- a/household_contact_tracing/infection.py +++ b/household_contact_tracing/infection.py @@ -51,8 +51,6 @@ def __init__(self, self.household_pairwise_survival_prob = 0.2 self.update_params(params) - # Perform initial set-up calculations - # Precomputing the cdf's for generating the overdispersed contact data household_size = len(self.total_contact_means) self.cdf_dict = {i + 1: compute_negbin_cdf(self.total_contact_means[i], self.overdispersion) @@ -80,6 +78,8 @@ def __init__(self, self.new_infection = new_infection(self.network, params) self.contact_rate_reduction = contact_rate_reduction(params) + self.starting_households = [] + # Initialise starting infections of simulation self.initialise() @@ -177,7 +177,8 @@ def contacts_made_today(self, household_size) -> int: obs = sum([int(cdf[i] < random) for i in range(100)]) return obs - def compute_hh_infection_probs(self, pairwise_survival_prob: float) -> np.ndarray: + @staticmethod + def compute_hh_infection_probs(pairwise_survival_prob: float) -> np.ndarray: # Precomputing the infection probabilities for the within household epidemics. contact_prob = 0.8 day_0_infection_prob = current_hazard_rate(0, pairwise_survival_prob) / contact_prob diff --git a/household_contact_tracing/network.py b/household_contact_tracing/network.py index 0eea059..3a487b7 100644 --- a/household_contact_tracing/network.py +++ b/household_contact_tracing/network.py @@ -608,7 +608,7 @@ def local_epidemic_completed(self): Returns true if all infections in the household have recovered, which is defined as being 10 """ - return all([node.recovered for node in self.nodes]) + return all([node.infection.recovered for node in self.nodes]) @property def household_epidemic_size(self): diff --git a/household_contact_tracing/node_attributes.py b/household_contact_tracing/node_attributes.py index 584de03..5d85f24 100644 --- a/household_contact_tracing/node_attributes.py +++ b/household_contact_tracing/node_attributes.py @@ -98,7 +98,7 @@ class ReturningTravellerAttributes(Parameterised): """ def __init__(self, attributes: dict): - self.pseudo_symptom_onset_time: Optional[float] = None + self.pseudo_symptom_onset_time: Optional[int] = None # Update instance variables with anything in attributes self.update_params(attributes) From 8818df32d9667cc110d7e5f8bdb7a206b5767e38 Mon Sep 17 00:00:00 2001 From: Martyn Fyles Date: Mon, 9 Aug 2021 16:20:29 +0100 Subject: [PATCH 65/70] updates to conda env for calibrate module --- env_household_contact_tracing.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/env_household_contact_tracing.yml b/env_household_contact_tracing.yml index c9ecc36..40247e7 100644 --- a/env_household_contact_tracing.yml +++ b/env_household_contact_tracing.yml @@ -2,6 +2,8 @@ name: household-contact-tracing channels: - defaults dependencies: + - pip + - pip3 - matplotlib - numpy - scipy @@ -11,8 +13,10 @@ dependencies: - jupyter - pyyaml - pandas - - pyvis - beautifulsoup4 - statsmodels - - torch - - ax + - pip: + - pyvis + - torch + - pip3: + - ax-platform From bf3a66ef67e95e7ec73768caac97e071a64aa3f1 Mon Sep 17 00:00:00 2001 From: Martyn Fyles Date: Fri, 27 Aug 2021 12:51:05 +0100 Subject: [PATCH 66/70] fix: new node attribute structure --- household_contact_tracing/views/statistics_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/household_contact_tracing/views/statistics_view.py b/household_contact_tracing/views/statistics_view.py index 5e580e2..1b3f911 100644 --- a/household_contact_tracing/views/statistics_view.py +++ b/household_contact_tracing/views/statistics_view.py @@ -86,7 +86,7 @@ def get_infection_times(self): """ Returns a list containing the times at which each node was infected """ - return [node.time_infected for node in self._model.network.all_nodes()] + return [node.infection.time_infected for node in self._model.network.all_nodes()] def get_daily_incidence(self): """Returns a list of the new infections at each time point. From 4e4c236f0bb4e4a6fc32e8d58ada36d608654e1c Mon Sep 17 00:00:00 2001 From: Martyn Fyles Date: Fri, 27 Aug 2021 13:22:38 +0100 Subject: [PATCH 67/70] performance improvement + fix --- household_contact_tracing/branching_process_models.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/household_contact_tracing/branching_process_models.py b/household_contact_tracing/branching_process_models.py index 8462616..d0e0df2 100644 --- a/household_contact_tracing/branching_process_models.py +++ b/household_contact_tracing/branching_process_models.py @@ -1,7 +1,7 @@ import math import os from typing import Callable -from copy import deepcopy +from copy import copy from household_contact_tracing.infection import Infection from household_contact_tracing.intervention import Intervention @@ -160,13 +160,15 @@ def run_simulation(self, state_criteria: dict) -> None: self._state.switch(RunningState, self.state_criteria) while type(self.state) is RunningState: - prev_network = deepcopy(self.network) + + initial_node_count = copy(self.network.node_count) # This chunk of code executes one step (a days worth of infections and contact tracings) self.simulate_one_step() # If graph changed, tell parent - if not prev_network == self.network: + # a graph change is typically equivalent to nodes being added to the network + if initial_node_count == self.network.node_count: BranchingProcessModel.graph_changed(self) # Call parent completed step @@ -210,7 +212,7 @@ def run_hh_sar_simulation(self, state_criteria: dict) -> None: # they do not infect. This is mainly for computational ease for node in self.network.all_nodes(): if node.household.id not in self.infection.starting_households: - node.recovered = True + node.infection.recovered = True self.time += 1 From 9e93c84eb174e6b72a856a908677b42977e949bc Mon Sep 17 00:00:00 2001 From: Martyn Fyles Date: Fri, 27 Aug 2021 13:38:52 +0100 Subject: [PATCH 68/70] Fixes and rerunning --- examples/calibration.ipynb | 214 ++++++++++++++++--------------------- 1 file changed, 91 insertions(+), 123 deletions(-) diff --git a/examples/calibration.ipynb b/examples/calibration.ipynb index 0b07397..ff0a0d6 100644 --- a/examples/calibration.ipynb +++ b/examples/calibration.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "source": [ "import household_contact_tracing.branching_process_models as bpm\r\n", "from household_contact_tracing.branching_process_controller import BranchingProcessController\r\n", @@ -44,7 +44,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "source": [ "params = {'outside_household_infectivity_scaling': 0.1,\r\n", " 'contact_tracing_success_prob': 0.0, # doesn't matter, no tracing\r\n", @@ -78,12 +78,12 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "source": [ "controller = BranchingProcessController(bpm.HouseholdLevelTracing(params))\r\n", "controller.graph_view.set_display(False) # don't draw the network\r\n", "controller.csv_view.set_display(False) # don't save csv info\r\n", - "controller.run_simulation(25, max_active_infections=10000)" + "controller.run_simulation({\"max_time\": 25, \"infection_threshold\": 1e4})" ], "outputs": [], "metadata": {} @@ -97,7 +97,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 4, "source": [ "controller.statistics_view.growth_rate_summary(discard_first_n_days=10)" ], @@ -107,8 +107,8 @@ "name": "stdout", "text": [ "15 time periods were used to estimate the growth rate.\n", - "The estimated growth rate was 8.57% (95.0% CI: 7.5-9.64%) per day.\n", - "The estimated doubling time is 8.43 (95.0% CI: 7.53-9.59) days.\n" + "The estimated growth rate was 8.17% (95.0% CI: 7.09-9.25%) per day.\n", + "The estimated doubling time is 8.83 (95.0% CI: 7.84-10.12) days.\n" ] } ], @@ -116,7 +116,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 5, "source": [ "controller.statistics_view.household_secondary_attack_rate_summary(use_first_generation_only=False)" ], @@ -126,10 +126,10 @@ "name": "stdout", "text": [ "Household secondary attack rate summary:\n", - "173 local household epidemics were eligible to be included.\n", + "161 local household epidemics were eligible to be included.\n", "All households with completed local epidemics were included. This may lead to a biased sample, as it is possible that local epidemics with a long duration were not included.\n", - "There were 312 non-index susceptible individuals exposed, of which 193 were infected.\n", - "This yields a household secondary attack rate of 62% (95% CI: 56-67%).\n" + "There were 280 non-index susceptible individuals exposed, of which 188 were infected.\n", + "This yields a household secondary attack rate of 67% (95% CI: 61-72%).\n" ] } ], @@ -153,7 +153,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 6, "source": [ "params['starting_infections'] = 500 # increasing the number of starting infections will increase the sample size\r\n", "controller = BranchingProcessController(bpm.HouseholdLevelTracing(params))\r\n", @@ -166,7 +166,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "source": [ "controller.statistics_view.household_secondary_attack_rate_summary(use_first_generation_only=True)" ], @@ -176,10 +176,10 @@ "name": "stdout", "text": [ "Household secondary attack rate summary:\n", - "499 local household epidemics were eligible to be included.\n", + "500 local household epidemics were eligible to be included.\n", "Only the first generation of the household epidemic was included in this calculation.\n", - "There were 1053 non-index susceptible individuals exposed, of which 809 were infected.\n", - "This yields a household secondary attack rate of 77% (95% CI: 74-79%).\n" + "There were 1099 non-index susceptible individuals exposed, of which 879 were infected.\n", + "This yields a household secondary attack rate of 80% (95% CI: 78-82%).\n" ] } ], @@ -215,7 +215,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 8, "source": [ "calibrator = StandardCalibrationHouseholdLevelTracing(\r\n", " desired_growth_rate=0.1, \r\n", @@ -256,7 +256,7 @@ "output_type": "execute_result", "data": { "text/plain": [ - "{'growth_rate': 0.0034215244104689423, 'hh_sar': 0.10606060606060606}" + "{'growth_rate': 0.0005186250516060311, 'hh_sar': 0.09735744089012517}" ] }, "metadata": {}, @@ -293,7 +293,7 @@ "output_type": "execute_result", "data": { "text/plain": [ - "0.18708774795757915" + "0.17932736046271186" ] }, "metadata": {}, @@ -330,7 +330,7 @@ "output_type": "execute_result", "data": { "text/plain": [ - "{'growth_rate': 0.0653517347589822, 'hh_sar': 0.22484134179510426}" + "{'growth_rate': 0.037726432895622844, 'hh_sar': 0.22599374720857526}" ] }, "metadata": {}, @@ -360,7 +360,7 @@ "output_type": "execute_result", "data": { "text/plain": [ - "0.066385683081691" + "0.06378285333901784" ] }, "metadata": {}, @@ -394,39 +394,39 @@ "output_type": "stream", "name": "stderr", "text": [ - "[INFO 08-06 10:14:42] ax.modelbridge.dispatch_utils: Using GPEI (Bayesian optimization) since there are more continuous parameters than there are categories for the unordered categorical parameters.\n", - "[INFO 08-06 10:14:42] ax.modelbridge.dispatch_utils: Using Bayesian Optimization generation strategy: GenerationStrategy(name='Sobol+GPEI', steps=[Sobol for 5 trials, GPEI for subsequent trials]). Iterations after 5 will take longer to generate due to model-fitting.\n", - "[INFO 08-06 10:14:42] ax.service.managed_loop: Started full optimization with 20 steps.\n", - "[INFO 08-06 10:14:42] ax.service.managed_loop: Running optimization trial 1...\n", - "[INFO 08-06 10:14:49] ax.service.managed_loop: Running optimization trial 2...\n", - "[INFO 08-06 10:14:56] ax.service.managed_loop: Running optimization trial 3...\n", - "[INFO 08-06 10:15:05] ax.service.managed_loop: Running optimization trial 4...\n", - "[INFO 08-06 10:15:10] ax.service.managed_loop: Running optimization trial 5...\n", - "[INFO 08-06 10:15:16] ax.service.managed_loop: Running optimization trial 6...\n", - "[INFO 08-06 10:15:24] ax.service.managed_loop: Running optimization trial 7...\n", - "[INFO 08-06 10:15:29] ax.service.managed_loop: Running optimization trial 8...\n", - "[INFO 08-06 10:15:32] ax.service.managed_loop: Running optimization trial 9...\n", - "[INFO 08-06 10:15:37] ax.service.managed_loop: Running optimization trial 10...\n", - "[INFO 08-06 10:15:41] ax.service.managed_loop: Running optimization trial 11...\n", - "[INFO 08-06 10:15:47] ax.service.managed_loop: Running optimization trial 12...\n", - "[INFO 08-06 10:15:52] ax.service.managed_loop: Running optimization trial 13...\n", - "[INFO 08-06 10:16:02] ax.service.managed_loop: Running optimization trial 14...\n", - "[INFO 08-06 10:16:06] ax.service.managed_loop: Running optimization trial 15...\n", - "[INFO 08-06 10:16:10] ax.service.managed_loop: Running optimization trial 16...\n", - "[INFO 08-06 10:16:15] ax.service.managed_loop: Running optimization trial 17...\n", - "[INFO 08-06 10:16:21] ax.service.managed_loop: Running optimization trial 18...\n", - "[INFO 08-06 10:16:27] ax.service.managed_loop: Running optimization trial 19...\n", - "[INFO 08-06 10:16:31] ax.service.managed_loop: Running optimization trial 20...\n" + "[INFO 08-27 13:26:54] ax.modelbridge.dispatch_utils: Using GPEI (Bayesian optimization) since there are more continuous parameters than there are categories for the unordered categorical parameters.\n", + "[INFO 08-27 13:26:54] ax.modelbridge.dispatch_utils: Using Bayesian Optimization generation strategy: GenerationStrategy(name='Sobol+GPEI', steps=[Sobol for 5 trials, GPEI for subsequent trials]). Iterations after 5 will take longer to generate due to model-fitting.\n", + "[INFO 08-27 13:26:54] ax.service.managed_loop: Started full optimization with 20 steps.\n", + "[INFO 08-27 13:26:54] ax.service.managed_loop: Running optimization trial 1...\n", + "[INFO 08-27 13:26:57] ax.service.managed_loop: Running optimization trial 2...\n", + "[INFO 08-27 13:27:00] ax.service.managed_loop: Running optimization trial 3...\n", + "[INFO 08-27 13:27:03] ax.service.managed_loop: Running optimization trial 4...\n", + "[INFO 08-27 13:27:09] ax.service.managed_loop: Running optimization trial 5...\n", + "[INFO 08-27 13:27:14] ax.service.managed_loop: Running optimization trial 6...\n", + "[INFO 08-27 13:27:18] ax.service.managed_loop: Running optimization trial 7...\n", + "[INFO 08-27 13:27:25] ax.service.managed_loop: Running optimization trial 8...\n", + "[INFO 08-27 13:27:32] ax.service.managed_loop: Running optimization trial 9...\n", + "[INFO 08-27 13:27:38] ax.service.managed_loop: Running optimization trial 10...\n", + "[INFO 08-27 13:27:45] ax.service.managed_loop: Running optimization trial 11...\n", + "[INFO 08-27 13:27:50] ax.service.managed_loop: Running optimization trial 12...\n", + "[INFO 08-27 13:27:56] ax.service.managed_loop: Running optimization trial 13...\n", + "[INFO 08-27 13:28:02] ax.service.managed_loop: Running optimization trial 14...\n", + "[INFO 08-27 13:28:08] ax.service.managed_loop: Running optimization trial 15...\n", + "[INFO 08-27 13:28:14] ax.service.managed_loop: Running optimization trial 16...\n", + "[INFO 08-27 13:28:20] ax.service.managed_loop: Running optimization trial 17...\n", + "[INFO 08-27 13:28:25] ax.service.managed_loop: Running optimization trial 18...\n", + "[INFO 08-27 13:28:30] ax.service.managed_loop: Running optimization trial 19...\n", + "[INFO 08-27 13:28:38] ax.service.managed_loop: Running optimization trial 20...\n" ] }, { "output_type": "execute_result", "data": { "text/plain": [ - "({'outside_household_infectivity_scaling': 0.2369218112772867,\n", - " 'household_pairwise_survival_prob': 0.8104007705085332},\n", - " ({'objective': 0.026804953615459474},\n", - " {'objective': {'objective': 7.980087959078822e-06}}))" + "({'outside_household_infectivity_scaling': 0.29105303724942716,\n", + " 'household_pairwise_survival_prob': 0.8266625785806926},\n", + " ({'objective': 0.02251479796147475},\n", + " {'objective': {'objective': 2.09914613456077e-05}}))" ] }, "metadata": {}, @@ -453,8 +453,8 @@ "output_type": "execute_result", "data": { "text/plain": [ - "{'outside_household_infectivity_scaling': 0.2369218112772867,\n", - " 'household_pairwise_survival_prob': 0.8104007705085332}" + "{'outside_household_infectivity_scaling': 0.29105303724942716,\n", + " 'household_pairwise_survival_prob': 0.8266625785806926}" ] }, "metadata": {}, @@ -472,28 +472,16 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 16, "source": [ "outputs = calibrator.get_fitted_model_metric_samples(n_obs = 20, state_criteria={'max_time': 25})" ], - "outputs": [ - { - "output_type": "error", - "ename": "TypeError", - "evalue": "get_fitted_model_metric_samples() got an unexpected keyword argument 'state_criteria'", - "traceback": [ - "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[1;31mTypeError\u001b[0m Traceback (most recent call last)", - "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[0moutputs\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mcalibrator\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mget_fitted_model_metric_samples\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mn_obs\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;36m20\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mstate_criteria\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;33m{\u001b[0m\u001b[1;34m'max_time'\u001b[0m\u001b[1;33m:\u001b[0m \u001b[1;36m25\u001b[0m\u001b[1;33m}\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", - "\u001b[1;31mTypeError\u001b[0m: get_fitted_model_metric_samples() got an unexpected keyword argument 'state_criteria'" - ] - } - ], + "outputs": [], "metadata": {} }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "source": [ "growth_rates = [\r\n", " output['growth_rate'] for output in outputs\r\n", @@ -510,94 +498,74 @@ "output_type": "execute_result", "data": { "text/plain": [ - "[{'growth_rate': 0.11082584992572188, 'hh_sar': 0.18533157663592445},\n", - " {'growth_rate': 0.10738751955334369, 'hh_sar': 0.21236059479553904},\n", - " {'growth_rate': 0.09084650192266462, 'hh_sar': 0.19239274657231314},\n", - " {'growth_rate': 0.10075203747153494, 'hh_sar': 0.2336119665640123},\n", - " {'growth_rate': 0.10735735237940272, 'hh_sar': 0.2076707202993452},\n", - " {'growth_rate': 0.08313646738902405, 'hh_sar': 0.23312331233123312},\n", - " {'growth_rate': 0.08981749442949848, 'hh_sar': 0.2202781516375056},\n", - " {'growth_rate': 0.09322273991873418, 'hh_sar': 0.21407121407121407},\n", - " {'growth_rate': 0.09289832274649205, 'hh_sar': 0.21107580571947346},\n", - " {'growth_rate': 0.09344663727571464, 'hh_sar': 0.20498614958448755},\n", - " {'growth_rate': 0.096859550802151, 'hh_sar': 0.21289151157512484},\n", - " {'growth_rate': 0.10469928901617155, 'hh_sar': 0.2064975522919448},\n", - " {'growth_rate': 0.10857606282994096, 'hh_sar': 0.20827710301394511},\n", - " {'growth_rate': 0.09836023406359318, 'hh_sar': 0.20940959409594095},\n", - " {'growth_rate': 0.08472873842505456, 'hh_sar': 0.20318181818181819},\n", - " {'growth_rate': 0.0941299371383913, 'hh_sar': 0.2197851387645479},\n", - " {'growth_rate': 0.09565612682725907, 'hh_sar': 0.20922795797167656},\n", - " {'growth_rate': 0.07646034952842615, 'hh_sar': 0.20435967302452315},\n", - " {'growth_rate': 0.09383057360324933, 'hh_sar': 0.17884702678166137},\n", - " {'growth_rate': 0.08110175647926467, 'hh_sar': 0.19276556776556777}]" + "[{'growth_rate': 0.09354097023311786, 'hh_sar': 0.19894598155467721},\n", + " {'growth_rate': 0.10459428353574798, 'hh_sar': 0.18727436823104693},\n", + " {'growth_rate': 0.09622990208899632, 'hh_sar': 0.16651705565529623},\n", + " {'growth_rate': 0.09736994810011314, 'hh_sar': 0.20397111913357402},\n", + " {'growth_rate': 0.10826046481796817, 'hh_sar': 0.19301470588235295},\n", + " {'growth_rate': 0.10203121214687849, 'hh_sar': 0.18088888888888888},\n", + " {'growth_rate': 0.10248554386808573, 'hh_sar': 0.1938821412505623},\n", + " {'growth_rate': 0.09207795040736884, 'hh_sar': 0.19294425087108014},\n", + " {'growth_rate': 0.11257986763297648, 'hh_sar': 0.2009367681498829},\n", + " {'growth_rate': 0.0973251165252016, 'hh_sar': 0.19762122598353157},\n", + " {'growth_rate': 0.10491466744499066, 'hh_sar': 0.17720350715274574},\n", + " {'growth_rate': 0.10545386644921687, 'hh_sar': 0.16810933940774486},\n", + " {'growth_rate': 0.09140776209925891, 'hh_sar': 0.18013544018058691},\n", + " {'growth_rate': 0.11275789392789268, 'hh_sar': 0.1863270777479893},\n", + " {'growth_rate': 0.08900103355920155, 'hh_sar': 0.17744092732946945},\n", + " {'growth_rate': 0.10833669452159703, 'hh_sar': 0.175512104283054},\n", + " {'growth_rate': 0.08669414824191035, 'hh_sar': 0.17235188509874327},\n", + " {'growth_rate': 0.09551511504055435, 'hh_sar': 0.20636363636363636},\n", + " {'growth_rate': 0.10026521661325233, 'hh_sar': 0.1894175188972877},\n", + " {'growth_rate': 0.111205551545628, 'hh_sar': 0.18227501142074007}]" ] }, "metadata": {}, - "execution_count": 18 + "execution_count": 17 } ], "metadata": {} }, { "cell_type": "code", - "execution_count": null, + "execution_count": 21, "source": [ - "sns.displot(growth_rates)" + "sns.displot(growth_rates)\r\n", + "plt.show()" ], "outputs": [ - { - "output_type": "execute_result", - "data": { - "text/plain": [ - "" - ] - }, - "metadata": {}, - "execution_count": 26 - }, { "output_type": "display_data", "data": { "text/plain": [ - "
" + "
" ], - "image/png": "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" + "image/svg+xml": "\r\n\r\n\r\n \r\n \r\n \r\n \r\n 2021-08-27T13:31:12.473955\r\n image/svg+xml\r\n \r\n \r\n Matplotlib v3.4.2, https://matplotlib.org/\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n\r\n", + "image/png": "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227r6lkAgP/QqR99v/baa3H99dfHfffdF+PHj4/a2tp2t1955ZVdMhwA9HadCvXjjz8eRx99dEREPPnkk+1u84tlANB1OhXqP/zhD109BwDQAR9zCQCJdeoZ9UknnfRff8T9+9//vtMDAQD/1qlQb399ers33ngjHnvssXjyySd3+LAOAKDzOhXqq666qsPrL7300ti4ceMeDQQA/FuXvkb9xS9+0d/5BoAu1KWhfuihh6J///5deZcA0Kt16kffn/70p9ttF0URLS0tsXTp0vje977XJYMBAJ0M9aBBg9pt77XXXjFmzJi47LLLYtKkSV0yGADQyVDffPPNXT0HANCBToV6u2XLlkVTU1PU1NTEuHHj4n3ve19XzQUARCdDvW7dujjrrLNi0aJFsd9++0VRFLF+/fo46aSTYt68eXHAAQd09ZwA0Ct16re+p02bFpVKJVasWBEvv/xyvPLKK/Hkk09GpVKJiy66qKtnBIBeq1PPqBcsWBD33XdfHHbYYW3XjRs3Lq699lq/TAYAXahTz6i3bdu2w2dQR0TU1tbGtm3b9ngoAOBNnQr1ySefHNOnT49//OMfbde98MIL8bWvfS0++tGPdtlwANDbdSrU11xzTWzYsCFGjRoVBx98cLz3ve+N0aNHx4YNG+InP/lJV88IAL1Wp16jbmxsjEcffTQWLlwYq1atiqIoYty4cXHKKad09XwA0Kvt1jPq3//+9zFu3LioVCoREfGxj30spk2bFhdddFEce+yxcfjhh8cf//jHbhkUAHqj3Qr11VdfHV/5yleivr5+h9sGDRoU5513Xlx55ZVdNhwA9Ha7Feq//vWvcdppp+309kmTJsWyZcv2eCgA4E27FeoXX3yxw7dlbde3b9/45z//ucdDAQBv2q1Qv/vd744nnnhip7c//vjj0dDQsMdDAQBv2q1Qf/zjH4/vf//78dprr+1w2+bNm2PWrFnxyU9+ssuGA4DebrfenvXd7343br/99jj00EPjwgsvjDFjxkRNTU00NTXFtddeG1u3bo3vfOc73TUrAPQ6uxXqoUOHxoMPPhhf/epXY+bMmVEURURE1NTUxKmnnhrXXXddDB06tFsGBYDeaLf/4MnIkSPjt7/9bbzyyivx1FNPRVEUccghh8S73vWu7pgPAHq1Tv1lsoiId73rXXHsscd25SwAwH/o1N/6BgDeGUINAIkJNQAkJtQAkJhQA0BiQg0AiQk1ACQm1ACQmFADQGJCDQCJCTUAJCbUAJCYUANAYkINAIkJNQAkJtQAkJhQA0BiQg0AiQk1ACQm1ACQmFADQGJCDQCJCTUAJCbUAJCYUANAYkINAImVGuo5c+bEscceGwMHDowDDzwwzjjjjPjb3/5W5kgAkEqpoV68eHFccMEFsWTJkli4cGFs2bIlJk2aFJs2bSpzLABIo2+ZB1+wYEG77ZtvvjkOPPDAWLZsWZxwwgk77F+tVqNarbZtVyqVLp+pubk5Wltbu/x+e4qmpqayRwDgLUoN9X9av359RETsv//+Hd4+Z86cmD17drcdv7m5OcaOPSw2b361247RU7xRfb3sEQCIRKEuiiJmzJgRH/rQh+KII47ocJ+ZM2fGjBkz2rYrlUo0NjZ22Qytra2xefOrcdyXZkV9w6guu9+epOWJh+LJu6+PLVu2lD0KAJEo1BdeeGE8/vjj8ac//Wmn+9TV1UVdXV23z1LfMCr2HzGm24+TUaXl2bJHAOAtUoR62rRpcffdd8cDDzwQ73nPe8oeBwDSKDXURVHEtGnT4o477ohFixbF6NGjyxwHANIpNdQXXHBB3HbbbXHXXXfFwIEDY+3atRERMWjQoNh7773LHA0AUij1fdRz586N9evXx4knnhgNDQ1tl1/+8pdljgUAaZT+o28AYOf8rW8ASEyoASAxoQaAxIQaABITagBITKgBIDGhBoDEhBoAEhNqAEhMqAEgMaEGgMSEGgASE2oASEyoASAxoQaAxIQaABITagBITKgBIDGhBoDEhBoAEhNqAEhMqAEgMaEGgMSEGgASE2oASEyoASAxoQaAxIQaABITagBITKgBIDGhBoDEhBoAEhNqAEhMqAEgMaEGgMSEGgASE2oASEyoASAxoQaAxIQaABITagBITKgBIDGhBoDEhBoAEhNqAEhMqAEgMaEGgMSEGgASE2oASEyoASAxoQaAxIQaABITagBITKgBIDGhBoDEhBoAEhNqAEhMqAEgMaEGgMSEGgASE2oASEyoASAxoQaAxIQaABITagBITKgBIDGhBoDEhBoAEhNqAEhMqAEgMaEGgMSEGgASE2oASEyoASCxUkP9wAMPxKc+9akYPnx41NTUxJ133lnmOACQTqmh3rRpUxx11FFxzTXXlDkGAKTVt8yDT548OSZPnrzL+1er1ahWq23blUqlO8YCgDR61GvUc+bMiUGDBrVdGhsbyx4JALpVjwr1zJkzY/369W2X559/vuyRAKBblfqj791VV1cXdXV1ZY8BAO+YHvWMGgB6G6EGgMRK/dH3xo0b46mnnmrbXrNmTTz22GOx//77x4gRI0qcDAByKDXUS5cujZNOOqlte8aMGRERMXXq1LjllltKmgoA8ig11CeeeGIURVHmCACQmteoASAxoQaAxIQaABITagBITKgBIDGhBoDEhBoAEhNqAEhMqAEgMaEGgMSEGgASE2oASEyoASAxoQaAxIQaABITagBITKgBIDGhBoDEhBoAEhNqAEhMqAEgMaEGgMSEGgASE2oASEyoASAxoQaAxIQaABITagBITKgBIDGhBoDEhBoAEhNqAEhMqAEgMaEGgMSEGgASE2oASEyoASAxoQaAxIQaABITagBITKgBIDGhBoDEhBoAEhNqAEhMqAEgMaEGgMSEGgASE2oASEyoASAxoQaAxIQaABITagBITKgBIDGhBoDEhBoAEhNqAEhMqAEgMaEGgMSEGgASE2oASEyoASAxoQaAxIQaABITagBITKgBIDGhBoDEhBoAEhNqAEhMqAEgMaEGgMSEGgASE2oASEyoASAxoQaAxEoP9XXXXRejR4+O/v37x4QJE+KPf/xj2SMBQBqlhvqXv/xlXHzxxfGd73wnli9fHh/+8Idj8uTJ0dzcXOZYAJBG3zIPfuWVV8aXv/zlOPfccyMi4uqrr45777035s6dG3PmzNlh/2q1GtVqtW17/fr1ERFRqVS6ZJ6NGzdGRMTLz/0ttlQ3d8l99jSVluciImL9C6ujtm9NydOUxzpYg+2sgzWIiKisffMJ5MaNG7usORERAwcOjJqat1nToiTVarXo06dPcfvtt7e7/qKLLipOOOGEDr9m1qxZRUS4uLi4uLj8T1zWr1//tr0s7Rl1a2trbN26NYYOHdru+qFDh8batWs7/JqZM2fGjBkz2ra3bdsWL7/8cgwePPjt/4+kF6lUKtHY2BjPP/981NfXlz1OatZq11mr3WO9dl1vXquBAwe+7T6l/ug7InYIbFEUO41uXV1d1NXVtbtuv/32667Rerz6+vped9J3lrXaddZq91ivXWetOlbaL5MNGTIk+vTps8Oz53Xr1u3wLBsAeqvSQt2vX7+YMGFCLFy4sN31CxcujOOPP76kqQAgl1J/9D1jxoyYMmVKHHPMMTFx4sS4/vrro7m5Oc4///wyx+rx6urqYtasWTu8TMCOrNWus1a7x3rtOmv139UURVGUOcB1110XP/zhD6OlpSWOOOKIuOqqq+KEE04ocyQASKP0UAMAO1f6nxAFAHZOqAEgMaEGgMSEGgASE+qEdvejPxcvXhwTJkyI/v37x0EHHRQ//elP293+xhtvxGWXXRYHH3xw9O/fP4466qhYsGDBHh83gzLW6tJLL42ampp2l2HDhnX599bVdmetWlpa4uyzz44xY8bEXnvtFRdffHGH+82fPz/GjRsXdXV1MW7cuLjjjjv26LhZlLFWPfW8iuj69VqxYkWceeaZMWrUqKipqYmrr756j4/bo+3ph2vQtebNm1fU1tYWN9xwQ7Fy5cpi+vTpxYABA4rnnnuuw/2feeaZYp999immT59erFy5srjhhhuK2tra4te//nXbPt/4xjeK4cOHF7/5zW+Kp59+urjuuuuK/v37F48++minj5tBWWs1a9as4vDDDy9aWlraLuvWrev273dP7O5arVmzprjooouKW2+9tTj66KOL6dOn77DPgw8+WPTp06e4/PLLi6ampuLyyy8v+vbtWyxZsqTTx82grLXqiedVUXTPev3lL38pLrnkkuIXv/hFMWzYsOKqq67a4+P2ZEKdzAc+8IHi/PPPb3fd2LFji29961sd7v+Nb3yjGDt2bLvrzjvvvOKDH/xg23ZDQ0NxzTXXtNvn9NNPL77whS90+rgZlLVWs2bNKo466qg9nP6dtSeP70c+8pEO/zH97Gc/W5x22mntrjv11FOLs846q0uOW5ay1qonnldF0T3r9VYjR47sMNQ98dzqLD/6TuT111+PZcuWxaRJk9pdP2nSpHjwwQc7/JqHHnpoh/1PPfXUWLp0abzxxhsR8ebnePfv37/dPnvvvXf86U9/6vRxy1bWWm23evXqGD58eIwePTrOOuuseOaZZ/b0W+o23fX47mw9t99nbzmvdsXbrdV2Pem8iijvMe6J59aeEOpEOvPRn2vXru1w/y1btkRra2tEvPkPwpVXXhmrV6+Obdu2xcKFC+Ouu+6KlpaWTh+3bGWtVUTEcccdFz//+c/j3nvvjRtuuCHWrl0bxx9/fLz00ktd/F12je56fHe2ntvvs7ecV7vi7dYqouedVxHlPcY98dzaE0Kd0O589OfO9n/r9T/+8Y/jkEMOibFjx0a/fv3iwgsvjHPOOSf69OmzR8fNoIy1mjx5cpx55plx5JFHximnnBK/+c1vIiLi1ltv7ZLvqbt0x+O7K/fZG86rrrjPnnpeRZT3GPfEc6szhDqRznz057Bhwzrcv2/fvjF48OCIiDjggAPizjvvjE2bNsVzzz0Xq1atin333TdGjx7d6eOWray16siAAQPiyCOPjNWrV+/hd9U9uuvx3dl6br/P3nJe7Yq3W6uOZD+vIsp7jHviubUnhDqRznz058SJE3fY/3e/+10cc8wxUVtb2+76/v37x7vf/e7YsmVLzJ8/P04//fROH7dsZa1VR6rVajQ1NUVDQ0Mnv5vu1V2P787Wc/t99pbzale83Vp1JPt5FVHeY9wTz609Us7vsLEz299ycOONNxYrV64sLr744mLAgAHFs88+WxRFUXzrW98qpkyZ0rb/9rccfe1rXytWrlxZ3HjjjTu85WjJkiXF/Pnzi6effrp44IEHipNPPrkYPXp08corr+zycTMqa62+/vWvF4sWLSqeeeaZYsmSJcUnP/nJYuDAgf9Ta1UURbF8+fJi+fLlxYQJE4qzzz67WL58ebFixYq22//85z8Xffr0Ka644oqiqampuOKKK3b69qz/5fOqKLpmrXrieVUU3bNe1Wq1bZ+GhobikksuKZYvX16sXr16l4/7v0SoE7r22muLkSNHFv369Sve//73F4sXL267berUqcVHPvKRdvsvWrSoeN/73lf069evGDVqVDF37twdbj/ssMOKurq6YvDgwcWUKVOKF154YbeOm1UZa/W5z32uaGhoKGpra4vhw4cXn/70p9v9I5PV7q5VROxwGTlyZLt9fvWrXxVjxowpamtri7Fjxxbz58/freNmVcZa9dTzqii6fr3WrFnT4T7/eT898dzqDB9zCQCJeY0aABITagBITKgBIDGhBoDEhBoAEhNqAEhMqAEgMaEGgMSEGgASE2oASEyoASCx/wcd583NVfOWWgAAAABJRU5ErkJggg==" }, - "metadata": { - "needs_background": "light" - } + "metadata": {} } ], "metadata": {} }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "source": [ - "sns.displot(hh_sars)" + "sns.displot(hh_sars)\r\n", + "plt.show()" ], "outputs": [ - { - "output_type": "execute_result", - "data": { - "text/plain": [ - "" - ] - }, - "metadata": {}, - "execution_count": 27 - }, { "output_type": "display_data", "data": { "text/plain": [ - "
" + "
" ], - "image/png": "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" + "image/svg+xml": "\r\n\r\n\r\n \r\n \r\n \r\n \r\n 2021-08-27T13:31:16.684902\r\n image/svg+xml\r\n \r\n \r\n Matplotlib v3.4.2, https://matplotlib.org/\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n\r\n", + "image/png": "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vz3BXP9gdOtQB/OnMq/FX/3VX+mzzz4L+B+m3//+93K5XOrVq1e7x/h8PlVVVcnlcl3zebpq/la/+MUv5PP59MQTT4T8vHbN3h6Trv1XX32lG24I/GcVEREh6/I3bobsvHbN3x6Trn+r6Ohoffe731Vzc7M8Ho8eeuihkJ3XrtnbE85rv3HjRj311FPasGFDm+9ZkDr+M8Stj2n31/71zt+eYK5/UML8zWsh1/pt+kVFRdbvfvc7a968eVafPn2skydPWpZlWc8//7w1bdq0gGMqKyutyspKKzU11XrsscesyspK69NPP/XfX11dbd14443W7NmzraNHj1q//vWvrfj4eOvnP/+5f83f//3fW2VlZdbx48etffv2WT/60Y+smJgY/3ntnL/VmDFjrEcffTSo85o8u8nXfsmSJVZMTIy1ceNG6/jx49aOHTusIUOGWI888sg1n9f0+U2+/vv27bM8Ho/1hz/8wdq1a5c1ceJEKykpyfryyy+v+bwmz27Xtd+wYYMVGRlprVy5MuBHk06fPu1f85//+Z9WRESE9eqrr1pVVVXWq6++2uGPZ4X7az9U84fq+ndWtw+1ZVnWypUrrdtuu83q1auX9Zd/+ZdWeXm5/77p06db48ePD1gvqc3ttttuC1izZ88e6+6777acTqc1ePBga+nSpVZzc7P//kcffdRyuVxWVFSUNWDAAOvHP/5xu8Gxa/6jR49akqwdO3YEdV6TZzf52l+8eNF64YUXrCFDhljR0dFWYmKi9eyzzwb8j+3Vzmv6/CZf/7KyMis5OdlyOp1W3759rWnTplm1tbWdOq/Js9t17cePH9/u/NOnTw94zF/+8peW2+22oqKirKFDh1oej6dT5zV9/lBe/87gz1wCAGCwbv0ZNQAAf+4INQAABiPUAAAYjFADAGAwQg0AgMEINQAABiPUAAAYjFADAGAwQg0AgMEINQAABiPUAAAY7P8AuenhiHUiHc0AAAAASUVORK5CYII=" }, - "metadata": { - "needs_background": "light" - } + "metadata": {} } ], "metadata": {} @@ -612,7 +580,7 @@ "orig_nbformat": 4, "language_info": { "name": "python", - "version": "3.9.4", + "version": "3.9.6", "mimetype": "text/x-python", "codemirror_mode": { "name": "ipython", @@ -624,10 +592,10 @@ }, "kernelspec": { "name": "python3", - "display_name": "Python 3.9.4 64-bit (conda)" + "display_name": "Python 3.9.6 64-bit ('household-contact-tracing': conda)" }, "interpreter": { - "hash": "5e088ab515b9ff01afc6092114ac7786286eca1d23e3587660ba5a8d6e06cf28" + "hash": "cd3f1befe734ef76657bf4cb28410d324f710ae4ddab2887191ba75a6c886d82" } }, "nbformat": 4, From 50a836f15d8e975bca5d51fa64065f6a4aa90399 Mon Sep 17 00:00:00 2001 From: Martyn Fyles Date: Tue, 19 Oct 2021 14:22:27 +0100 Subject: [PATCH 69/70] multiprocessing logging fix --- household_contact_tracing/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/household_contact_tracing/__init__.py b/household_contact_tracing/__init__.py index 7344142..0ea9936 100644 --- a/household_contact_tracing/__init__.py +++ b/household_contact_tracing/__init__.py @@ -15,7 +15,7 @@ def set_up_logger(): logger.remove() logger.add(sys.stderr, format="{level} - {message}", level="INFO") logger.add("simulation.log", format="{time:YYYY-MM-DD HH:mm:ss}: {level} - {message}", - level="DEBUG", delay=True) + level="DEBUG", delay=True, enqueue = True) set_up_logger() From bf8dc90a7495e136493ca91ffd0acb9a84a5848e Mon Sep 17 00:00:00 2001 From: Martyn Fyles Date: Tue, 19 Oct 2021 14:36:39 +0100 Subject: [PATCH 70/70] renaming variable --- examples/calibration.ipynb | 6 +++--- household_contact_tracing/branching_process_models.py | 8 ++++---- household_contact_tracing/calibration.py | 2 +- household_contact_tracing/utilities.py | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/calibration.ipynb b/examples/calibration.ipynb index ff0a0d6..9f7dbd1 100644 --- a/examples/calibration.ipynb +++ b/examples/calibration.ipynb @@ -83,7 +83,7 @@ "controller = BranchingProcessController(bpm.HouseholdLevelTracing(params))\r\n", "controller.graph_view.set_display(False) # don't draw the network\r\n", "controller.csv_view.set_display(False) # don't save csv info\r\n", - "controller.run_simulation({\"max_time\": 25, \"infection_threshold\": 1e4})" + "controller.run_simulation({\"max_time\": 25, \"max_active_infections\": 1e4})" ], "outputs": [], "metadata": {} @@ -385,7 +385,7 @@ " outside_household_infectivity_scaling_range = [0.2, 0.4],\r\n", " state_criteria={\r\n", " 'max_time': 25,\r\n", - " 'infection_threshold': 1e5\r\n", + " 'max_active_infections': 1e5\r\n", " }\r\n", ")" ], @@ -600,4 +600,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} \ No newline at end of file +} diff --git a/household_contact_tracing/branching_process_models.py b/household_contact_tracing/branching_process_models.py index d0e0df2..66d599f 100644 --- a/household_contact_tracing/branching_process_models.py +++ b/household_contact_tracing/branching_process_models.py @@ -94,7 +94,7 @@ def evaluate_model_state(self, ): self.state.switch(ExtinctState, {"total_increments": self.time, "non_recovered_nodes": self.network.count_non_recovered_nodes(), "total_nodes": self.network.node_count}) - elif self.network.count_non_recovered_nodes() > self.state_criteria["infection_threshold"]: + elif self.network.count_non_recovered_nodes() > self.state_criteria["max_active_infections"]: # Simulation ends if number of infectious nodes > threshold self.state.switch(MaxNodesInfectiousState, {"total_increments": self.time, "non_recovered_nodes": 0, @@ -102,15 +102,15 @@ def evaluate_model_state(self, ): def set_default_state_criteria(self): """Set default values for the state criteria if they have not yet been set.""" - valid_state_criteria = ["max_time", "min_non_recovered_nodes", "infection_threshold"] + valid_state_criteria = ["max_time", "min_non_recovered_nodes", "max_active_infections"] for criterion in self.state_criteria: if criterion not in valid_state_criteria: raise ParameterError(f"Criterion '{criterion}', is not a valid state criterion.\n" f"Valid state criteria are: {valid_state_criteria}.") - if "infection_threshold" not in self.state_criteria: - self.state_criteria["infection_threshold"] = 10000 + if "max_active_infections" not in self.state_criteria: + self.state_criteria["max_active_infections"] = 10000 if "max_time" not in self.state_criteria: self.state_criteria["max_time"] = 40 diff --git a/household_contact_tracing/calibration.py b/household_contact_tracing/calibration.py index 5bd1b33..3ec9d84 100644 --- a/household_contact_tracing/calibration.py +++ b/household_contact_tracing/calibration.py @@ -123,7 +123,7 @@ def eval_metrics( controller_hh_sar.csv_view.set_display(False) controller_hh_sar.run_hh_sar_simulation( state_criteria = { - 'infection_threshold': math.inf, + 'max_active_infections': math.inf, 'max_time': math.inf } ) diff --git a/household_contact_tracing/utilities.py b/household_contact_tracing/utilities.py index bc19fc3..f1b560b 100644 --- a/household_contact_tracing/utilities.py +++ b/household_contact_tracing/utilities.py @@ -18,7 +18,7 @@ def run_parameterised_simulation(model_type: Type[HouseholdLevelTracing], num_st model_results = [] for param_set in processed_params: model = model_type(param_set) - model.run_simulation({"max_time": num_steps, "infection_threshold": 1000}) + model.run_simulation({"max_time": num_steps, "max_active_infections": 1000}) model_results.append(model) print(len(model_results))