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
// 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 _ = context.add_entity(Person).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<Person>, 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:
- "public" data types and functions
- "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..100 {
let _ = context.add_entity(Person).expect("failed to add person");
}
}
We use Person here to represent a new entity with all default property values–
our one and only Property was defined to have a default value of
InfectionStatus::S (susceptible), so no additional information is needed.
The .expect("failed to add person") method call handles the case where adding
a person could 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".
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.
Constants
Having "magic numbers" embedded in your code, such as the constant 100 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 100 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 _ = context.add_entity(Person).expect("failed to add person");
}
}
Let's revisit src/main.rs:
// ANCHOR: header
mod incidence_report;
mod infection_manager;
mod people;
mod transmission_manager;
use ixa::{error, info, run_with_args, Context};
static POPULATION: u64 = 100;
static FORCE_OF_INFECTION: f64 = 0.1;
static INFECTION_DURATION: f64 = 10.0;
static MAX_TIME: f64 = 200.0;
// ANCHOR_END: header
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);
}
}
}
- Your IDE might have added the
mod people;line for you. If not, add it now. It tells the compiler that thepeoplemodule is attached to themainmodule (that is,main.rs). - We also need to declare our static constant for the total number of people.
- 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:
use ixa::prelude::*;
use ixa::trace;
use crate::POPULATION;
// ANCHOR: define_property
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
);
// ANCHOR_END: define_property
// ANCHOR: init
/// Populates the "world" with the `POPULATION` number of people.
pub fn init(context: &mut Context) {
trace!("Initializing people");
for _ in 0..POPULATION {
let _ = context.add_entity(Person).expect("failed to add person");
}
}
// ANCHOR_END: init