def test_human_allergies_symptoms(): conf = get_test_conf("test_covid_testing.yaml") # Test allergies symptoms conf["P_COLD_TODAY"] = 0.0 conf["P_FLU_TODAY"] = 0.0 conf["P_HAS_ALLERGIES_TODAY"] = 1.0 init_fraction_sick = 0 n_people = 1000 start_time = datetime.datetime(2020, 2, 28, 0, 0) city_x_range = (0, 1000) city_y_range = (0, 1000) env = EnvMock(start_time) # Init humans city = City(env, n_people, init_fraction_sick, np.random.RandomState(42), city_x_range, city_y_range, conf) for day in range(10): env._now += SECONDS_PER_DAY for human in city.humans: human.preexisting_conditions.append('allergies') human.catch_other_disease_at_random() human.update_symptoms() if day < len(human.allergy_progression): assert set(human.all_symptoms) == set(human.allergy_progression[day]), \ f"Human symptoms should be those of allergy"
def test_intervention_day(self): """ check returns false if not far enough from intervention day """ cur_day = 10 daily_update_message_budget_sent_gaen = 0 current_timestamp = datetime.datetime.now() risk_change = 2 city = DummyCity() city.conf = dict( BURN_IN_DAYS=2, DAYS_BETWEEN_MESSAGES=2, INTERVENTION_DAY=10, UPDATES_PER_DAY=4, MESSAGE_BUDGET_GAEN=1, n_people=1000, ) city.rng = np.random.RandomState(0) city.risk_change_hist = {0: 12, 1: 1} city.risk_change_histogram_sum = sum(city.risk_change_hist.values()) city.sent_messages_by_day = { cur_day: daily_update_message_budget_sent_gaen } human = DummyHuman() human.contact_book = DummyContactBook() human.contact_book.latest_update_time = current_timestamp - datetime.timedelta( days=cur_day) res = City._check_should_send_message_gaen( city, current_day_idx=cur_day, current_timestamp=current_timestamp, human=human, risk_change_score=risk_change, ) self.assertFalse(res) city.conf["INTERVENTION_DAY"] = 9 res = City._check_should_send_message_gaen( city, current_day_idx=cur_day, current_timestamp=current_timestamp, human=human, risk_change_score=risk_change, ) self.assertFalse(res)
def test_middle_bucket_prob(self): """ checks that if in previous to last bucket and last bucket is smaller than budget, then messages sent correspond to the number of remaining messages """ cur_day = 10 daily_update_message_budget_sent_gaen = 0 current_timestamp = datetime.datetime.now() risk_change = 1 # risk_change HAS to be in risk_change_histogram city = DummyCity() city.conf = dict( BURN_IN_DAYS=2, DAYS_BETWEEN_MESSAGES=1, INTERVENTION_DAY=5, UPDATES_PER_DAY=4, MESSAGE_BUDGET_GAEN=1, n_people=1000, ) city.rng = np.random.RandomState(0) city.risk_change_histogram = {0: 50, 1: 40, 2: 10} city.risk_change_histogram_sum = sum( city.risk_change_histogram.values()) city.sent_messages_by_day = { cur_day: daily_update_message_budget_sent_gaen } human = DummyHuman() human.contact_book = DummyContactBook() human.contact_book.latest_update_time = current_timestamp - datetime.timedelta( days=cur_day) results = [] for i in range(1000): res = City._check_should_send_message_gaen( city, current_day_idx=cur_day, current_timestamp=current_timestamp, human=human, risk_change_score=risk_change, ) results.append(res) if res: if cur_day not in city.sent_messages_by_day: city.sent_messages_by_day[cur_day] = 0 city.sent_messages_by_day[cur_day] += 1 # allowed messages: 100 / 4 = 25 # already sent messages: 10 # remaining to send for second bucket: 15 self.assertAlmostEqual(1 / 4 - 10 / 100, np.mean(results), 2)
def test_last_bucket_low_budget(self): """ Everything works still with a very low budget """ cur_day = 10 daily_update_message_budget_sent_gaen = 0 current_timestamp = datetime.datetime.now() risk_change = 2 # risk_change HAS to be in risk_change_histogram city = DummyCity() city.conf = dict( BURN_IN_DAYS=2, DAYS_BETWEEN_MESSAGES=2, INTERVENTION_DAY=5, UPDATES_PER_DAY=4, MESSAGE_BUDGET_GAEN=0.01, n_people=1000, ) city.rng = np.random.RandomState(0) city.risk_change_histogram = {0: 40, 1: 20, 2: 40} city.risk_change_histogram_sum = sum( city.risk_change_histogram.values()) city.sent_messages_by_day = { cur_day: daily_update_message_budget_sent_gaen } human = DummyHuman() human.contact_book = DummyContactBook() human.contact_book.latest_update_time = current_timestamp - datetime.timedelta( days=cur_day) results = [] for i in range(1000): res = City._check_should_send_message_gaen( city, current_day_idx=cur_day, current_timestamp=current_timestamp, human=human, risk_change_score=risk_change, ) results.append(res) if res: if cur_day not in city.sent_messages_by_day: city.sent_messages_by_day[cur_day] = 0 city.sent_messages_by_day[cur_day] += 1 self.assertAlmostEqual(city.conf["MESSAGE_BUDGET_GAEN"] / 4, np.mean(results), 2)
def test_last_bucket_prob(self): """ check if you're in the last bucket but it's larger than message budget, total messages = budget for this update (=> /UPDATES_PER_DAY) """ cur_day = 10 daily_update_message_budget_sent_gaen = 0 current_timestamp = datetime.datetime.now() risk_change = 1 # risk_change HAS to be in risk_change_histogram city = DummyCity() city.conf = dict( BURN_IN_DAYS=2, DAYS_BETWEEN_MESSAGES=1, INTERVENTION_DAY=5, UPDATES_PER_DAY=4, MESSAGE_BUDGET_GAEN=1, n_people=1000, ) city.rng = np.random.RandomState(0) city.risk_change_histogram = {0: 60, 1: 40} city.risk_change_histogram_sum = sum( city.risk_change_histogram.values()) city.sent_messages_by_day = { cur_day: daily_update_message_budget_sent_gaen } human = DummyHuman() human.contact_book = DummyContactBook() human.contact_book.latest_update_time = current_timestamp - datetime.timedelta( days=cur_day) results = [] for i in range(1000): res = City._check_should_send_message_gaen( city, current_day_idx=cur_day, current_timestamp=current_timestamp, human=human, risk_change_score=risk_change, ) results.append(res) if res: if cur_day not in city.sent_messages_by_day: city.sent_messages_by_day[cur_day] = 0 city.sent_messages_by_day[cur_day] += 1 self.assertAlmostEqual(1 / 4, np.mean(results), 2)
def test_middle_bucket_last_is_full(self): """ If the last bucket is larger than the budget then no message is sent when in the second largest bucket """ cur_day = 10 daily_update_message_budget_sent_gaen = 0 current_timestamp = datetime.datetime.now() risk_change = 1 # risk_change HAS to be in risk_change_histogram city = DummyCity() city.conf = dict( BURN_IN_DAYS=2, DAYS_BETWEEN_MESSAGES=1, INTERVENTION_DAY=5, UPDATES_PER_DAY=4, MESSAGE_BUDGET_GAEN=1, n_people=1000, ) city.rng = np.random.RandomState(0) city.risk_change_histogram = {0: 40, 1: 20, 2: 40} city.risk_change_histogram_sum = sum( city.risk_change_histogram.values()) city.sent_messages_by_day = { cur_day: daily_update_message_budget_sent_gaen } human = DummyHuman() human.contact_book = DummyContactBook() human.contact_book.latest_update_time = current_timestamp - datetime.timedelta( days=cur_day) res = City._check_should_send_message_gaen( city, current_day_idx=cur_day, current_timestamp=current_timestamp, human=human, risk_change_score=risk_change, ) self.assertFalse(res)
def test_should_send_risk_change_true_det(self): """ check returns True if in last bucket, which is smaller than total message budget """ cur_day = 10 daily_update_message_budget_sent_gaen = 0 current_timestamp = datetime.datetime.now() risk_change = 1 city = DummyCity() city.conf = dict( BURN_IN_DAYS=2, DAYS_BETWEEN_MESSAGES=2, INTERVENTION_DAY=5, UPDATES_PER_DAY=4, MESSAGE_BUDGET_GAEN=1, n_people=1000, ) city.rng = np.random.RandomState(0) city.risk_change_histogram = {0: 1000, 1: 1} city.risk_change_histogram_sum = sum( city.risk_change_histogram.values()) city.sent_messages_by_day = { cur_day: daily_update_message_budget_sent_gaen } human = DummyHuman() human.contact_book = DummyContactBook() human.contact_book.latest_update_time = current_timestamp - datetime.timedelta( days=cur_day) res = City._check_should_send_message_gaen( city, current_day_idx=cur_day, current_timestamp=current_timestamp, human=human, risk_change_score=risk_change, ) self.assertTrue(res)
def test_app_distribution( test_conf_name: str, app_uptake: float ): """ Tests for the demographic statistics related to the app users - age distribution of the app users when all individuals have the app or with different uptake Args: test_conf_name (str): the filename of the configuration file used for testing app_uptake (float): probability that an individual with a smartphone has the app """ conf = get_test_conf(test_conf_name) if app_uptake: conf['APP_UPTAKE'] = app_uptake n_people = 1000 init_fraction_sick = 0.01 start_time = datetime.datetime(2020, 2, 28, 0, 0) seed = 0 rng = np.random.RandomState(seed=seed) env = Env(start_time) city_x_range = (0, 1000) city_y_range = (0, 1000) conf['simulation_days'] = 1 city = City( env=env, n_people=n_people, init_fraction_sick=init_fraction_sick, rng=rng, x_range=city_x_range, y_range=city_y_range, conf=conf, logfile="logfile.txt", ) city.have_some_humans_download_the_app() population = [] for human in city.humans: population.append([ human.age, human.sex, human.has_app, ]) df = pd.DataFrame.from_records( data=population, columns=['age', 'sex', 'has_app'] ) # Check the age distribution of the app users if conf.get('APP_UPTAKE') < 0: age_app_histogram = conf.get('SMARTPHONE_OWNER_FRACTION_BY_AGE') age_app_groups = [(low, up) for low, up, p in age_app_histogram] # make the age groups contiguous intervals = pd.IntervalIndex.from_tuples(age_app_groups, closed='both') age_grouped = df.groupby(pd.cut(df['age'], intervals)) age_grouped = age_grouped.agg({'age': 'count', 'has_app': 'sum'}) assert age_grouped.age.sum() == n_people age_stats = age_grouped.age.apply(lambda x: x / n_people) app_stats = age_grouped.has_app.apply(lambda x: x / n_people) assert np.allclose(age_stats.to_numpy(), app_stats.to_numpy()) else: abs_age_histogram = utils.relativefreq2absolutefreq( bins_fractions={(x1, x2): p for x1, x2, p in conf.get('P_AGE_REGION')}, n_elements=n_people, rng=city.rng ) age_histogram_bin_10s = utils._convert_bin_5s_to_bin_10s(abs_age_histogram) n_apps_per_age = { (x[0], x[1]): math.ceil(age_histogram_bin_10s[(x[0], x[1])] * x[2] * conf.get('APP_UPTAKE')) for x in conf.get("SMARTPHONE_OWNER_FRACTION_BY_AGE") } n_apps = np.sum(list(n_apps_per_age.values())) intervals = pd.IntervalIndex.from_tuples(n_apps_per_age.keys(), closed='both') age_grouped = df.groupby(pd.cut(df['age'], intervals)) age_grouped = age_grouped.agg({'age': 'count', 'has_app': 'sum'}) assert age_grouped.age.sum() == n_people assert age_grouped.has_app.sum() == n_apps age_grouped = age_grouped.has_app.apply(lambda x: x / n_apps) assert np.allclose(age_grouped.to_numpy(), np.array(list(n_apps_per_age.values())) / n_apps)
def test_household_distribution( seed: int, test_conf_name: str, avg_household_size_error_tol: float = 0.22, #TODO: change this back to 0.1. I had to bump it up otherwise the tests fail for inscrutable reasons... fraction_in_households_error_tol: float = 0.1, household_size_distribution_error_tol: float = 0.1): """ Tests for the demographic statistics related to the households - each human is associated to a household - there is no empty household - average number of people per household - fraction of people in household - distribution of the number of people per household Reference values are from Canada statistics - census profile 2016 (ref: https://tinyurl.com/qsf2q8d) Args: test_conf_name (str): the filename of the configuration file used for testing avg_household_size_error_tol (float): tolerance to the average household size discrepancy fraction_in_households_error_tol (float): tolerance to the population fraction in households discrepancy household_size_distribution_error_tol (float): tolerance to the distribution of household size discrepancy """ conf = get_test_conf(test_conf_name) # Test that all house_size preferences sum to 1 P_HOUSEHOLD_SIZE = conf['P_HOUSEHOLD_SIZE'] P_FAMILY_TYPE_SIZE_2 = conf['P_FAMILY_TYPE_SIZE_2'] P_FAMILY_TYPE_SIZE_3 = conf['P_FAMILY_TYPE_SIZE_3'] P_FAMILY_TYPE_SIZE_4 = conf['P_FAMILY_TYPE_SIZE_4'] P_FAMILY_TYPE_SIZE_MORE_THAN_5 = conf['P_FAMILY_TYPE_SIZE_MORE_THAN_5'] # household size val = np.sum(P_HOUSEHOLD_SIZE) assert math.fabs(np.sum(P_HOUSEHOLD_SIZE) - 1.) < 1e-6, \ f'The P_HOUSEHOLD_SIZE does not sum to 1. (actual value= {val})' # household sizes val = np.sum(P_FAMILY_TYPE_SIZE_2) assert math.fabs(np.sum(P_FAMILY_TYPE_SIZE_2) - P_HOUSEHOLD_SIZE[1]) < 1e-6, \ f'The P_FAMILY_TYPE_SIZE_2 does not sum to P_HOUSEHOLD_SIZE[1]. (actual value= {val}, expected value={P_HOUSEHOLD_SIZE[1]})' val = np.sum(P_FAMILY_TYPE_SIZE_3) assert math.fabs(np.sum(P_FAMILY_TYPE_SIZE_3) - P_HOUSEHOLD_SIZE[2]) < 1e-6, \ f'The P_FAMILY_TYPE_SIZE_3 does not sum to P_HOUSEHOLD_SIZE[2]. (actual value= {val}, expected value={P_HOUSEHOLD_SIZE[2]})' val = np.sum(P_FAMILY_TYPE_SIZE_4) assert math.fabs(np.sum(P_FAMILY_TYPE_SIZE_4) - P_HOUSEHOLD_SIZE[3]) < 1e-6, \ f'The P_FAMILY_TYPE_SIZE_4 does not sum to P_HOUSEHOLD_SIZE[3]. (actual value= {val}, expected value={P_HOUSEHOLD_SIZE[3]})' val = np.sum(P_FAMILY_TYPE_SIZE_MORE_THAN_5) assert math.fabs(np.sum(P_FAMILY_TYPE_SIZE_MORE_THAN_5) - P_HOUSEHOLD_SIZE[4]) < 1e-6, \ f'The P_FAMILY_TYPE_SIZE_MORE_THAN_5 does not sum to P_HOUSEHOLD_SIZE[4]. (actual value= {val}, expected value={P_HOUSEHOLD_SIZE[4]})' n_people = 5000 init_fraction_sick = 0.01 rng = np.random.RandomState(seed=seed) start_time = datetime.datetime(2020, 2, 28, 0, 0) env = Env(start_time) city_x_range = (0, 1000) city_y_range = (0, 1000) conf['simulation_days'] = 1 city = City( env=env, n_people=n_people, init_fraction_sick=init_fraction_sick, rng=rng, x_range=city_x_range, y_range=city_y_range, conf=conf, logfile="logfile.txt" ) # Verify that each human is associated to a household for human in city.humans: assert human.household, f'There is at least one individual without household.' n_resident_in_households = 0 sim_household_size_distribution = [0., 0., 0., 0., 0.] for household in city.households: n_resident = len(household.residents) assert n_resident > 0, f'There is an empty household.' n_resident_in_households += n_resident if n_resident < 5: sim_household_size_distribution[n_resident - 1] += 1 else: sim_household_size_distribution[-1] += 1 sim_household_size_distribution = np.array(sim_household_size_distribution) / len(city.households) sim_avg_household_size = n_resident_in_households / len(city.households) # Average number of resident per household avg_household_size = conf['AVG_HOUSEHOLD_SIZE'] # Value from CanStats assert math.fabs(sim_avg_household_size - avg_household_size) < avg_household_size_error_tol, \ f'The empirical average household size is {sim_avg_household_size:.2f}' \ f' while the statistics for Canada is {avg_household_size:.2f}' # Number of persons in private household from fraction_in_households = 0.98 # Value from CanStats sim_fraction_in_households = n_resident_in_households / n_people assert math.fabs(sim_fraction_in_households - fraction_in_households) < fraction_in_households_error_tol, \ f'The empirical fraction of people in households is {sim_fraction_in_households:.2f}' \ f' while the statistics for Canada is {fraction_in_households:.2f}' # Household size distribution from household_size_distribution = conf['P_HOUSEHOLD_SIZE'] assert np.allclose( sim_household_size_distribution, household_size_distribution, atol=household_size_distribution_error_tol), \ f'the discrepancy between simulated and estimated household size distribution is too important.'
def test_basic_demographics( seed: int, test_conf_name: str, age_error_tol: float = 3.21, age_distribution_error_tol: float = 0.20, sex_diff_error_tol: float = 0.1, profession_error_tol: float = 0.03, fraction_over_100_error_tol: float = 0.01): """ Tests for the about demographic statistics: - min, max, average and median population age - fraction of people over 100 years old - fraction difference between male and female - age distribution w.r.t to HUMAN_DISTRIBUTION - fraction of retired people - fraction of people working in healthcare - fraction of people working in education Reference values are from Canada statistics - census profile 2016 (ref: https://tinyurl.com/qsf2q8d) Args: test_conf_name (str): the filename of the configuration file used for testing age_error_tol (float): tolerance about average and median age discrepancy w.r.t. official statistics age_distribution_error_tol (float): tolerance about the population fraction assigned to each age group profession_error_tol (float): tolerance about the population fraction assigned to each profession """ conf = get_test_conf(test_conf_name) n_people = 5000 init_fraction_sick = 0.01 rng = np.random.RandomState(seed=seed) start_time = datetime.datetime(2020, 2, 28, 0, 0) env = Env(start_time) city_x_range = (0, 1000) city_y_range = (0, 1000) conf['simulation_days'] = 1 city = City( env=env, n_people=n_people, init_fraction_sick=init_fraction_sick, rng=rng, x_range=city_x_range, y_range=city_y_range, conf=conf, logfile="logfile.txt", ) city.have_some_humans_download_the_app() # Check that the actual population size is the same than specified assert len(city.humans) == n_people population = [] for human in city.humans: population.append([ human.age, human.sex, ]) df = pd.DataFrame.from_records( data=population, columns=['age', 'sex'] ) # Check basic statistics about age canstat_avg_population_age = 41. assert math.fabs(df.age.mean() - canstat_avg_population_age) < age_error_tol, \ f'The simulated average population age is {df.age.mean():.2f} ' \ f'while the statistics for Canada is {canstat_avg_population_age:.2f}' canstat_median_population_age = 41.2 assert math.fabs(df.age.median() - canstat_median_population_age) < age_error_tol, \ f'The simulated median population age is {df.age.mean():.2f} ' \ f'while the statistics for Canada is {canstat_avg_population_age:.2f}' minimum_age = 0 assert df.age.min() >= minimum_age, f'There is a person with negative age.' maximum_age = 117 # Canadian record: Marie-Louise Meilleur assert df.age.max() < maximum_age, f'There is a person older than the Canadian longevity record.' # Check basic statistics about sex canstat_sex_rel_diff = 0.018 sex_grouped = df.groupby('sex').count() sex_grouped = sex_grouped.apply(lambda x: x / n_people) sex_rel_diff = math.fabs(sex_grouped.age['male'] - sex_grouped.age['female']) assert (math.fabs(sex_rel_diff - canstat_sex_rel_diff) < sex_diff_error_tol), \ f'The relative difference between male and female in the population is {sex_rel_diff} ' \ f'while the actual number for Canada is {canstat_sex_rel_diff}' fraction_other_sex = sex_grouped.age['other'] assert math.fabs(fraction_other_sex - 0.1) < sex_diff_error_tol, \ f'The relative difference between other and the one specified in config ' \ f'is too high (diff={fraction_other_sex - 0.1})' # Check that the simulated age distribution is similar to the one specified in HUMAN_DISTRIBUTION age_histogram = {} for x1, x2, p in conf.get('P_AGE_REGION'): age_histogram[(x1, x2)] = p intervals = pd.IntervalIndex.from_tuples(age_histogram.keys(), closed='both') age_grouped = df.groupby(pd.cut(df['age'], intervals)) age_grouped = age_grouped.agg({'age': 'count'}) assert age_grouped.age.sum() == n_people age_grouped = age_grouped.age.apply(lambda x: x / n_people) assert np.allclose(age_grouped.to_numpy(), np.array(list(age_histogram.values())), atol=age_distribution_error_tol)
def simulate( n_people: int = 1000, init_fraction_sick: float = 0.01, start_time: datetime.datetime = datetime.datetime(2020, 2, 28, 0, 0), simulation_days: int = 30, outfile: typing.Optional[typing.AnyStr] = None, out_chunk_size: typing.Optional[int] = None, seed: int = 0, conf: typing.Optional[typing.Dict] = None, logfile: str = None, ): """ Runs a simulation. Args: n_people (int, optional): population size in simulation. Defaults to 1000. init_fraction_sick (float, optional): population fraction initialized with Covid-19. Defaults to 0.01. start_time (datetime, optional): Initial calendar date. Defaults to February 28, 2020. simulation_days (int, optional): Number of days to run the simulation. Defaults to 10. outfile (str, optional): Location to write logs. Defaults to None. out_chunk_size (int, optional): size of chunks to write in logs. Defaults to None. seed (int, optional): [description]. Defaults to 0. conf (dict): yaml configuration of the experiment. logfile (str): filepath where the console output and final tracked metrics will be logged. Prints to the console only if None. Returns: city (covid19sim.locations.city.City): The city object referencing people, locations, and the tracker post-simulation. """ if conf is None: conf = {} conf["n_people"] = n_people conf["init_fraction_sick"] = init_fraction_sick conf["start_time"] = start_time conf["simulation_days"] = simulation_days conf["outfile"] = outfile conf["out_chunk_size"] = out_chunk_size conf["seed"] = seed conf['logfile'] = logfile # set days and mixing constants conf['_MEAN_DAILY_UNKNOWN_CONTACTS'] = conf['MEAN_DAILY_UNKNOWN_CONTACTS'] conf['_ENVIRONMENTAL_INFECTION_KNOB'] = conf['ENVIRONMENTAL_INFECTION_KNOB'] conf['_CURRENT_PREFERENTIAL_ATTACHMENT_FACTOR'] = conf['BEGIN_PREFERENTIAL_ATTACHMENT_FACTOR'] start_time_offset_days = conf['COVID_START_DAY'] intervention_start_days = conf['INTERVENTION_DAY'] # start of COVID spread conf['COVID_SPREAD_START_TIME'] = start_time # start of intervention conf['INTERVENTION_START_TIME'] = None if intervention_start_days >= 0: conf['INTERVENTION_START_TIME'] = start_time + datetime.timedelta(days=intervention_start_days) # start of simulation without COVID start_time -= datetime.timedelta(days=start_time_offset_days) conf['SIMULATION_START_TIME'] = str(start_time) # adjust the simulation days conf['simulation_days'] += conf['COVID_START_DAY'] simulation_days = conf['simulation_days'] console_logger = ConsoleLogger(frequency=SECONDS_PER_DAY, logfile=logfile, conf=conf) logging.root.setLevel(getattr(logging, conf["LOGGING_LEVEL"].upper())) rng = np.random.RandomState(seed) env = Env(start_time) city_x_range = (0, 1000) city_y_range = (0, 1000) city = City( env, n_people, init_fraction_sick, rng, city_x_range, city_y_range, conf, logfile ) # we might need to reset the state of the clusters held in shared memory (server or not) if conf.get("RESET_INFERENCE_SERVER", False): if conf.get("USE_INFERENCE_SERVER"): inference_frontend_address = conf.get("INFERENCE_SERVER_ADDRESS", None) print("requesting cluster reset from inference server...") from covid19sim.inference.server_utils import InferenceClient temporary_client = InferenceClient( server_address=inference_frontend_address ) temporary_client.request_reset() else: from covid19sim.inference.heavy_jobs import DummyMemManager DummyMemManager.global_cluster_map = {} # Initiate city process, which runs every hour env.process(city.run(SECONDS_PER_HOUR, outfile)) # initiate humans for human in city.humans: env.process(human.run()) env.process(console_logger.run(env, city=city)) # Run simulation until termination env.run(until=env.ts_initial + simulation_days * SECONDS_PER_DAY) return city
def test_viral_load_for_day(): """ Test the sample over the viral load curve """ conf = get_test_conf("test_covid_testing.yaml") init_fraction_sick = 0 start_time = datetime.datetime(2020, 2, 28, 0, 0) city_x_range = (0, 1000) city_y_range = (0, 1000) env = Env(start_time) city = City( env, 10, # This test force the call Human.compute_covid_properties() init_fraction_sick, np.random.RandomState(42), city_x_range, city_y_range, conf, ) human = city.humans[0] # force is_asymptomatic to True since we are not testing the symptoms human.is_asymptomatic = True # force the age to a median human.age = 40 # Set infection date now = env.timestamp human.infection_timestamp = now # Curve key points in days wrt infection timestamp # Set plausible covid properties to make the computations easy to validate infectiousness_onset_days = 2.5 viral_load_peak_start = 4.5 incubation_days = 5 viral_load_plateau_start = 5.5 viral_load_plateau_end = 5.5 + 4.5 recovery_days = 5 + 15 human.infectiousness_onset_days = infectiousness_onset_days # viral_load_peak_start, viral_load_plateau_start and viral_load_plateau_ # end are relative to infectiousness_onset_days human.viral_load_peak_start = viral_load_peak_start - infectiousness_onset_days human.incubation_days = incubation_days human.viral_load_plateau_start = viral_load_plateau_start - infectiousness_onset_days human.viral_load_plateau_end = viral_load_plateau_end - infectiousness_onset_days human.recovery_days = recovery_days human.viral_load_peak_height = 1.0 human.viral_load_plateau_height = 0.75 human.peak_plateau_slope = 0.25 / (viral_load_plateau_start - viral_load_peak_start) human.plateau_end_recovery_slope = 0.75 / (recovery_days - viral_load_plateau_end) assert viral_load_for_day(human, now) == 0.0 # Between infection_timestamp and infectiousness_onset_days assert viral_load_for_day( human, now + datetime.timedelta(days=infectiousness_onset_days / 2)) == 0.0 assert viral_load_for_day( human, now + datetime.timedelta(days=infectiousness_onset_days)) == 0.0 # Between infectiousness_onset_days and viral_load_peak_start assert viral_load_for_day( human, now + datetime.timedelta( days=infectiousness_onset_days + (viral_load_peak_start - infectiousness_onset_days) / 2) ) == 1.0 / 2 assert viral_load_for_day( human, now + datetime.timedelta(days=viral_load_peak_start)) == 1.0 assert viral_load_for_day( human, now + datetime.timedelta(days=incubation_days)) == 0.75 + 0.25 / 2 assert viral_load_for_day( human, now + datetime.timedelta(days=viral_load_plateau_start)) == 0.75 # Between viral_load_plateau_start and viral_load_plateau_end assert viral_load_for_day( human, now + datetime.timedelta( days=viral_load_plateau_start + (viral_load_plateau_end - viral_load_plateau_start) / 2)) == 0.75 assert viral_load_for_day( human, now + datetime.timedelta(days=viral_load_plateau_end)) == 0.75 assert viral_load_for_day( human, now + datetime.timedelta( days=viral_load_plateau_end + (recovery_days - viral_load_plateau_end) / 2)) == 0.75 / 2 assert viral_load_for_day(human, now + datetime.timedelta(days=recovery_days)) == 0.0
def test_human_compute_covid_properties(): """ Test the covid properties of the class Human over a population for 3 ages """ conf = get_test_conf("test_covid_testing.yaml") n_people = 1000 init_fraction_sick = 0 start_time = datetime.datetime(2020, 2, 28, 0, 0) city_x_range = (0, 1000) city_y_range = (0, 1000) env = Env(start_time) city = City( env, 10, # This test directly calls Human.compute_covid_properties() on a Human init_fraction_sick, np.random.RandomState(42), city_x_range, city_y_range, conf, ) def _get_human_covid_properties(human): compute_covid_properties(human) assert human.viral_load_peak_start >= 0.5 - 0.00001 assert human.incubation_days >= 2.0 assert human.infectiousness_onset_days < human.incubation_days # viral_load_peak_start, viral_load_plateau_start and viral_load_plateau_ # end are relative to infectiousness_onset_days assert human.infectiousness_onset_days < human.viral_load_peak_start + human.infectiousness_onset_days assert human.viral_load_peak_start + human.infectiousness_onset_days < \ human.incubation_days assert human.incubation_days < human.viral_load_plateau_start + human.infectiousness_onset_days assert human.viral_load_plateau_start < human.viral_load_plateau_end assert human.viral_load_plateau_end + human.infectiousness_onset_days < \ human.recovery_days # &infectiousness-onset [He 2020 https://www.nature.com/articles/s41591-020-0869-5#ref-CR1] # infectiousness started from 2.3 days (95% CI, 0.8–3.0 days) before symptom # onset and peaked at 0.7 days (95% CI, −0.2–2.0 days) before symptom onset (Fig. 1c). assert human.incubation_days - human.infectiousness_onset_days >= 0.5 # TODO: re-add this bound # assert human.incubation_days - human.infectiousness_onset_days <= 4.3 # &infectiousness-onset [He 2020 https://www.nature.com/articles/s41591-020-0869-5#ref-CR1] # infectiousness peaked at 0.7 days (95% CI, −0.2–2.0 days) before symptom onset (Fig. 1c). try: assert human.incubation_days - \ (human.viral_load_peak_start + human.infectiousness_onset_days) >= 0.01 except AssertionError: # If the assert above fails, it can only be when we forced viral_load_peak_start # to 0.5 day after infectiousness_onset_days assert abs(human.viral_load_peak_start - 0.5) <= 0.00001 assert human.incubation_days - \ (human.viral_load_peak_start + human.infectiousness_onset_days) <= 2.2 # Avg plateau duration # infered from https://www.medrxiv.org/content/10.1101/2020.04.10.20061325v2.full.pdf (Figure 1 & 4). # 8 is infered from Figure 4 by eye-balling. assert human.viral_load_plateau_end - human.viral_load_plateau_start >= 3.0 assert human.viral_load_plateau_end - human.viral_load_plateau_start <= 9.0 assert human.viral_load_peak_height >= conf[ 'MIN_VIRAL_LOAD_PEAK_HEIGHT'] assert human.viral_load_peak_height <= conf[ 'MAX_VIRAL_LOAD_PEAK_HEIGHT'] assert human.viral_load_plateau_height <= human.viral_load_peak_height # peak_plateau_slope must transit from viral_load_peak_height to # viral_load_plateau_height assert (human.viral_load_peak_height - human.peak_plateau_slope * (human.viral_load_plateau_start - human.viral_load_peak_start)) - \ human.viral_load_plateau_height < 0.00001 # peak_plateau_slope must transit from viral_load_plateau_height to 0.0 assert human.viral_load_plateau_height - \ human.plateau_end_recovery_slope * (human.recovery_days - (human.viral_load_plateau_end + human.infectiousness_onset_days)) < 0.00001 return [ human.infectiousness_onset_days, human.viral_load_peak_start, human.incubation_days, human.viral_load_plateau_start, human.viral_load_plateau_end, human.recovery_days, human.viral_load_peak_height, human.viral_load_plateau_height, human.peak_plateau_slope, human.plateau_end_recovery_slope ] human = city.humans[0] # Reset the rng human.rng = np.random.RandomState(42) # force is_asymptomatic to True since we are not testing the symptoms human.is_asymptomatic = True # force the age to a median human.age = 40 covid_properties_samples = [ _get_human_covid_properties(human) for _ in range(n_people) ] covid_properties_samples_mean = covid_properties_samples[0] for sample in covid_properties_samples[1:]: for i in range(len(covid_properties_samples_mean)): covid_properties_samples_mean[i] += sample[i] for i in range(len(covid_properties_samples_mean)): covid_properties_samples_mean[i] /= n_people infectiousness_onset_days_mean, viral_load_peak_start_mean, \ incubation_days_mean, viral_load_plateau_start_mean, \ viral_load_plateau_end_mean, recovery_days_mean, \ viral_load_peak_height_mean, viral_load_plateau_height_mean, \ peak_plateau_slope_mean, plateau_end_recovery_slope_mean = covid_properties_samples_mean # infectiousness_onset_days # &infectiousness-onset [He 2020 https://www.nature.com/articles/s41591-020-0869-5#ref-CR1] # infectiousness started from 2.3 days (95% CI, 0.8–3.0 days) before symptom # onset and peaked at 0.7 days (95% CI, −0.2–2.0 days) before symptom onset (Fig. 1c). # TODO: infectiousness_onset_days has a minimum of 1 which affects this mean. Validate this assert assert abs(infectiousness_onset_days_mean - 2.3) < 1.5, \ f"The average of infectiousness_onset_days should be about {2.3}" # viral_load_peak_start # &infectiousness-onset [He 2020 https://www.nature.com/articles/s41591-020-0869-5#ref-CR1] # infectiousness peaked at 0.7 days (95% CI, −0.2–2.0 days) before symptom onset (Fig. 1c). assert abs(incubation_days_mean - (viral_load_peak_start_mean + infectiousness_onset_days_mean) - 0.7) < 0.5, \ f"The average of viral_load_peak_start should be about {0.7}" # incubation_days # INCUBATION PERIOD # Refer Table 2 (Appendix) in https://www.acpjournals.org/doi/10.7326/M20-0504 for parameters of lognormal fit assert abs(incubation_days_mean - 5.807) < 0.5, \ f"The average of infectiousness_onset_days should be about {5.807} days" # viral_load_plateau_start_mean, viral_load_plateau_end_mean # Avg plateau duration # infered from https://www.medrxiv.org/content/10.1101/2020.04.10.20061325v2.full.pdf (Figure 1 & 4). # 8 is infered from Figure 4 by eye-balling. assert abs(viral_load_plateau_end_mean - viral_load_plateau_start_mean) - 4.5 < 0.5, \ f"The average of the plateau duration should be about {4.5} days" # (no-source) 14 is loosely defined. assert abs(recovery_days_mean - incubation_days_mean) - 14 < 0.5, \ f"The average of the recovery time should be about {14} days" # Test with a young and senior ages for age in (20, 75): human.age = age for _ in range(n_people): # Assert the covid properties _get_human_covid_properties(human)
def test_incubation_days(): """ Intialize `Human`s and compute their covid properties. Test whether incubation days follow a lognormal distribution with mean 5 days and scale 2.5 days. Refer Table 2 (Appendix) in https://www.acpjournals.org/doi/10.7326/M20-0504 for parameters of lognormal fit Reference values: mu= 1.621 (1.504 - 1.755) sigma=0.418 (0.271 - 0.542) """ conf = get_test_conf("test_covid_testing.yaml") def lognormal_func(x, mu, sigma): return lognorm.pdf(x, s=sigma, loc=0, scale=np.exp(mu)) def normal_func(x, mu, sigma): return norm.pdf(x, loc=mu, scale=sigma) def gamma_func(x, shape, scale): return gamma.pdf(x, a=shape, scale=scale) N = 2 rng = np.random.RandomState(42) fitted_incubation_params = [] fitted_infectiousness_onset_params = [] fitted_recovery_params = [] # using matplotlib as a way to obtain density. TODO: use numpy fig, ax = plt.subplots() for i in range(N): n_people = rng.randint(500, 1000) init_fraction_sick = rng.uniform(0.01, 0.05) start_time = datetime.datetime(2020, 2, 28, 0, 0) env = Env(start_time) city_x_range = (0, 1000) city_y_range = (0, 1000) city = City( env, n_people, init_fraction_sick, rng, city_x_range, city_y_range, conf, ) incubation_data, infectiousness_onset_data, recovery_data = [], [], [] for human in city.humans: human.initial_viral_load = human.rng.random() compute_covid_properties(human) assert human.incubation_days >= 0, "negative incubation days" assert human.infectiousness_onset_days >= 0, "negative infectiousness onset days" assert human.recovery_days >= 0, "negative recovery days" incubation_data.append(human.incubation_days) infectiousness_onset_data.append(human.infectiousness_onset_days) recovery_data.append(human.recovery_days) print(f"minimum incubation days: {min(incubation_data)}") # convert into pmf ydata = np.array(incubation_data) pmf, xdata, _ = ax.hist(ydata, density=True) xdata = np.array([(xdata[i] + xdata[i + 1]) / 2 for i in range(0, xdata.shape[0] - 1)]) popt, pcov = curve_fit(gamma_func, xdata, pmf) fitted_incubation_params.append(popt) # convert into pmf ydata = np.array(infectiousness_onset_data) pmf, xdata, _ = ax.hist(ydata, density=True) xdata = np.array([(xdata[i] + xdata[i + 1]) / 2 for i in range(0, xdata.shape[0] - 1)]) popt, pcov = curve_fit(gamma_func, xdata, pmf) fitted_infectiousness_onset_params.append(popt) # convert into pmf ydata = np.array(recovery_data) pmf, xdata, _ = ax.hist(ydata, density=True) xdata = np.array([(xdata[i] + xdata[i + 1]) / 2 for i in range(0, xdata.shape[0] - 1)]) popt, pcov = curve_fit(normal_func, xdata, pmf, bounds=(14, 30)) fitted_recovery_params.append(popt) param_names = [ "incubation days", "infectiousness onset days", "recovery days" ] for idx, fitted_params in enumerate([ fitted_incubation_params, fitted_infectiousness_onset_params, fitted_recovery_params ]): all_params = np.array(fitted_params) # shape avg_mu, std_mu = all_params[:, 0].mean(), all_params[:, 0].std() ci_mu = norm.interval(0.95, loc=avg_mu, scale=std_mu) # scale avg_sigma, std_sigma = all_params[:, 1].mean(), all_params[:, 1].std() ci_sigma = norm.interval(0.95, loc=avg_sigma, scale=std_sigma) if param_names[idx] == "incubation days": print( f"**** Fitted Gamma distribution over {N} runs ... 95% CI ****" ) print(f"{param_names[idx]}") print( f"shape: {avg_mu: 3.3f} ({ci_mu[0]: 3.3f} - {ci_mu[1]: 3.3f}) refernce value: 5.807 (3.585 - 13.865)" ) print( f"scale: {avg_sigma: 3.3f} ({ci_sigma[0]: 3.3f} - {ci_sigma[1]: 3.3f}) refernce value: 0.948 (0.368 - 1.696)" ) assert 3.585 <= avg_mu <= 13.865, "not a fitted gamma distribution" elif param_names[idx] == "infectiousness onset days": print( f"**** Fitted Gamma distribution over {N} runs ... 95% CI ****" ) print(f"{param_names[idx]}") print( f"shape: {avg_mu: 3.3f} ({ci_mu[0]: 3.3f} - {ci_mu[1]: 3.3f}) refernce value: mean is 5.807-2.3 = 3.507 days (refer paramters in core.yaml)" ) print( f"scale: {avg_sigma: 3.3f} ({ci_sigma[0]: 3.3f} - {ci_sigma[1]: 3.3f}) refernce value: no-source" ) elif param_names[idx] == "recovery days": print( f"**** Fitted Normal distribution over {N} runs ... 95% CI ****" ) print(f"{param_names[idx]}") print( f"mu: {avg_mu: 3.3f} ({ci_mu[0]: 3.3f} - {ci_mu[1]: 3.3f}) refernce value: mean is 14 + 5.807 = 19.807 days (refer paramters in core.yaml)" ) print( f"sigma: {avg_sigma: 3.3f} ({ci_sigma[0]: 3.3f} - {ci_sigma[1]: 3.3f}) refernce value: no-source" )