From 8d20682e578f97e89029183b08b5a413771a3dae Mon Sep 17 00:00:00 2001 From: k88hudson-cfa Date: Tue, 20 Aug 2024 08:45:42 -0400 Subject: [PATCH] Edits --- examples/basic_infection/README.md | 224 +++++++++++++++++----------- examples/basic_infection/events.png | Bin 0 -> 15269 bytes 2 files changed, 141 insertions(+), 83 deletions(-) create mode 100644 examples/basic_infection/events.png diff --git a/examples/basic_infection/README.md b/examples/basic_infection/README.md index 7949825f..4ccb5df0 100644 --- a/examples/basic_infection/README.md +++ b/examples/basic_infection/README.md @@ -1,74 +1,115 @@ # Infection model: constant force of infection -The purpose of this example is to model the infection process in a homogeneous population assuming a constant force of infection, such as from the environment. - -## Entities, states and variables - -- individuals: represent people in a population who can have three states representing their infection status: susceptible, infected, or recovered. - - susceptible: individuals with this status can be infected - - infected: represents active infection. In this model, infected individuals are not able to transmit to others. After a recovery period, infected individuals update their status to recovered. - - recovered: represents recovery from infection. Individuals with this state are not able to get infected. -- variables: - - population size: number of individuals to include in the simulation - - force of infection: rate at which susceptible individuals become infected - - infection period: time that an individual spends from infection to recovery +This example demonstrates a simple model which infects a homogeneous population +assuming a constant force of infection. In other words, all individuals have the same +characteristics, and infections are caused by something like a food-borne disease + and not from interactions between individuals. ## Simulation overview -The first infection attempt is scheduled at time 0. Infection events are scheduled to occur based on the constant force of infection. Once an infection event is scheduled, a susceptible individual is selected to be infected. After infection attempt is finished, the next infection event is scheduled based on the constant force of infection. The simulation ends after no more infection events are scheduled. +The first infection attempt is scheduled at time 0. Infection attempts are scheduled to occur based on the constant force of infection. Once an infection event is scheduled, a susceptible individual is selected to be infected. After an infection attempt is finished, the next infection event is scheduled based on the constant force of infection. The simulation ends after no more infection events are scheduled. Infected individuals schedule their recovery at time `t + infected period`. The infection status of recovered individuals remains as recovered for the rest of the simulation. +The global model parameters are as follows: +* `population_size`: number of individuals to include in the simulation +* `foi`: force of infection, rate at which susceptible individuals become infected +* `infection_period`: time that an individual spends from infection to recovery -## Main simulation -The simulation is set and run by a main simulation routine. The main routine loads the required modules and executes the simulation. The first module to be loaded is the parameters modul. Then, the population manager creates a population of individuals with unique person ids and a person property determining their infection status, which is initially set as Susceptible. After the population is created, the transmission manager module is loaded. In the transmission manager, new infections occur at a rate determined by a constant force of infection, which doesn't require interactions among individuals (e.g., food-borne disease). The transmission manager is initialized by creating a new random generator number for transmission events and scheduling an infection attempt on day 0. Each infection attempt selects a random person in the population. This random person is only infected if its current infection status is Susceptible. After an infection attempt is made, a new infection attempt is scheduled for a time drawn from an exponential distribution with mean value of `1/infection_rate`. Finally, in the main simulation routine, the infection manager module is loaded. This module is initialized by subscribing to events related to changes in person properties, particularly when a person's infection status is set to Infected (handled by `handle_infection_status_change`). The purpose of the function `handle_infection_status_change` is to schedule the recovery of an infected individual for a time `t + infection_period` where `infection_period` comes from an exponential distribution. The simulation ends when there aren't more events or plans scheduled in the context. +## Architecture +As in other `ixa` models, the simulation is managed by a central `Context` object +which loads parameters, initializes user-defined modules, and starts a callback +execution loop: -- Main ``` -load context -context.add_module(parameters); // includes force of infection, population size, and infection period. -context.add_module(population_manager); // it requires parameters module to read population size +struct Parameters { + random_seed: u64, + population_size: usize, + foi: f64, + infection_period: u64 +} + +let context = Context::new(); +context.load_parameters("config.toml") + +// Initialize modules +context.add_module(population_manager); context.add_module(transmission_manager); context.add_module(infection_manager); context.add_module(person_property_report); -//do all the steps necessary to set up the report item handler -setup_report!(context, person_property_report, "incidence_report.csv"); - +// Run the simulation context.execute(); - ``` -- parameters module -``` -parameters = struct( - population_size = 1000, - foi = 0.04, - infection_period = 5) +Individuals transition through a typical SIR pattern they where +start off as susceptible (S), are randomly selected to become infected +(I), and eventually recover (R). -init(context) { - context.create_data_container(parameters) -} +```mermaid +flowchart LR +S(Susceptible) --FoI--> I(Infected) --inf. period--> R(Recovered) ``` -- Population module: The population module depends on the person module, the parameters module, and the infection properties module. Within the population manager, a number of persons are created and given a unique person id (from 0 to `population_size`). At person creation, an infection status is defined as a property for each individual with an initial value of Susceptible, indicating that everyone in the simulation could be infected at the beginning of the simulation. The context stores the person properties as a data structure for each individual as well as an set containing the population defined by a `person_id`. +The transition is unidirectional; once recovered, individuals cannot become +suceptible again. The simulation ends when no more infection attempts or +recoveries are scheduled. -``` -// Crate a new infection_status and add it as a property -infection_status = enum(Susceptible, Infected, Recovered); -init(context) { - context.define_person_property(infection_status, default = Susceptible); +The basic structure of the model is as follows: + +* A `Population Loader`, which initializes a population and attaches an infection + status property to each individual +* A `Transmission Manager`, which attempts infections, updates the infections status when successful, and schedules the next attempt +* An `Infection Manager`, which listens for infections and schedules recoveries. +* A `Report Writer` module, which listens for infections and recoveries and writes output to csv + files. + + Note that this will require some kind of parameter loading utility +from `ixa` which reads from a config file / command line arguments, +and exposes values to modules as global properties. + +### People and person properties +When the `Population Loader` module initializes, a number of +persons are created and given a unique person id (from `0` to `population_size`). +This functionality is provided by an `add_person` method from `ixa`, which adds +them to a `People` data container. + +In order to record the infection status of a person, we use another `ixa` utility +for defining "person properties". Internally, this associates each person in +the `People` data container with an enum value and and provides an API for modules +to read it, change it, or subscribe to events +when the property is changed somewhere else in the system. - for (person_id in 0..parameters.get_parameter(population_size)) { - context.create_person(person_id = person_id) - } -} ``` +infection_status = enum( + Susceptible, + Infected, + Recovered +); + +for (person_id in 0..parameters.get_parameter(population_size)) { + context.create_person(person_id = person_id) +} -- Transmission manager module: +context.define_person_property( + infection_status, + default = Susceptible +); ``` -dependencies: context, parameters, random number generator, person infection status, person, population manager; -//methods +When initialized, each person is assigned an default state (`Susceptible`). + +Once the population has been created, all modules have been initialized, and event listeners have been registered (more on this below), the simulation is ready +to begin the execution loop. + +### Scheduling infections and recoveries +In this model, the `Transmission Manager` module begins the simulation by adding a +infection attempt `plan`, which is just a callback scheduled to execute at +`current_time = 0`. The callback randomly selects a person and transitions +them to infected if they are susceptible; if they are not susceptible, +it will skip over them. Finally, a new infection attempt is scheduled for + a time drawn from an exponential distribution with mean value of + `1/infection_rate`. +``` fn attempt_infection(context) { transmission_rng = rng.get_rng(id = transmission); population = context.get_population(); @@ -88,15 +129,32 @@ init(context) { context.add_rng(id = transmission); context.add_plan(attempt_infection(context), time = 0); } - ``` -- Infection manager module: At initialization, this module adds a new random number generator and subscribes to events generated by changes in infection status. This module handles infection events by scheduling recovery for infected individuals. -``` -dependencies: random number generator, person infection status, person, context, parameters; +Note that this makes use of the following `ixa` functionality: + +* The getters/setters provided by `person_properties`, as described in the previous + section +* An `add_plan` method to register infection attempt callbacks +* A `random` module to sample the population and generate the next infection time + +Updating the `infection_status` of a person should broadcast a mutation +event through the system, which might be structured something like the following: + +![Event diagram](events.png) + +For any person property that is registered, `ixa` stores a list of callbacks +registered by other modules. When a mutation is made to that person property, +the event manager releases an event with relevant related data +(the id of the person, the old and/or new property) to all matching registered +callbacks. -//methods -fn handle_infection_status_change(context, person_id, previous_infection_status) { +In this model, when the `disease_status` is updated to `Infected`, a handler +registered by the `Infection Manager` will be triggered, which is responsible +for scheduling recovery plans: + +``` +fn handler(context, person_id, previous_infection_status) { if (context.get_infection_status(person_id) == Infected) { infection_rng = context.get_rng(id = infection); infection_period = parameters.get_parameter(infection_period) @@ -108,23 +166,26 @@ fn handle_infection_status_change(context, person_id, previous_infection_status) //initialization init(context) { context.add_rng(id = infection); - - // This function in context should send, time, person_id, and infection status change. - context.observe_person_property_event(handle_infection_status_change); + context.observe_person_property_event::(handler); } ``` -- Infection status transitions -```mermaid -flowchart LR -S(Susceptible) --FoI--> I(Infected) --inf. period--> R(Recovered) -``` +Recovery of an infected individuals are scheduled for a time `t + infection_period` +where `infection_period` comes from an exponential distribution. This is provided +by an `rng` instance independent from the one in the `Transmission Manager`. + +### Reports + +This model includes two types of reports focused on tracking the state of the +infection status: + +1. Instantaneous report on changes in person properties, and +2. The current state of person properties reported periodically. -## Reports -This model includes two types of report focused on tracking the state of the infection status defined as a person property: 1) instantaneous report on changes in person properties, and 2) current state of person properties reported periodically. +#### Instantaneous Reports +This report requires a data structure to store instantaneous changes in person properties that will be printed to a file. For this model, only changes to +Infected or Recovered are reported. -### Report for changes in person properties (Infection Status) -This report requires a data structure to store instantaneous changes in person properties that will be printed to a file. For this model, only changes to Infected or Recover are reported. ``` //data report_data = struct(t:u64, person_property_type:Type(InfectionStatus), person_property_value:InfectionStatus, person_id:u64); @@ -145,7 +206,7 @@ fn handle_person_property_change(context, person_id, infection_status){ } ``` -### Periodic report for current value in person properties (Infection Status) +#### Periodic Reports To report the current state of each Infection Status (Susceptible, Infected, or Recovered), this report needs an additional data structure that keeps a count of individuals on each state and is updated every time an event is released due to changes in infection status. ``` person_property_counter = struct { @@ -202,22 +263,19 @@ fn report_periodic_item(context) { } ``` -## Modules description - - *parameters module*: this module manages data for parameter values for population size, force of infection, and infection period. At initialization, all parameter variables are set to a specific value. - - *person module*: contains a unique ID for each person that identifies each person in the simulation. - - *`person_infection_status` module*: this module connects each person ID with a specific value for a person's infection status, which could be one of Susceptible, Infected, or Recovered. - - *population manager module*: The population manager module is in charge of setting up the population. At initialization, the module reads the population size (N) parameter and creates N persons with unique ids (1..N) and sets their initial infection status to Susceptible. - - *transmission module*: This module is in charge of spreading the infection through the population. At initialization, it schedules an infection attempt for time 0. An infection attempt consists of choosing at random a susceptible individual from the population and setting their infection status to **infected**. Each infection attempt, also schedules the next infection attempt, which time is based on the force of infection. - - *infection manager module*: This module controls the infection status of each person. Transmission of pathogen is not handled in this module, only individual's infection status. Specifically, it handles the progression of newly infected persons by scheduling recovery, which is drawn from an exponential distribution based on the infection period parameter from the parameters module. - - *reports module*: This module reports changes in infection status. - - *random_number_generator module*: Two random number generators are required. One to control the sequence of susceptible persons drawn in the transmission module, and another to control the specific times of recovery in the infection manager module. - -## Events and observation -- Changes in the infection status of individuals release an event that are observed by the simulation context. -- Only events changing from susceptible to infected are handled by scheduling a change in infection status to recovered based on the infected period. - -## Stochasticity -Stochasticity in the simulation affects - - the timing of infections, - - the order of individuals infected, and - - times to recovery from infected individuals. +## Ixa dependencies + +The following are a summary of assumed dependencies from `ixa`: + +* `parameters` component: Loads parameters from a file or command line args, + loads them into global properties (below) +* `global_properties` component: Defines properties which can be accessible + to any module +* `person` component: Creates a `People` data container which unique ID for each person, + provides an `add_person()` method +* `person_properties` component: connects each person ID defined by the `person` + component with a specific property (e.g., `infection_status`), provides add/change/subscribe API +* `reports` component: Handles file writing, provides api for writing typed rows +* `random_number_generator` component: Provides seedable rng api to sample over + a distribution, list of person ids +* `event_manager` component: provides a global send/subscribe interface diff --git a/examples/basic_infection/events.png b/examples/basic_infection/events.png new file mode 100644 index 0000000000000000000000000000000000000000..f458cd9beb636f9834de921f36f864781a7a209a GIT binary patch literal 15269 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 x>3