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)
예제 #8
0
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)
예제 #9
0
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.'
예제 #10
0
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)
예제 #11
0
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
예제 #12
0
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
예제 #13
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)
예제 #14
0
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"
            )