From dc71a673a7e7d5b4018f2aa4838f3663617718fd Mon Sep 17 00:00:00 2001 From: Scott Olesen Date: Thu, 19 Feb 2026 13:41:27 -0500 Subject: [PATCH 1/5] Textual tweaks --- docs/book/models/disease_model/src/main.rs | 6 ++- docs/book/models/disease_model/src/people.rs | 2 - docs/book/src/first_model/people.md | 13 +++--- docs/book/src/first_model/transmission.md | 15 ++---- docs/book/src/first_model/your-first-model.md | 46 ++++++++++--------- 5 files changed, 40 insertions(+), 42 deletions(-) diff --git a/docs/book/models/disease_model/src/main.rs b/docs/book/models/disease_model/src/main.rs index b0df0a68..d9856f99 100644 --- a/docs/book/models/disease_model/src/main.rs +++ b/docs/book/models/disease_model/src/main.rs @@ -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, _| { diff --git a/docs/book/models/disease_model/src/people.rs b/docs/book/models/disease_model/src/people.rs index 5b9cae9c..ef8f0df1 100644 --- a/docs/book/models/disease_model/src/people.rs +++ b/docs/book/models/disease_model/src/people.rs @@ -1,4 +1,3 @@ -/* ANCHOR: all */ use ixa::prelude::*; use ixa::trace; @@ -29,4 +28,3 @@ pub fn init(context: &mut Context) { } } // ANCHOR_END: init -/* ANCHOR_END: all */ diff --git a/docs/book/src/first_model/people.md b/docs/book/src/first_model/people.md index 1fcbaf96..05604180 100644 --- a/docs/book/src/first_model/people.md +++ b/docs/book/src/first_model/people.md @@ -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}} ``` @@ -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"); } } @@ -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 @@ -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}} ``` diff --git a/docs/book/src/first_model/transmission.md b/docs/book/src/first_model/transmission.md index 810dfe3d..d8aebc6b 100644 --- a/docs/book/src/first_model/transmission.md +++ b/docs/book/src/first_model/transmission.md @@ -10,12 +10,12 @@ 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... } ``` @@ -36,14 +36,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... ``` diff --git a/docs/book/src/first_model/your-first-model.md b/docs/book/src/first_model/your-first-model.md index 995919ef..51c390f0 100644 --- a/docs/book/src/first_model/your-first-model.md +++ b/docs/book/src/first_model/your-first-model.md @@ -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](). +> 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](). -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 @@ -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 From b3ac3101f06ceae7c256d26537c7670db7bf2540 Mon Sep 17 00:00:00 2001 From: Scott Olesen Date: Thu, 19 Feb 2026 14:16:56 -0500 Subject: [PATCH 2/5] Ignore .csv report --- docs/book/models/disease_model/.gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/book/models/disease_model/.gitignore b/docs/book/models/disease_model/.gitignore index f91c2acd..64a827fd 100644 --- a/docs/book/models/disease_model/.gitignore +++ b/docs/book/models/disease_model/.gitignore @@ -3,3 +3,5 @@ /target Cargo.lock + +*.csv From 8cbb1d9a9f3e2fd02a0acb2908329ec70bbd8c60 Mon Sep 17 00:00:00 2001 From: Scott Olesen Date: Thu, 19 Feb 2026 14:43:36 -0500 Subject: [PATCH 3/5] Clarify why this model works --- .../disease_model/src/transmission_manager.rs | 10 +++--- docs/book/src/first_model/transmission.md | 31 ++++++++++++++----- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/docs/book/models/disease_model/src/transmission_manager.rs b/docs/book/models/disease_model/src/transmission_manager.rs index 6da8ee9b..78b01dcc 100644 --- a/docs/book/models/disease_model/src/transmission_manager.rs +++ b/docs/book/models/disease_model/src/transmission_manager.rs @@ -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); } diff --git a/docs/book/src/first_model/transmission.md b/docs/book/src/first_model/transmission.md index d8aebc6b..0f3e25d0 100644 --- a/docs/book/src/first_model/transmission.md +++ b/docs/book/src/first_model/transmission.md @@ -23,10 +23,29 @@ pub fn init(context: &mut Context) { 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 @@ -75,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 From c1599077ba7b84d49870cd0ec6f3091b01a23fbd Mon Sep 17 00:00:00 2001 From: Scott Olesen Date: Thu, 19 Feb 2026 14:59:46 -0500 Subject: [PATCH 4/5] Exercises --- docs/book/src/first_model/next-steps.md | 9 +++++---- docs/book/src/first_model/reporter.md | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/book/src/first_model/next-steps.md b/docs/book/src/first_model/next-steps.md index 7c886927..dade05ed 100644 --- a/docs/book/src/first_model/next-steps.md +++ b/docs/book/src/first_model/next-steps.md @@ -9,18 +9,19 @@ 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. diff --git a/docs/book/src/first_model/reporter.md b/docs/book/src/first_model/reporter.md index e7f7e2e4..115e7e7d 100644 --- a/docs/book/src/first_model/reporter.md +++ b/docs/book/src/first_model/reporter.md @@ -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 From eb8b26eaafdcc5dcb4750e0f35f58595d069212f Mon Sep 17 00:00:00 2001 From: Scott Olesen Date: Thu, 19 Feb 2026 17:23:25 -0500 Subject: [PATCH 5/5] Some hints --- docs/book/src/first_model/next-steps.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/book/src/first_model/next-steps.md b/docs/book/src/first_model/next-steps.md index dade05ed..6c10b361 100644 --- a/docs/book/src/first_model/next-steps.md +++ b/docs/book/src/first_model/next-steps.md @@ -25,3 +25,5 @@ in its entirety. 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._