The Infection Manager
The infection manager (infection_manager.rs) is responsible for the evolution
of an infected person after they have been infected. In this simple model, there
is only one thing for the infection manager to do: schedule the time an infected
person recovers. We've already seen how to change a person's InfectionStatus
property and how to schedule plans on the timeline in the transmission module.
But how does the infection manager know about new infections?
Events
Modules can subscribe to events. The infection manager registers a function with Ixa that will be called in response to a change in a particular property.
// in infection_manager.rs
use ixa::prelude::*;
use rand_distr::Exp;
use crate::INFECTION_DURATION;
use crate::people::{InfectionStatus, Person, PersonId};
pub type InfectionStatusEvent = PropertyChangeEvent<Person, InfectionStatus>;
define_rng!(InfectionRng);
fn schedule_recovery(context: &mut Context, person_id: PersonId) {
trace!("Scheduling recovery");
let current_time = context.get_current_time();
let sampled_infection_duration =
context.sample_distr(InfectionRng, Exp::new(1.0 / INFECTION_DURATION).unwrap());
let recovery_time = current_time + sampled_infection_duration;
context.add_plan(recovery_time, move |context| {
context.set_property(person_id, InfectionStatus::R);
});
}
fn handle_infection_status_change(context: &mut Context, event: InfectionStatusEvent) {
trace!(
"Handling infection status change from {:?} to {:?} for {:?}",
event.previous, event.current, event.entity_id
);
if event.current == InfectionStatus::I {
schedule_recovery(context, event.entity_id);
}
}
pub fn init(context: &mut Context) {
trace!("Initializing infection_manager");
context.subscribe_to_event::<InfectionStatusEvent>(handle_infection_status_change);
}
This line isn't defining a new struct or even a new type. Rather, it defines an
alias for PropertyChangeEvent<E: Entity, P: Property<E>> with the generic
types instantiated for the property we want to monitor, InfectionStatus. This
is effectively the name of the event we subscribe to in the module's init()
function:
// in infection_manager.rs
use ixa::prelude::*;
use rand_distr::Exp;
use crate::INFECTION_DURATION;
use crate::people::{InfectionStatus, Person, PersonId};
pub type InfectionStatusEvent = PropertyChangeEvent<Person, InfectionStatus>;
define_rng!(InfectionRng);
fn schedule_recovery(context: &mut Context, person_id: PersonId) {
trace!("Scheduling recovery");
let current_time = context.get_current_time();
let sampled_infection_duration =
context.sample_distr(InfectionRng, Exp::new(1.0 / INFECTION_DURATION).unwrap());
let recovery_time = current_time + sampled_infection_duration;
context.add_plan(recovery_time, move |context| {
context.set_property(person_id, InfectionStatus::R);
});
}
fn handle_infection_status_change(context: &mut Context, event: InfectionStatusEvent) {
trace!(
"Handling infection status change from {:?} to {:?} for {:?}",
event.previous, event.current, event.entity_id
);
if event.current == InfectionStatus::I {
schedule_recovery(context, event.entity_id);
}
}
pub fn init(context: &mut Context) {
trace!("Initializing infection_manager");
context.subscribe_to_event::<InfectionStatusEvent>(handle_infection_status_change);
}
The event handler is just a regular Rust function that takes a Context and an
InfectionStatusEvent, the latter of which holds the PersonId of the person
whose InfectionStatus changed, the current InfectionStatus value, and the
previous InfectionStatus value.
// in infection_manager.rs
use ixa::prelude::*;
use rand_distr::Exp;
use crate::INFECTION_DURATION;
use crate::people::{InfectionStatus, Person, PersonId};
pub type InfectionStatusEvent = PropertyChangeEvent<Person, InfectionStatus>;
define_rng!(InfectionRng);
fn schedule_recovery(context: &mut Context, person_id: PersonId) {
trace!("Scheduling recovery");
let current_time = context.get_current_time();
let sampled_infection_duration =
context.sample_distr(InfectionRng, Exp::new(1.0 / INFECTION_DURATION).unwrap());
let recovery_time = current_time + sampled_infection_duration;
context.add_plan(recovery_time, move |context| {
context.set_property(person_id, InfectionStatus::R);
});
}
fn handle_infection_status_change(context: &mut Context, event: InfectionStatusEvent) {
trace!(
"Handling infection status change from {:?} to {:?} for {:?}",
event.previous, event.current, event.entity_id
);
if event.current == InfectionStatus::I {
schedule_recovery(context, event.entity_id);
}
}
pub fn init(context: &mut Context) {
trace!("Initializing infection_manager");
context.subscribe_to_event::<InfectionStatusEvent>(handle_infection_status_change);
}
We only care about new infections in this model.
Scheduling Recovery
As in attempt_infection(), we sample the recovery time from the exponential
distribution with mean INFECTION_DURATION, a constant we define in main.rs.
We define a random number source for this module's exclusive use with
define_rng!(InfectionRng) as we did before.
use ixa::prelude::*;
use rand_distr::Exp;
use crate::INFECTION_DURATION;
use crate::people::{InfectionStatus, Person, PersonId};
pub type InfectionStatusEvent = PropertyChangeEvent<Person, InfectionStatus>;
define_rng!(InfectionRng);
fn schedule_recovery(context: &mut Context, person_id: PersonId) {
trace!("Scheduling recovery");
let current_time = context.get_current_time();
let sampled_infection_duration =
context.sample_distr(InfectionRng, Exp::new(1.0 / INFECTION_DURATION).unwrap());
let recovery_time = current_time + sampled_infection_duration;
context.add_plan(recovery_time, move |context| {
context.set_property(person_id, InfectionStatus::R);
});
}
fn handle_infection_status_change(context: &mut Context, event: InfectionStatusEvent) {
trace!(
"Handling infection status change from {:?} to {:?} for {:?}",
event.previous, event.current, event.entity_id
);
if event.current == InfectionStatus::I {
schedule_recovery(context, event.entity_id);
}
}
pub fn init(context: &mut Context) {
trace!("Initializing infection_manager");
context.subscribe_to_event::<InfectionStatusEvent>(handle_infection_status_change);
}
Notice that the plan is again just a Rust function, but this time it takes the form of a closure rather than a traditionally defined function. This is convenient when the function is only a line or two.
closures and captured variables
The move keyword in the syntax for Rust closures instructs the closure to
take ownership of any variables it uses from its surrounding context—these are
known as captured variables. Normally, when a closure refers to variables
defined outside of its own body, it borrows them, which means it uses
references to those values. However, with move, the closure takes full
ownership by moving the variables into its own scope. This is especially
useful when the closure must outlive the current scope or be passed to another
thread, as it ensures that the closure has its own independent copy of the
data without relying on references that might become invalid.