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 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.