Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/book/models/disease_model/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@

/target
Cargo.lock

*.csv
6 changes: 4 additions & 2 deletions docs/book/models/disease_model/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
// ANCHOR: header
mod incidence_report;
mod infection_manager;
mod people;
mod transmission_manager;

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

static POPULATION: u64 = 100;
static FORCE_OF_INFECTION: f64 = 0.1;
static MAX_TIME: f64 = 200.0;
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, _| {
Expand Down
2 changes: 0 additions & 2 deletions docs/book/models/disease_model/src/people.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* ANCHOR: all */
use ixa::prelude::*;
use ixa::trace;

Expand Down Expand Up @@ -29,4 +28,3 @@ pub fn init(context: &mut Context) {
}
}
// ANCHOR_END: init
/* ANCHOR_END: all */
10 changes: 6 additions & 4 deletions docs/book/models/disease_model/src/transmission_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ fn attempt_infection(context: &mut Context) {
context.set_property(person_to_infect, InfectionStatus::I);
}

#[allow(clippy::cast_precision_loss)]
let next_attempt_time = context.get_current_time()
+ context.sample_distr(TransmissionRng, Exp::new(FORCE_OF_INFECTION).unwrap())
/ POPULATION as f64;
let current_time = context.get_current_time();
let delay_to_next_attempt = context.sample_distr(
TransmissionRng,
Exp::new(FORCE_OF_INFECTION * POPULATION as f64).unwrap(),
);
let next_attempt_time = current_time + delay_to_next_attempt;

context.add_plan(next_attempt_time, attempt_infection);
}
Expand Down
11 changes: 7 additions & 4 deletions docs/book/src/first_model/next-steps.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,21 @@ in its entirety.
{{#rustdoc_include ../../models/disease_model/src/main.rs}}
```

Exercises:
## Exercises

1. Currently the simulation runs until `MAX_TIME` even if every single person
has been infected and has recovered. Add a check somewhere that calls
`context.shutdown()` if there is no more work for the simulation to do. Where
should this check live?
should this check live? _Hint: Use `context.query_entity_count`._
2. Analyze the data output by the incident reporter. Plot the number of people
with each `InfectionStatus` on the same axis to see how they change over the
course of the simulation. Are the curves what we expect to see given our
abstract model?
abstract model? _Hint: Remember this model has a fixed force of infection,
unlike a typical SIR model._
3. Add another property that moderates the risk of infection of the individual.
(Imagine, for example, people wearing face masks for an airborne illness.)
(Imagine, for example, that some people wash their hands more frequently.)
Give a randomly sampled subpopulation that intervention and add a check to
the transmission module to see if the person that we are attempting to infect
has that property. Change the probability of infection accordingly.
_Hint: You will probably need some new constants, a new person property, a new
random number generator, and the `Bernoulli` distribution._
13 changes: 7 additions & 6 deletions docs/book/src/first_model/people.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ a new file in the `src` directory called `people.rs`.
## Defining an Entity and Property

```rust
// people.rs

{{#rustdoc_include ../../models/disease_model/src/people.rs:define_property}}
```

Expand Down Expand Up @@ -50,11 +52,11 @@ For our `people` module, the `init()` function just inserts people into the
`Context`.

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

for _ in 0..1000 {
for _ in 0..100 {
let _: PersonId = context.add_entity(()).expect("failed to add person");
}
}
Expand All @@ -81,12 +83,12 @@ would need to explicitly specify the entity type using

## Constants

Having "magic numbers" embedded in your code, such as the constant `1000` here
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 `1000` with
top of `src/main.rs` named `POPULATION` and replace the literal `100` with
`POPULATION`:

```rust
Expand Down Expand Up @@ -115,6 +117,5 @@ 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:

```rust
//people.rs
{{#rustdoc_include ../../models/disease_model/src/people.rs:all}}
{{#rustdoc_include ../../models/disease_model/src/people.rs}}
```
2 changes: 1 addition & 1 deletion docs/book/src/first_model/reporter.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# The Incident Reporter
# The Incidence 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
Expand Down
46 changes: 27 additions & 19 deletions docs/book/src/first_model/transmission.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,42 @@ manager. Create the file `src/transmission_manager.rs` and add
use ixa::Context;

fn attempt_infection(context: &mut Context) {

// attempt an infection...
}

pub fn init(context: &mut Context) {
trace!("Initializing transmission manager");

trace!("Initializing transmission manager");
// initialize the transmission manager...
}
```

## Constants

Recall our abstract model: We assume that each susceptible person has a constant
risk of becoming infected over time, independent of past infections, expressed
as a force of infection. Mathematically, this results in an exponentially
distributed duration between infection events. So we need to represent the
constant `FORCE_OF_INFECTION` and a random number source to sample exponentially
distributed random time durations.
as a force of infection.

There are at least three ways to implement this model:

1. At the start of the simulation, schedule each person's infection. This approach
is possible because, in this model, everyone will eventually be infected, and all
infections occur independently of one another.
2. At the start of the simulation, schedule a single infection. When that infection occurs,
schedule the next infection. If, for each susceptible person, the time to infection
is exponentially distributed, then the time until the next infection of _any_
susceptible person in the simulation is also exponentially distributed, with a rate
equal to the force of infection times the number of susceptibles. Upon any one
infection, we select the next infectee at random from the remaining susceptibles
and schedule their infection.
3. Schedule infection _attempts_, occurring at a rate equal to the force of infection
times the total number of people. Upon any one infection attempt, we check if the
attempted infectee is susceptible, and, if so, infect them. We then select the next
attempted infectee at random from the entire population, and schedule their attempted
infection. Infection attempts occur at a rate equal to the force of infection times
the total number of people.

These three approaches are mathematically equivalent. Here we demonstrate the third
approach because it is the simplest to implement in ixa.

We have already dealt with constants when we defined the constant `POPULATION`
in `main.rs`. Let's define `FORCE_OF_INFECTION` right next to it. We also cap
Expand All @@ -36,14 +55,7 @@ error.

```rust
// main.rs
mod people;
mod transmission_manager;

use ixa::Context;

static POPULATION: u64 = 1000;
static FORCE_OF_INFECTION: f64 = 0.1;
static MAX_TIME: f64 = 200.0;
{{#rustdoc_include ../../models/disease_model/src/main.rs:header}}
// ...the rest of the file...
```

Expand Down Expand Up @@ -82,10 +94,6 @@ accomplishes the three tasks above. A few observations:
"empty query" `()`, which means we want to sample from the entire population.
The population will never be empty, so the result will never be `None`, and so
we just call `unwrap()` on the `Some(PersonId)` value to get the `PersonId`.
- The `#[allow(clippy::cast_precision_loss)]` is optional; without it the
compiler will warn you about converting `population` 's integral type `usize`
to the floating point type `f64`, but we know that this conversion is safe to
do in this context.
- If the sampled person is not susceptible, then the only thing this function
does is schedule the next attempt at infection.
- The time at which the next attempt is scheduled is sampled randomly from the
Expand Down
46 changes: 25 additions & 21 deletions docs/book/src/first_model/your-first-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,24 @@ just a starting point. It is _not_ intended to be:
We introduce modeling in Ixa by implementing a simple model for a food-borne
illness where infection events follow a **Poisson process**. We assume that each
susceptible person has a constant risk of becoming infected over time,
independent of past infections. The Poisson process describes events
(infections) occurring randomly in time but with a constant rate.
independent of other infections.

> [!WARNING] This is not the typical SIR model
> While this model has susceptible, infected, and recovered disease states,
> it is different from the
> [canonical "SIR" model](<https://en.wikipedia.org/wiki/Compartmental_models_(epidemiology)#The_SIR_model>).
> In this model, the risk of infection does not depend on the prevalence of
> infected persons. Put another way, the people in the model can become
> _infected_ but they are not _infectious_.

In this model, each individual susceptible person has an exponentially
distributed time until they are infected. This means the time between successive
infection events follows an exponential distribution. The rate of infection is
typically expressed as a **force of infection**, which is a measure of the risk
of a susceptible individual contracting the disease. In the case of a food-borne
illness, this force is constant, meaning each susceptible individual faces a
fixed probability per unit time of becoming infected, independent of the number
of people already infected. Infected individuals subsequently recovery and
cannot be re-infected. (Note that while this model has S, I, and R compartments,
it is different from the
[canonical "SIR" model](<https://en.wikipedia.org/wiki/Compartmental_models_(epidemiology)#The_SIR_model>).
In our simple model, the force of infection does not depend on the prevalence of
infected persons. Put another way, our "I" compartment consists merely of
_infected_ persons; they are not _infectious_.)
distributed time until they are infected. The rate of infections is referred to
as the **force of infection**, and the mean time to infection for each individual
is the inverse of the force of infection. (It follows that the time between
successive infection events is also exponentially distributed.)

Infected individuals subsequently recover and
cannot be re-infected. The times to recovery are exponentially distributed.

## High-level view of how Ixa functions

Expand All @@ -51,25 +52,28 @@ concepts we need to understand about models in Ixa are:
model and is the primary way code interacts with anything in the running
model.
2. **Timeline:** A future event list of the simulation, the timeline is a queue
of `Callback` objects -called _plans_ - that will assume control of the
of `Callback` objects, called _plans_, that will assume control of the
`Context` at a future point in time and execute the logic in the plan.
3. **Plan:** A piece of logic scheduled to execute at a certain time on the
timeline. Plans are added to the timeline through the `Context`.
4. **Agents:** Generally people in a disease model, agents are the entities that
4. **Entities:** Generally people in a disease model, the entities in the model
dynamically interact over the course of the simulation. Data can be
associated with agents as properties—"people properties" in our case.
5. **Property:** Data attached to an agent.
associated with entities as _properties_.
5. **Property:** Data attached to an entity. In our case, we have _people properties_.
6. **Module:** An organizational unit of functionality. Simulations are
constructed out of a series of interacting modules that take turns
manipulating the Context through a mutable reference. Modules store data in
manipulating the `Context` through a mutable reference. Modules store data in
the simulation using the `DataPlugin` trait that allows them to retrieve data
by type.
7. **Event:** Modules can also emit 'events' that other modules can subscribe to
7. **Event:** Modules can also emit _events_ that other modules can subscribe to
handle by event type. This allows modules to broadcast that specific things
have occurred and have other modules take turns reacting to these
occurrences. An example of an event might be a person becoming infected by a
disease.

> [!TIP]
> The term "agent" is sometimes used as a synonym for "entity."

## The organization of a model's implementation

A model in Ixa is a computer program written in the Rust programming language
Expand Down