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:
- 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.
- 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.
- 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:
- Randomly sample a person from the population to attempt to infect.
- Check the sampled person's current
InfectionStatus, changing it to infected (InfectionStatus::I) if and only if the person is currently susceptible (InfectionStatus::S). - 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 anOption<PersonId>, which can have the value ofSome(PersonId)orNone. In this case, we usePersonand no property filters, which means we want to sample from the entire population. If we wanted to, we could pass filters with thewith!macro (e.g.,with!(Person, Region("California"))) The population will never be empty, so the result will never beNone, and so we just callunwrap()on theSome(PersonId)value to get thePersonId. - 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
TransmissionRngthat we defined specifically for this purpose. - None of this code refers to the people module (except to import the types
InfectionStatusandPersonId) 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.