Exemplo n.º 1
0
    def initialize_humans_and_locations(self):
        """
        Samples a synthetic population along with their dwellings and workplaces according to census.
        """
        self.age_histogram = relativefreq2absolutefreq(
            bins_fractions={(x[0], x[1]): x[2] for x in self.conf.get('P_AGE_REGION')},
            n_elements=self.n_people,
            rng=self.rng,
        )

        # initalize human objects
        self.humans = get_humans_with_age(self, self.age_histogram, self.conf, self.rng)

        # find best grouping to put humans together in a house
        # /!\ households are created at the time of allocation.
        # self.households is initialized within this function through calls to `self.create_location`
        self.humans = assign_households_to_humans(self.humans, self, self.conf, self.logfile)

        # assign workplace to humans
        # self.`location_type`s are created in this function
        self.humans, self = create_locations_and_assign_workplace_to_humans(self.humans, self, self.conf, self.logfile)

        # prepare schedule
        log("Preparing schedule ... ")
        start_time = datetime.datetime.now()
        # TODO - parallelize this for speedup in initialization
        for human in self.humans:
            human.mobility_planner.initialize()

        timedelta = (datetime.datetime.now() - start_time).total_seconds()
        log(f"Schedule prepared (Took {timedelta:2.3f}s)", self.logfile)
        self.hd = {human.name: human for human in self.humans}
Exemplo n.º 2
0
    def have_some_humans_download_the_app(self):
        """
        Simulates the process of downloading the app on for smartphone users according to `APP_UPTAKE` and `SMARTPHONE_OWNER_FRACTION_BY_AGE`.
        """
        def _log_app_info(human):
            if not human.has_app:
                return
            # (assumption) 90% of the time, healthcare workers will declare it
            human.obs_is_healthcare_worker = False
            if (
                human.workplace is not None
                and human.workplace.location_type == "HOSPITAL"
                and human.rng.random() < 0.9
            ):
                human.obs_is_healthcare_worker = True

            human.has_logged_info = human.rng.rand() < human.proba_report_age_and_sex
            human.obs_age = human.age if human.has_logged_info else None
            human.obs_sex = human.sex if human.has_logged_info else None
            human.obs_preexisting_conditions = human.preexisting_conditions if human.has_logged_info else []

        log("Downloading the app...", self.logfile)
        # app users
        all_has_app = self.conf.get('APP_UPTAKE') < 0
        # The dict below keeps track of an app quota for each age group
        age_histogram_bin_10s = _convert_bin_5s_to_bin_10s(self.age_histogram)
        n_apps_per_age = {
            (x[0], x[1]): math.ceil(age_histogram_bin_10s[(x[0], x[1])] * x[2] * self.conf.get('APP_UPTAKE'))
            for x in self.conf.get("SMARTPHONE_OWNER_FRACTION_BY_AGE")
        }
        for human in self.humans:
            if all_has_app:
                # i get an app, you get an app, everyone gets an app
                human.has_app = True
                _log_app_info(human)
                continue

            age_bin = human.age_bin_width_10.bin
            if n_apps_per_age[age_bin] > 0:
                # This human gets an app If there's quota left in his age group
                human.has_app = True
                _log_app_info(human)
                n_apps_per_age[age_bin] -= 1
                continue

        self.tracker.track_app_adoption()
Exemplo n.º 3
0
def main(conf: DictConfig):
    """
    Enables command line execution of the simulator.

    Args:
        conf (DictConfig): yaml configuration file
    """

    # -------------------------------------------------
    # -----  Load the experimental configuration  -----
    # -------------------------------------------------
    conf = parse_configuration(conf)

    # -------------------------------------
    # -----  Create Output Directory  -----
    # -------------------------------------
    if conf["outdir"] is None:
        conf["outdir"] = str(Path(__file__) / "output")

    timenow = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
    conf[
        "outdir"
    ] = "{}/sim_v2_people-{}_days-{}_init-{}_uptake-{}_seed-{}_{}_{}".format(
        conf["outdir"],
        conf["n_people"],
        conf["simulation_days"],
        conf["init_fraction_sick"],
        conf["APP_UPTAKE"],
        conf["seed"],
        timenow,
        str(time.time_ns())[-6:],
    )

    if Path(conf["outdir"]).exists():
        out_path = Path(conf["outdir"])
        out_idx = 1
        while (out_path.parent / (out_path.name + f"_{out_idx}")).exists():
            out_idx += 1
        conf["outdir"] = str(out_path.parent / (out_path.name + f"_{out_idx}"))

    os.makedirs(conf["outdir"])
    logfile = f"{conf['outdir']}/log_{timenow}.txt"
    outfile = os.path.join(conf["outdir"], "data")

    # ---------------------------------
    # -----  Filter-Out Warnings  -----
    # ---------------------------------
    import warnings
    # warnings.filterwarnings("ignore")

    # ----------------------------
    # -----  Run Simulation  -----
    # ----------------------------
    # correctness of configuration file
    assert not conf['RISK_MODEL'] != "" or conf['INTERVENTION_DAY'] >= 0, "risk model is given, but no intervnetion day specified"
    assert conf['N_BEHAVIOR_LEVELS'] >= 2, "At least 2 behavior levels are required to model behavior changes"
    if conf['TRACE_SYMPTOMS']:
        warnings.warn("TRACE_SYMPTOMS: True hasn't been implemented. It will have no affect.")

    log(f"RISK_MODEL = {conf['RISK_MODEL']}", logfile)
    log(f"INTERVENTION_DAY = {conf['INTERVENTION_DAY']}", logfile)
    log(f"seed: {conf['seed']}", logfile)

    # complete decsription of intervention
    type_of_run = _get_intervention_string(conf)
    conf['INTERVENTION'] = type_of_run
    log(f"Type of run: {type_of_run}", logfile)
    if conf['COLLECT_TRAINING_DATA']:
        data_output_path = os.path.join(conf["outdir"], "train.zarr")
        collection_server = DataCollectionServer(
            data_output_path=data_output_path,
            config_backup=conf,
            human_count=conf['n_people'],
            simulation_days=conf['simulation_days'],
        )
        collection_server.start()
    else:
        collection_server = None

    conf["outfile"] = outfile
    city = simulate(
        n_people=conf["n_people"],
        init_fraction_sick=conf["init_fraction_sick"],
        start_time=conf["start_time"],
        simulation_days=conf["simulation_days"],
        outfile=conf["outfile"],
        out_chunk_size=conf["out_chunk_size"],
        seed=conf["seed"],
        conf=conf,
        logfile=logfile
    )

    # write the full configuration file along with git commit hash
    dump_conf(city.conf, "{}/full_configuration.yaml".format(city.conf["outdir"]))

    # log the simulation statistics
    city.tracker.write_metrics()

    # (baseball-cards) write full simulation data
    if hasattr(city, "tracker") and \
            hasattr(city.tracker, "collection_server") and \
            isinstance(city.tracker.collection_server, DataCollectionServer) and \
            city.tracker.collection_server is not None:
        city.tracker.collection_server.stop_gracefully()
        city.tracker.collection_server.join()

    # if COLLECT_TRAINING_DATA is true
    if not conf["tune"]:
        # ----------------------------------------------
        # -----  Not Tune: Collect Training Data   -----
        # ----------------------------------------------
        # write values to train with
        train_priors = os.path.join(f"{conf['outdir']}/train_priors.pkl")
        city.tracker.write_for_training(city.humans, train_priors, conf)

        timenow = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
        log("Dumping Tracker Data in {}".format(conf["outdir"]), logfile)

        Path(conf["outdir"]).mkdir(parents=True, exist_ok=True)
        filename = f"tracker_data_n_{conf['n_people']}_seed_{conf['seed']}_{timenow}.pkl"
        data = extract_tracker_data(city.tracker, conf)
        dump_tracker_data(data, conf["outdir"], filename)
    else:
        # ------------------------------------------------------
        # -----     Tune: Write logs And Tacker Data       -----
        # ------------------------------------------------------
        timenow = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
        log("Dumping Tracker Data in {}".format(conf["outdir"]), logfile)

        Path(conf["outdir"]).mkdir(parents=True, exist_ok=True)
        filename = f"tracker_data_n_{conf['n_people']}_seed_{conf['seed']}_{timenow}.pkl"
        data = extract_tracker_data(city.tracker, conf)
        dump_tracker_data(data, conf["outdir"], filename)
    # Shutdown the data collection server if one's running
    if collection_server is not None:
        collection_server.stop_gracefully()
        collection_server.join()
        # Remove the IPCs if they were stored somewhere custom
        if os.environ.get("COVID19SIM_IPC_PATH", None) is not None:
            print("<<<<<<<< Cleaning Up >>>>>>>>")
            for file in Path(os.environ.get("COVID19SIM_IPC_PATH")).iterdir():
                if file.name.endswith(".ipc"):
                    print(f"Removing {str(file)}...")
                    os.remove(str(file))
    return conf
Exemplo n.º 4
0
    def run(self, env, city: City):
        """
        Infinite loop yields events at regular intervals. It logs and saves information to `self.logfile`.
        Args:
            env (): [description]
            city (City): [description]
        Yields:
            simpy.Environment.Timeout: Event that resumes after some specified duration.
        """
        process_start = time.time()
        n_days = 0
        while True:
            assert city.tracker.fully_initialized

            if self.print_legend:
                log(self.legend, self.logfile)
                self.print_legend = False

            # social network & mobility
            average_degree = np.mean([len(h.known_connections) for h in city.humans])
            mobility = f"| MC: {average_degree: 3.3f}"

            # simulation time related
            env_time = str(env.timestamp).split()[0]
            day = "Day {:2}:".format((city.env.timestamp - city.start_time).days)
            proc_time = "{:8}".format("({}s)".format(int(time.time() - process_start)))

            if not city.tracker.start_tracking:
                str_to_print = f"{proc_time} {day} {env_time} {mobility}"
                log(str_to_print, self.logfile)
                yield env.timeout(self.frequency)
                continue

            # test stats
            t_P = city.tracker.test_results_per_day[env.timestamp.date()]['positive']
            t_T = 0 if len(city.tracker.tested_per_day) < 2 else city.tracker.tested_per_day[-2]
            test_queue_length = len(city.covid_testing_facility.test_queue)

            # hospitalization stats
            H = sum(len(hospital.humans) for hospital in city.hospitals)
            C = sum(len(hospital.icu.humans) for hospital in city.hospitals)
            D = sum(city.tracker.deaths_per_day)

            # SEIR stats
            S = city.tracker.s_per_day[-1]
            E = city.tracker.e_per_day[-1]
            I = city.tracker.i_per_day[-1]
            R = city.tracker.r_per_day[-1]
            T = E + I + R

            # projections
            Projected3 = min(1.0*city.tracker.n_infected_init * 2 ** (n_days/3), len(city.humans))
            doubling_rate_days =  n_days / np.log2(1.0 * T / (city.tracker.n_infected_init + 1e-6))

            # other diseases
            cold = sum(h.has_cold for h in city.humans)
            allergies = sum(h.has_allergy_symptoms for h in city.humans)
            flu = sum(h.has_flu for h in city.humans)

            # intervention related
            n_quarantine = sum(h.intervened_behavior.is_under_quarantine for h in city.humans if not h.is_dead)

            # prepare string
            nd = str(len(str(city.n_people)))
            SEIR = f"| S:{S:<{nd}} E:{E:<{nd}} I:{I:<{nd}} E+I+R:{T:<{nd}} +Test:{t_P}/{t_T} TestQueue:{test_queue_length}"
            stats = f"| P3:{Projected3:5.2f}"
            stats += f" 2x:{doubling_rate_days: 2.2f}" if doubling_rate_days > 0 else ""
            other_diseases = f"| cold:{cold} allergies:{allergies} flu:{flu}"
            hospitalizations = f"| H:{H} C:{C} D:{D}"
            quarantines = f"| Q: {n_quarantine}"

            str_to_print = f"{proc_time} {day} {env_time} {SEIR} {stats} {other_diseases} {hospitalizations} {mobility} {quarantines}"
            # conditional prints
            colors, risk = "", ""
            if self.conf['INTERVENTION_DAY'] >= 0 and self.conf['RISK_MODEL'] != "":
                # on day 1, if tracker is not informed about tracing, recommended levels daily are not appended.
                # this will throw an error about list out of index.
                green, blue, orange, red = 0, 0, 0, 0
                if len(city.tracker.recommended_levels_daily) > 0:
                    green, blue, orange, red = city.tracker.recommended_levels_daily[-1]
                colors = f"| G:{green} B:{blue} O:{orange} R:{red}"

                prec, _, _ = city.tracker.risk_precision_daily[-1]
                risk = f"| RiskP:{prec[1][0]:3.2f}"

                str_to_print = f"{str_to_print} {colors} {risk}"

            log(str_to_print, self.logfile)
            yield env.timeout(self.frequency)
            n_days += 1
Exemplo n.º 5
0
    def run(self, duration, outfile):
        """
        Run the City.
        Several daily tasks take place here.
        Examples include - (1) modifying behavior of humans based on an intervention
        (2) gathering daily statistics on humans

        Args:
            duration (int): duration of a step, in seconds.
            outfile (str): may be None, the run's output file to write to

        Yields:
            simpy.Timeout
        """
        humans_notified, infections_seeded = False, False
        last_day_idx = 0
        while True:
            current_day = (self.env.timestamp - self.start_time).days

            # seed infections and change mixing constants (end of burn-in period)
            if (
                not infections_seeded
                and self.env.timestamp == self.conf['COVID_SPREAD_START_TIME']
            ):
                self._initiate_infection_spread_and_modify_mixing_if_needed()
                infections_seeded = True

            # Notify humans to follow interventions on intervention day
            if (
                not humans_notified
                and self.env.timestamp == self.conf.get('INTERVENTION_START_TIME')
            ):
                log("\n *** ****** *** ****** *** INITIATING INTERVENTION *** *** ****** *** ******\n", self.logfile)
                log(self.conf['INTERVENTION'], self.logfile)

                # if its a tracing method, load the class that can compute risk
                if self.conf['RISK_MODEL'] != "":
                    self.tracing_method = get_tracing_method(risk_model=self.conf['RISK_MODEL'], conf=self.conf)
                    self.have_some_humans_download_the_app()

                # initialize everyone from the baseline behavior
                for human in self.humans:
                    human.intervened_behavior.initialize()
                    if self.tracing_method is not None:
                        human.set_tracing_method(self.tracing_method)

                # log reduction levels
                log("\nCONTACT REDUCTION LEVELS (first one is not used) -", self.logfile)
                for location_type, value in human.intervened_behavior.reduction_levels.items():
                    log(f"{location_type}: {value} ", self.logfile)

                humans_notified = True
                if self.tracing_method is not None:
                    self.tracker.track_daily_recommendation_levels(set_tracing_started_true=True)

                # modify knobs because now people are more aware
                if self.conf['ASSUME_NO_ENVIRONMENTAL_INFECTION_AFTER_INTERVENTION_START']:
                    self.conf['_ENVIRONMENTAL_INFECTION_KNOB'] = 0.0

                if self.conf['ASSUME_NO_UNKNOWN_INTERACTIONS_AFTER_INTERVENTION_START']:
                    self.conf['_MEAN_DAILY_UNKNOWN_CONTACTS'] = 0.0

                log("\n*** *** ****** *** ****** *** ****** *** ****** *** ****** *** ****** *** ****** *** ***\n", self.logfile)
            # run city testing routine, providing test results for those who need them
            # TODO: running this every hour of the day might not be correct.
            # TODO: testing budget is used up at hour 0 if its small
            self.covid_testing_facility.clear_test_queue()

            alive_humans = []

            # run non-app-related-stuff for all humans here (test seeking, infectiousness updates)
            for human in self.humans:
                if not human.is_dead:
                    human.check_if_needs_covid_test()  # humans can decide to get tested whenever
                    human.check_covid_symptom_start()
                    human.check_covid_recovery()
                    human.fill_infectiousness_history_map(current_day)
                    alive_humans.append(human)

            # now, run app-related stuff (risk assessment, message preparation, ...)
            prev_risk_history_maps, update_messages = self.run_app(current_day, outfile, alive_humans)

            # update messages may not be sent if the distribution strategy (e.g. GAEN) chooses to filter them
            self.register_new_messages(
                current_day_idx=current_day,
                current_timestamp=self.env.timestamp,
                update_messages=update_messages,
                prev_human_risk_history_maps=prev_risk_history_maps,
                new_human_risk_history_maps={h: h.risk_history_map for h in self.humans},
            )

            # for debugging/plotting a posteriori, track all human/location attributes...
            self.tracker.track_humans(hd=self.hd, current_timestamp=self.env.timestamp)
            # self.tracker.track_locations() # TODO

            yield self.env.timeout(int(duration))
            # finally, run end-of-day activities (if possible); these include mailbox cleanups, symptom updates, ...
            if current_day != last_day_idx:
                alive_humans = [human for human in self.humans if not human.is_dead]
                last_day_idx = current_day
                if self.conf.get("DIRECT_INTERVENTION", -1) == current_day:
                    self.conf['GLOBAL_MOBILITY_SCALING_FACTOR'] = self.conf['GLOBAL_MOBILITY_SCALING_FACTOR']  / 2
                self.do_daily_activies(current_day, alive_humans)
Exemplo n.º 6
0
    def __init__(
            self,
            env: "Env",
            n_people: int,
            init_fraction_sick: float,
            rng: np.random.RandomState,
            x_range: typing.Tuple,
            y_range: typing.Tuple,
            conf: typing.Dict,
            logfile: str = None,
    ):
        """
        Constructs a city object.

        Args:
            env (simpy.Environment): Keeps track of events and their schedule
            n_people (int): Number of people in the city
            init_fraction_sick (float): fraction of population to be infected on day 0
            rng (np.random.RandomState): Random number generator
            x_range (tuple): (min_x, max_x)
            y_range (tuple): (min_y, max_y)
            human_type (covid19.simulator.Human): Class for the city's human instances
            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.
        """
        self.conf = conf
        self.logfile = logfile
        self.env = env
        self.rng = np.random.RandomState(rng.randint(2 ** 16))
        self.x_range = x_range
        self.y_range = y_range
        self.total_area = (x_range[1] - x_range[0]) * (y_range[1] - y_range[0])
        self.n_people = n_people
        self.init_fraction_sick = init_fraction_sick
        self.hash = int(time.time_ns())  # real-life time used as hash for inference server data hashing
        self.tracker = Tracker(env, self, conf, logfile)

        self.test_type_preference = list(zip(*sorted(conf.get("TEST_TYPES").items(), key=lambda x:x[1]['preference'])))[0]
        assert len(self.test_type_preference) == 1, "WARNING: Do not know how to handle multiple test types"
        self.max_capacity_per_test_type = {
            test_type: max(int(self.conf['PROPORTION_LAB_TEST_PER_DAY'] * self.n_people), 1)
            for test_type in self.test_type_preference
        }

        if 'DAILY_TARGET_REC_LEVEL_DIST' in conf:
            # QKFIX: There are 4 recommendation levels, value is hard-coded here
            self.daily_target_rec_level_dists = (np.asarray(conf['DAILY_TARGET_REC_LEVEL_DIST'], dtype=np.float_)
                                                   .reshape((-1, 4)))
        else:
            self.daily_target_rec_level_dists = None
        self.daily_rec_level_mapping = None
        self.covid_testing_facility = TestFacility(self.test_type_preference, self.max_capacity_per_test_type, env, conf)

        self.humans = []
        self.hd = {}
        self.households = OrderedSet()
        self.age_histogram = None

        log("Initializing humans ...", self.logfile)
        self.initialize_humans_and_locations()

        # self.log_static_info()
        self.tracker.track_static_info()


        log("Computing their preferences", self.logfile)
        self._compute_preferences()
        self.tracing_method = None

        # GAEN summary statistics that enable the individual to determine whether they should send their info
        self.risk_change_histogram = Counter()
        self.risk_change_histogram_sum = 0
        self.sent_messages_by_day: typing.Dict[int, int] = {}

        # note: for good efficiency in the simulator, we will not allow humans to 'download'
        # database diffs between their last timeslot and their current timeslot; instead, we
        # will give them the global mailbox object (a dictionary) and have them 'pop' all
        # messages they consume from their own (simulation-only!) personal mailbox
        self.global_mailbox: SimulatorMailboxType = defaultdict(dict)
        self.tracker.initialize()