Ejemplo n.º 1
0
    def test_get_test_conf(self):
        """
        Check tests/utils.py/get_test_conf
        """
        conf_name = "naive_local.yaml"
        self.assertIsInstance(get_test_conf(conf_name), dict)
        test_conf = get_test_conf(conf_name)

        path = Path(__file__).parent / "test_configs" / conf_name
        with path.open("r") as f:
            reference_conf = yaml.safe_load(f)

        for k in reference_conf:
            self.assertEqual(test_conf[k], reference_conf[k])
Ejemplo n.º 2
0
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"
Ejemplo n.º 3
0
    def setUp(self):
        self.conf = get_test_conf("naive_local.yaml")
        self.start_time = datetime.datetime(2020, 2, 28, 0, 0)
        self.simulation_days = 40
        self.city_x_range = (0, 1000)
        self.city_y_range = (0, 1000)
        self.rng = np.random.RandomState(42)
        self.heuristic = Heuristic(version=1, conf=self.conf)
        self.env = Env(self.start_time)
        self.city = EmptyCity(self.env, self.rng, self.city_x_range,
                              self.city_y_range, self.conf)
        try:
            if self.city.tracker is None:
                self.city.tracker = TrackerMock()
        except AttributeError:
            self.city.tracker = TrackerMock()

        self.sr = _create_senior_residences(2, self.city, self.city.rng,
                                            self.conf)

        house = Household(
            env=self.city.env,
            rng=np.random.RandomState(self.city.rng.randint(2**16)),
            conf=self.conf,
            name=f"HOUSEHOLD:{len(self.city.households)}",
            location_type="HOUSEHOLD",
            lat=self.city.rng.randint(*self.city.x_range),
            lon=self.city.rng.randint(*self.city.y_range),
            area=None,
            capacity=None,
        )

        self.human1 = Human(env=self.city.env,
                            city=self.city,
                            name=1,
                            age=42,
                            rng=self.rng,
                            conf=self.conf)
        self.human1.assign_household(house)
        self.human1.has_app = True
        self.human1._rec_level = 0
        setattr(self.human1, "_heuristic_rec_level", 0)

        self.human2 = Human(env=self.city.env,
                            city=self.city,
                            name=2,
                            age=6 * 9,
                            rng=self.rng,
                            conf=self.conf)
        self.human2.assign_household(house)
        self.human2.has_app = True
        self.human1._rec_level = 0
        setattr(self.human2, "_heuristic_rec_level", 0)

        self.humans = [self.human1, self.human2]
        self.hd = {h.name: h for h in self.humans}
    def setUp(self):
        self.config = get_test_conf(TEST_CONF_NAME)
        self.config['COLLECT_LOGS'] = True
        self.config['INTERVENTION_DAY'] = 0
        self.config['INTERVENTION'] = "Tracing"

        self.test_seed = 136
        self.n_people = 100
        self.location_start_time = datetime.datetime(2020, 2, 28, 0, 0)
        self.simulation_days = 20
Ejemplo n.º 5
0
    def test_dump_conf(self):
        """
        asserts that a conf that is dumped and parsed again yields identical results
        """

        conf = get_test_conf("naive_local.yaml")

        with TemporaryDirectory() as d:
            dump_conf(conf, Path(d) / "dumped.yaml")
            with (Path(d) / "dumped.yaml").open("r") as f:
                loaded_conf = yaml.safe_load(f)
            parsed_conf = parse_configuration(loaded_conf)
Ejemplo n.º 6
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)
Ejemplo n.º 7
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.'
Ejemplo n.º 8
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)
def test_functional_seniors_residence():
    """ Run a simulation of 1 infection in a seniors residence, and perform some sanity checks """

    with tempfile.TemporaryDirectory() as output_dir:

        rng = np.random.RandomState(42)

        # Config
        start_time = datetime.datetime(2020, 2, 28, 0, 0)
        simulation_days = 100
        city_x_range = (0, 1000)
        city_y_range = (0, 1000)

        # Find the test_configs directory, and load the required config yaml
        conf = get_test_conf("naive_local.yaml")
        conf['simulation_days'] = simulation_days
        conf['COVID_SPREAD_START_TIME'] = start_time
        conf['INTERVENTION_START_TIME'] = start_time
        conf['_MEAN_DAILY_UNKNOWN_CONTACTS'] = 0.5

        env = Env(start_time)
        city = EmptyCity(env, rng, city_x_range, city_y_range, conf)
        sr = Household(
                        env=env,
                        rng=np.random.RandomState(rng.randint(2 ** 16)),
                        conf=conf,
                        name=f"SENIOR_RESIDENCE:0",
                        location_type="SENIOR_RESIDENCE",
                        lat=rng.randint(*city_x_range),
                        lon=rng.randint(*city_y_range),
                        area=1000,
                        capacity=None,
                    )
        city.senior_residences.append(sr)

        N = 10

        # Create humans
        ages = city.rng.randint(*(65, 100), size=N)

        infection = [None] * N
        # One initial infection
        infection[0] = city.start_time
        city.n_init_infected = 1

        humans = [
            Human(
                env=city.env,
                city=city,
                name=i,
                age=ages[i],
                rng=rng,
                conf=conf,
            )
            for i in range(N)
        ]

        for human in humans:
            human.assign_household(sr)
            sr.residents.append(human)
            human.mobility_planner.initialize()

        # pick some humans and make sure they cannot recover (for later checks)
        for i in range(N):
            humans[i].never_recovers = True
        # Infect one of the humans
        humans[np.random.randint(N)]._get_infected(1)

        city.humans = humans
        city.hd = {h.name: h for h in humans}
        city.initWorld()

        outfile = os.path.join(output_dir, "test1")

        env.process(city.run(SECONDS_PER_HOUR, outfile))

        for human in city.humans:
            env.process(human.run())

        with unittest.mock.patch.object(
                City, "run_app",
                new=fake_run_app) as mock:
            env.run(until=env.ts_initial+simulation_days*SECONDS_PER_DAY)

        # Check dead humans are removed from the residence
        assert sum([h.is_dead for h in city.humans]) == N - len(sr.residents)

        # Check there are some dead
        assert sum([h.is_dead for h in city.humans]) > 0

        # Check there are no humans that are infectious
        assert not any([h.is_infectious for h in city.humans])
Ejemplo n.º 10
0
def test_track_serial_interval():
    """
    Test the various cases of serial interval tracking
    """

    with tempfile.TemporaryDirectory() as output_dir:

        rng = np.random.RandomState(42)

        # Config
        start_time = datetime.datetime(2020, 2, 28, 0, 0)
        simulation_days = 40
        city_x_range = (0, 1000)
        city_y_range = (0, 1000)

        # Find the test_configs directory, and load the required config yaml
        conf = get_test_conf("naive_local.yaml")

        env = Env(start_time)
        city = EmptyCity(env, rng, city_x_range, city_y_range, conf)

        sr =  Household(
                        env=env,
                        rng=np.random.RandomState(rng.randint(2 ** 16)),
                        conf=conf,
                        name=f"SENIOR_RESIDENCE:0",
                        location_type="SENIOR_RESIDENCE",
                        lat=rng.randint(*city_x_range),
                        lon=rng.randint(*city_y_range),
                        area=1000,
                        capacity=None,
                    )
        city.senior_residences.append(sr)

        N = 10

        # Create humans
        ages = city.rng.randint(*(65, 100), size=N)

        humans = [
            Human(
                env=city.env,
                city=city,
                name=i,
                age=ages[i],
                rng=rng,
                conf=conf,
            )
            for i in range(N)
        ]
        city.n_init_infected = 0

        for human in humans:
            human.assign_household(sr)
            sr.residents.append(human)

        city.humans = humans
        city.initWorld()

        t = Tracker(env, city, conf, None)
        t.start_tracking = True
        # Create some infections
        infections = [(0,1), (0,2), (1,3), (2,5)]
        for infector, infectee in infections:
            to_human = humans[infectee]
            from_human = humans[infector]
            t.serial_interval_book_to[to_human.name][from_human.name] = (to_human, from_human)
            t.serial_interval_book_from[from_human.name][to_human.name] = (to_human, from_human)

        # check no interval is registered for only to_human symptoms
        # or only from_human symptoms
        humans[1].covid_symptom_start_time = datetime.datetime(2020, 2, 28, 0, 0)
        t.track_serial_interval(humans[1].name)
        assert len(t.serial_intervals)==0

        humans[5].covid_symptom_start_time = (datetime.datetime(2020, 2, 28, 0, 0)+datetime.timedelta(days=4))
        t.track_serial_interval(humans[5].name)
        assert len(t.serial_intervals)==0

        # check a negative interval is registered for subsequent infector symptoms
        humans[0].covid_symptom_start_time = (datetime.datetime(2020, 2, 28, 0, 0)+datetime.timedelta(days=1))
        t.track_serial_interval(humans[0].name)
        assert len(t.serial_intervals)==1
        assert t.serial_intervals[0]==-1.0

        # check infector and infectee intervals are registered
        humans[2].covid_symptom_start_time = (datetime.datetime(2020, 2, 28, 0, 0)+datetime.timedelta(days=2))
        t.track_serial_interval(humans[2].name)
        assert len(t.serial_intervals)==3
        # Intervals (2,5) and (0,2) should be registered
        assert sorted(t.serial_intervals[-2:])==[1,2]

        # assert calling twice has no effect
        t.track_serial_interval(humans[2].name)
        assert len(t.serial_intervals)==3
        # Intervals (2,5) and (0,2) should be registered
        assert sorted(t.serial_intervals[-2:])==[1,2]

        # check what's left in the serial_interval_book_to, serial_interval_book_from
        assert humans[1].name in t.serial_interval_book_to[humans[3].name]
        assert len(t.serial_interval_book_to[humans[3].name])==1

        assert humans[3].name in t.serial_interval_book_from[humans[1].name]
        assert len(t.serial_interval_book_from[humans[1].name])==1

        #check all the others are empty
        for i in [5,0,2]:
            assert len(t.serial_interval_book_from[humans[i].name])==0
            assert len(t.serial_interval_book_to[humans[i].name])==0
Ejemplo n.º 11
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
Ejemplo n.º 12
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)
Ejemplo n.º 13
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"
            )
Ejemplo n.º 14
0
        else:
            q.append(0)
        d.append(day)
        rates.append(np.mean(q))
        days.append(" - ".join(
            [_.replace("2020-", "").replace("-", "/") for _ in d]))
    return {d: "{:.3f}%".format(r * 100) for d, r in zip(days, rates)}


if __name__ == "__main__":
    import warnings

    warnings.filterwarnings("ignore")
    # https://coronavirus.jhu.edu/testing/testing-positivity
    # https://www.canada.ca/content/dam/phac-aspc/documents/services/diseases/2019-novel-coronavirus-infection/surv-covid19-epi-update-eng.pdf
    conf = get_test_conf("test_covid_testing.yaml")
    # test_covid_test = no intervention
    outfile = None

    # ----------------------------
    # -----  Run Simulation  -----
    # ----------------------------
    n_people = 100
    simulation_days = 20
    init_fraction_sick = 0.01
    start_time = datetime.datetime(2020, 2, 28, 0, 0)
    city = simulate(
        n_people=n_people,
        start_time=start_time,
        simulation_days=simulation_days,
        outfile=outfile,