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 People Module

In Ixa we organize our models into modules each of which is responsible for a single aspect of the model.

modules

In fact, the code of Ixa itself is organized into modules in just the same way models are.

Ixa is a framework for developing agent-based models. In most of our models, the agents will represent people. So let's create a module that is responsible for people and their properties—the data that is attached to each person. Create a new file in the src directory called people.rs.

Defining an Entity and Property

use ixa::prelude::*;
use ixa::trace;

use crate::POPULATION;

define_entity!(Person);
define_property!(
    // The type of the property
    enum InfectionStatus {
        S,
        I,
        R,
    },
    // The entity the property is associated with
    Person,
    // The property's default value for newly created `Person` entities
    default_const = InfectionStatus::S
);

/// Populates the "world" with the `POPULATION` number of people.
pub fn init(context: &mut Context) {
    trace!("Initializing people");
    for _ in 0..POPULATION {
        let _: PersonId = context.add_entity(()).expect("failed to add person");
    }
}

We have to define the Person entity before we can associate properties with it. The define_entity!(Person) macro invocation automatically defines the Person type, implements the Entity trait for Person, and creates the type alias PersonId = EntityId, which is the type we can use to represent specific instances of our entity, a single person, in our simulation.

To each person we will associate a value of the enum (short for “enumeration”) named InfectionStatus. An enum is a way to create a type that can be one of several predefined values. Here, we have three values:

  • S: Represents someone who is susceptible to infection.
  • I: Represents someone who is currently infected.
  • R: Represents someone who has recovered.

Each value in the enum corresponds to a stage in our simple model. The enum value for a person's InfectionStatus property will refer to an individual’s health status in our simulation.

The module's init() function

While not strictly enforced by Ixa, the general formula for an Ixa module is:

  1. "public" data types and functions
  2. "private" data types and functions

The init() function is how your module will insert any data into the context and set up whatever initial conditions it requires before the simulation begins. For our people module, the init() function just inserts people into the Context.

/// Populates the "world" with people.
pub fn init(context: &mut Context) {
   trace!("Initializing people");

   for _ in 0..1000 {
      let _: PersonId = context.add_entity(()).expect("failed to add person");
   }
}

The context.add_entity() method call might look a little odd, because we are not giving context any data to insert, but that is because our one and only Property was defined to have a default value of InfectionStatus::S (susceptible)—so context.add_entity() doesn't need any information to create a new person. Another odd thing is the .expect("failed to add person") method call. In more complicated scenarios adding a person can fail. We could intercept that failure if we wanted, but in this simple case we will just let the program crash with a message about the reason: "failed to add person".

Finally, the Context::add_entity method returns an entity ID wrapped in a Result, which the expect method unwraps. We can use this ID if we need to refer to this newly created person. Since we don't need it, we assign the value to the special "don't care" variable _ (underscore), which just throws the value away. Why assign it to anything, though? So that the compiler can infer that it is a Person we are creating, as opposed to some other entity we may have defined. If we just omitted the let _: PersonId = part completely, we would need to explicitly specify the entity type using turbo fish notation.

Constants

Having "magic numbers" embedded in your code, such as the constant 1000 here representing the total number of people in our model, is bad practice. What if we want to change this value later? Will we even be able to find it in all of our source code? Ixa has a formal mechanism for managing these kinds of model parameters, but for now we will just define a "static constant" near the top of src/main.rs named POPULATION and replace the literal 1000 with POPULATION:

use ixa::prelude::*;
use ixa::trace;

use crate::POPULATION;

define_entity!(Person);
define_property!(
    // The type of the property
    enum InfectionStatus {
        S,
        I,
        R,
    },
    // The entity the property is associated with
    Person,
    // The property's default value for newly created `Person` entities
    default_const = InfectionStatus::S
);

/// Populates the "world" with the `POPULATION` number of people.
pub fn init(context: &mut Context) {
    trace!("Initializing people");
    for _ in 0..POPULATION {
        let _: PersonId = context.add_entity(()).expect("failed to add person");
    }
}

Let's revisit src/main.rs:

mod incidence_report;
mod infection_manager;
mod people;
mod transmission_manager;

use ixa::{Context, error, info, run_with_args};

static POPULATION: u64 = 100;
static FORCE_OF_INFECTION: f64 = 0.1;
static MAX_TIME: f64 = 200.0;
static INFECTION_DURATION: f64 = 10.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);
        }
    }
}
  1. Your IDE might have added the mod people; line for you. If not, add it now. It tells the compiler that the people module is attached to the main module (that is, main.rs).
  2. We also need to declare our static constant for the total number of people.
  3. We need to initialize the people module.

Imports

Turning back to src/people.rs, your IDE might have been complaining to you about not being able to find things "in this scope"—or, if you are lucky, your IDE was smart enough to import the symbols you need at the top of the file automatically. The issue is that the compiler needs to know where externally defined items are coming from, so we need to have use statements at the top of the file to import those items. Here is the complete src/people.rs file:

//people.rs
use ixa::prelude::*;
use ixa::trace;

use crate::POPULATION;

define_entity!(Person);
define_property!(
    // The type of the property
    enum InfectionStatus {
        S,
        I,
        R,
    },
    // The entity the property is associated with
    Person,
    // The property's default value for newly created `Person` entities
    default_const = InfectionStatus::S
);

/// Populates the "world" with the `POPULATION` number of people.
pub fn init(context: &mut Context) {
    trace!("Initializing people");
    for _ in 0..POPULATION {
        let _: PersonId = context.add_entity(()).expect("failed to add person");
    }
}