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) {
}
pub fn init(context: &mut Context) {
trace!("Initializing 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. Mathematically, this results in an exponentially
distributed duration between infection events. So we need to represent the
constant FORCE_OF_INFECTION and a random number source to sample exponentially
distributed random time durations.
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 people;
mod transmission_manager;
use ixa::Context;
static POPULATION: u64 = 1000;
static FORCE_OF_INFECTION: f64 = 0.1;
static MAX_TIME: f64 = 200.0;
// ...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, PersonId};
use crate::{FORCE_OF_INFECTION, POPULATION};
define_rng!(TransmissionRng);
fn attempt_infection(context: &mut Context) {
trace!("Attempting infection");
let person_to_infect: PersonId = context.sample_entity(TransmissionRng, ()).unwrap();
let person_status: InfectionStatus = context.get_property(person_to_infect);
if person_status == InfectionStatus::S {
context.set_property(person_to_infect, InfectionStatus::I);
}
#[allow(clippy::cast_precision_loss)]
let next_attempt_time = context.get_current_time()
+ context.sample_distr(TransmissionRng, Exp::new(FORCE_OF_INFECTION).unwrap())
/ POPULATION as f64;
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, PersonId};
use crate::{FORCE_OF_INFECTION, POPULATION};
define_rng!(TransmissionRng);
fn attempt_infection(context: &mut Context) {
trace!("Attempting infection");
let person_to_infect: PersonId = context.sample_entity(TransmissionRng, ()).unwrap();
let person_status: InfectionStatus = context.get_property(person_to_infect);
if person_status == InfectionStatus::S {
context.set_property(person_to_infect, InfectionStatus::I);
}
#[allow(clippy::cast_precision_loss)]
let next_attempt_time = context.get_current_time()
+ context.sample_distr(TransmissionRng, Exp::new(FORCE_OF_INFECTION).unwrap())
/ POPULATION as f64;
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, ())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 give it the "empty query"(), which means we want to sample from the entire population. The population will never be empty, so the result will never beNone, and so we just callunwrap()on theSome(PersonId)value to get thePersonId. - The
#[allow(clippy::cast_precision_loss)]is optional; without it the compiler will warn you about convertingpopulation's integral typeusizeto the floating point typef64, but we know that this conversion is safe to do in this context. - 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.