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 Incident Reporter

An agent-based model does not output an answer at the end of a simulation in the usual sense. Rather, the simulation evolves the state of the world over time. If we want to track that evolution for later analysis, it is up to us to collect the data we want to have. The built-in report feature makes it easy to record data to a CSV file during the simulation.

Our model will only have a single report that records the current in-simulation time, the PersonId, and the InfectionStatus of a person whenever their InfectionStatus changes. We define a struct representing a single row of data.

// in incidence_report.rs
use std::path::PathBuf;

use ixa::prelude::*;
use ixa::trace;
use serde::Serialize;

use crate::infection_manager::InfectionStatusEvent;
use crate::people::{InfectionStatus, PersonId};

#[derive(Serialize, Clone)]
struct IncidenceReportItem {
    time: f64,
    person_id: PersonId,
    infection_status: InfectionStatus,
}

define_report!(IncidenceReportItem);

fn handle_infection_status_change(context: &mut Context, event: InfectionStatusEvent) {
    trace!(
        "Recording infection status change from {:?} to {:?} for {:?}",
        event.previous, event.current, event.entity_id
    );
    context.send_report(IncidenceReportItem {
        time: context.get_current_time(),
        person_id: event.entity_id,
        infection_status: event.current,
    });
}

pub fn init(context: &mut Context) -> Result<(), IxaError> {
    trace!("Initializing incidence_report");

    // Output directory is relative to the directory with the Cargo.toml file.
    let output_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));

    // In the configuration of report options below, we set `overwrite(true)`, which is not
    // recommended for production code in order to prevent accidental data loss. It is set
    // here so that newcomers won't have to deal with a confusing error while running
    // examples.
    context
        .report_options()
        .directory(output_path)
        .overwrite(true);
    context.add_report::<IncidenceReportItem>("incidence")?;
    context.subscribe_to_event::<InfectionStatusEvent>(handle_infection_status_change);
    Ok(())
}

The fact that IncidenceReportItem derives Serialize is what makes this magic work. We define a report for this struct using the define_report! macro.

use std::path::PathBuf;

use ixa::prelude::*;
use ixa::trace;
use serde::Serialize;

use crate::infection_manager::InfectionStatusEvent;
use crate::people::{InfectionStatus, PersonId};

#[derive(Serialize, Clone)]
struct IncidenceReportItem {
    time: f64,
    person_id: PersonId,
    infection_status: InfectionStatus,
}

define_report!(IncidenceReportItem);

fn handle_infection_status_change(context: &mut Context, event: InfectionStatusEvent) {
    trace!(
        "Recording infection status change from {:?} to {:?} for {:?}",
        event.previous, event.current, event.entity_id
    );
    context.send_report(IncidenceReportItem {
        time: context.get_current_time(),
        person_id: event.entity_id,
        infection_status: event.current,
    });
}

pub fn init(context: &mut Context) -> Result<(), IxaError> {
    trace!("Initializing incidence_report");

    // Output directory is relative to the directory with the Cargo.toml file.
    let output_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));

    // In the configuration of report options below, we set `overwrite(true)`, which is not
    // recommended for production code in order to prevent accidental data loss. It is set
    // here so that newcomers won't have to deal with a confusing error while running
    // examples.
    context
        .report_options()
        .directory(output_path)
        .overwrite(true);
    context.add_report::<IncidenceReportItem>("incidence")?;
    context.subscribe_to_event::<InfectionStatusEvent>(handle_infection_status_change);
    Ok(())
}

The way we listen to events is almost identical to how we did it in the infection module. First let's make the event handler, that is, the callback that will be called whenever an event is emitted.

use std::path::PathBuf;

use ixa::prelude::*;
use ixa::trace;
use serde::Serialize;

use crate::infection_manager::InfectionStatusEvent;
use crate::people::{InfectionStatus, PersonId};

#[derive(Serialize, Clone)]
struct IncidenceReportItem {
    time: f64,
    person_id: PersonId,
    infection_status: InfectionStatus,
}

define_report!(IncidenceReportItem);

fn handle_infection_status_change(context: &mut Context, event: InfectionStatusEvent) {
    trace!(
        "Recording infection status change from {:?} to {:?} for {:?}",
        event.previous, event.current, event.entity_id
    );
    context.send_report(IncidenceReportItem {
        time: context.get_current_time(),
        person_id: event.entity_id,
        infection_status: event.current,
    });
}

pub fn init(context: &mut Context) -> Result<(), IxaError> {
    trace!("Initializing incidence_report");

    // Output directory is relative to the directory with the Cargo.toml file.
    let output_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));

    // In the configuration of report options below, we set `overwrite(true)`, which is not
    // recommended for production code in order to prevent accidental data loss. It is set
    // here so that newcomers won't have to deal with a confusing error while running
    // examples.
    context
        .report_options()
        .directory(output_path)
        .overwrite(true);
    context.add_report::<IncidenceReportItem>("incidence")?;
    context.subscribe_to_event::<InfectionStatusEvent>(handle_infection_status_change);
    Ok(())
}

Just pass a IncidenceReportItem to context.send_report()! We also emit a trace log message so we can trace the execution of our model.

In the init() function there is a little bit of setup needed. Also, we can't forget to register this callback to listen to InfectionStatusEvents.

use std::path::PathBuf;

use ixa::prelude::*;
use ixa::trace;
use serde::Serialize;

use crate::infection_manager::InfectionStatusEvent;
use crate::people::{InfectionStatus, PersonId};

#[derive(Serialize, Clone)]
struct IncidenceReportItem {
    time: f64,
    person_id: PersonId,
    infection_status: InfectionStatus,
}

define_report!(IncidenceReportItem);

fn handle_infection_status_change(context: &mut Context, event: InfectionStatusEvent) {
    trace!(
        "Recording infection status change from {:?} to {:?} for {:?}",
        event.previous, event.current, event.entity_id
    );
    context.send_report(IncidenceReportItem {
        time: context.get_current_time(),
        person_id: event.entity_id,
        infection_status: event.current,
    });
}

pub fn init(context: &mut Context) -> Result<(), IxaError> {
    trace!("Initializing incidence_report");

    // Output directory is relative to the directory with the Cargo.toml file.
    let output_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));

    // In the configuration of report options below, we set `overwrite(true)`, which is not
    // recommended for production code in order to prevent accidental data loss. It is set
    // here so that newcomers won't have to deal with a confusing error while running
    // examples.
    context
        .report_options()
        .directory(output_path)
        .overwrite(true);
    context.add_report::<IncidenceReportItem>("incidence")?;
    context.subscribe_to_event::<InfectionStatusEvent>(handle_infection_status_change);
    Ok(())
}

Note that:

  • the configuration you do on context.report_options() applies to all reports attached to that context;
  • using overwrite(true) is useful for debugging but potentially devastating for production;
  • this init() function returns a result, which will be whatever error that context.add_report() returns if the CSV file cannot be created for some reason, or Ok(()) otherwise.

result and handling errors

The Rust Result<U, V> type is an enum used for error handling. It represents a value that can either be a successful outcome (Ok) containing a value of type U, or an error (Err) containing a value of type V. Think of it as a built-in way to return and propagate errors without relying on exceptions, similar to using “Either” types or special error codes in other languages.

The ? operator works with Result to simplify error handling. When you append ? to a function call that returns a Result, it automatically checks if the result is an Ok or an Err. If it’s Ok, the value is extracted; if it’s an Err, the error is immediately returned from the enclosing function. This helps keep your code concise and easy to read by reducing the need for explicit error-checking logic.

If your IDE isn't capable of adding imports for you, the external symbols we need for this module are as follows.

use std::path::PathBuf;

use ixa::prelude::*;
use ixa::trace;
use serde::Serialize;

use crate::infection_manager::InfectionStatusEvent;
use crate::people::{InfectionStatus, PersonId};

#[derive(Serialize, Clone)]
struct IncidenceReportItem {
    time: f64,
    person_id: PersonId,
    infection_status: InfectionStatus,
}

define_report!(IncidenceReportItem);

fn handle_infection_status_change(context: &mut Context, event: InfectionStatusEvent) {
    trace!(
        "Recording infection status change from {:?} to {:?} for {:?}",
        event.previous, event.current, event.entity_id
    );
    context.send_report(IncidenceReportItem {
        time: context.get_current_time(),
        person_id: event.entity_id,
        infection_status: event.current,
    });
}

pub fn init(context: &mut Context) -> Result<(), IxaError> {
    trace!("Initializing incidence_report");

    // Output directory is relative to the directory with the Cargo.toml file.
    let output_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));

    // In the configuration of report options below, we set `overwrite(true)`, which is not
    // recommended for production code in order to prevent accidental data loss. It is set
    // here so that newcomers won't have to deal with a confusing error while running
    // examples.
    context
        .report_options()
        .directory(output_path)
        .overwrite(true);
    context.add_report::<IncidenceReportItem>("incidence")?;
    context.subscribe_to_event::<InfectionStatusEvent>(handle_infection_status_change);
    Ok(())
}