Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The Transmission Manager

We call the module in charge of initiating new infections the transmission manager. Create the file src/transmission_manager.rs and add mod transmission_manager; to the top of src/main.rs right next to the mod people; statement. We need to flesh out this skeleton.

// transmission_manager.rs
use ixa::Context;

fn attempt_infection(context: &mut Context) {
  // attempt an infection...
}

pub fn init(context: &mut Context) {
  trace!("Initializing transmission manager");
  // initialize the transmission manager...
}

Constants

Recall our abstract model: We assume that each susceptible person has a constant risk of becoming infected over time, independent of past infections, expressed as a force of infection.

There are at least three ways to implement this model:

  1. At the start of the simulation, schedule each person's infection. This approach is possible because, in this model, everyone will eventually be infected, and all infections occur independently of one another.
  2. At the start of the simulation, schedule a single infection. When that infection occurs, schedule the next infection. If, for each susceptible person, the time to infection is exponentially distributed, then the time until the next infection of any susceptible person in the simulation is also exponentially distributed, with a rate equal to the force of infection times the number of susceptibles. Upon any one infection, we select the next infectee at random from the remaining susceptibles and schedule their infection.
  3. Schedule infection attempts, occurring at a rate equal to the force of infection times the total number of people. Upon any one infection attempt, we check if the attempted infectee is susceptible, and, if so, infect them. We then select the next attempted infectee at random from the entire population, and schedule their attempted infection. Infection attempts occur at a rate equal to the force of infection times the total number of people.

These three approaches are mathematically equivalent. Here we demonstrate the third approach because it is the simplest to implement in ixa.

We have already dealt with constants when we defined the constant POPULATION in main.rs. Let's define FORCE_OF_INFECTION right next to it. We also cap the simulation time to an arbitrarily large number, a good practice that prevents the simulation from running forever in case we make a programming error.

// main.rs
mod incidence_report;
mod infection_manager;
mod people;
mod transmission_manager;

use ixa::{error, info, run_with_args, Context};

static POPULATION: u64 = 100;
static FORCE_OF_INFECTION: f64 = 0.1;
static INFECTION_DURATION: f64 = 10.0;
static MAX_TIME: f64 = 200.0;

fn main() {
    let result = run_with_args(|context: &mut Context, _args, _| {
        // Add a plan to shut down the simulation after `max_time`, regardless of
        // what else is happening in the model.
        context.add_plan(MAX_TIME, |context| {
            context.shutdown();
        });
        people::init(context);
        transmission_manager::init(context);
        infection_manager::init(context);
        incidence_report::init(context).expect("Failed to init incidence report");
        Ok(())
    });

    match result {
        Ok(_) => {
            info!("Simulation finished executing");
        }
        Err(e) => {
            error!("Simulation exited with error: {}", e);
        }
    }
}
// ...the rest of the file...

Infection Attempts

We need to import these constants into transmission_manager. To define a new random number source in Ixa, we use define_rng!. There are other symbols from Ixa we will need for the implementation of attempt_infection(). You can have your IDE add these imports for you as you go, or you can add them yourself now.

// transmission_manager.rs
use ixa::prelude::*;
use ixa::trace;
use rand_distr::Exp;

use crate::people::{InfectionStatus, Person};
use crate::{FORCE_OF_INFECTION, POPULATION};

define_rng!(TransmissionRng);

fn attempt_infection(context: &mut Context) {
    trace!("Attempting infection");
    let person_to_infect = context.sample_entity(TransmissionRng, Person).unwrap();
    let person_status: InfectionStatus = context.get_property(person_to_infect);

    if person_status == InfectionStatus::S {
        context.set_property(person_to_infect, InfectionStatus::I);
    }

    let current_time = context.get_current_time();
    let delay_to_next_attempt = context.sample_distr(
        TransmissionRng,
        Exp::new(FORCE_OF_INFECTION * POPULATION as f64).unwrap(),
    );
    let next_attempt_time = current_time + delay_to_next_attempt;

    context.add_plan(next_attempt_time, attempt_infection);
}

pub fn init(context: &mut Context) {
    trace!("Initializing transmission manager");
    context.add_plan(0.0, attempt_infection);
}
// ...the rest of the file...

The function attempt_infection() needs to do the following:

  1. Randomly sample a person from the population to attempt to infect.
  2. Check the sampled person's current InfectionStatus, changing it to infected (InfectionStatus::I) if and only if the person is currently susceptible (InfectionStatus::S).
  3. Schedule the next infection attempt by inserting a plan into the timeline that will run attempt_infection() again.
use ixa::prelude::*;
use ixa::trace;
use rand_distr::Exp;

use crate::people::{InfectionStatus, Person};
use crate::{FORCE_OF_INFECTION, POPULATION};

define_rng!(TransmissionRng);

fn attempt_infection(context: &mut Context) {
    trace!("Attempting infection");
    let person_to_infect = context.sample_entity(TransmissionRng, Person).unwrap();
    let person_status: InfectionStatus = context.get_property(person_to_infect);

    if person_status == InfectionStatus::S {
        context.set_property(person_to_infect, InfectionStatus::I);
    }

    let current_time = context.get_current_time();
    let delay_to_next_attempt = context.sample_distr(
        TransmissionRng,
        Exp::new(FORCE_OF_INFECTION * POPULATION as f64).unwrap(),
    );
    let next_attempt_time = current_time + delay_to_next_attempt;

    context.add_plan(next_attempt_time, attempt_infection);
}

pub fn init(context: &mut Context) {
    trace!("Initializing transmission manager");
    context.add_plan(0.0, attempt_infection);
}

Read through this implementation and make sure you understand how it accomplishes the three tasks above. A few observations:

  • The method call context.sample_entity(TransmissionRng, Person) takes the name of a random number source and a query and returns an Option<PersonId>, which can have the value of Some(PersonId) or None. In this case, we use Person and no property filters, which means we want to sample from the entire population. If we wanted to, we could pass filters with the with! macro (e.g., with!(Person, Region("California"))) The population will never be empty, so the result will never be None, and so we just call unwrap() on the Some(PersonId) value to get the PersonId.
  • If the sampled person is not susceptible, then the only thing this function does is schedule the next attempt at infection.
  • The time at which the next attempt is scheduled is sampled randomly from the exponential distribution according to our abstract model and using the random number source TransmissionRng that we defined specifically for this purpose.
  • None of this code refers to the people module (except to import the types InfectionStatus and PersonId) or the infection manager we are about to write, demonstrating the software engineering principle of modularity.

random number generators

Each module generally defines its own random number source with define_rng!, avoiding interfering with the random number sources used elsewhere in the simulation in order to preserve determinism. In Monte Carlo simulations, deterministic pseudorandom number sequences are desirable because they ensure reproducibility, improve efficiency, provide control over randomness, enable consistent statistical testing, and reduce the likelihood of bias or error. These qualities are critical in scientific computing, optimization problems, and simulations that require precise and verifiable results.