Skip to content

Commit

Permalink
Add global properties and example (#47)
Browse files Browse the repository at this point in the history
Closes issue #24
  • Loading branch information
confunguido authored Oct 12, 2024
1 parent 8ef8b74 commit 79c1610
Show file tree
Hide file tree
Showing 16 changed files with 603 additions and 6 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ rand = "0.8.5"
csv = "1.3"
serde = { version = "1.0", features = ["derive"] }
serde_derive = "1.0"
serde_json = "1.0.128"

[dev-dependencies]
rand_distr = "0.4.3"
Expand Down
45 changes: 45 additions & 0 deletions examples/parameter-loading/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Parameters loading in Ixa

The goal of global properties is to load values for parameters or variables that will be accessed all throughout the simulation. For this example, we will build on the basic-infection example and focus on parameters that can't change over time and are read from a config file.

```yaml
population: 1000
seed: 123
foi: 0.1
infection_duration: 5.0
```
To read parameters, we create a struct called Parameters and read from the configuration file.
```rust
use ixa::context::Context;
use ixa::global_properties::GlobalPropertiesContext;

mod global_properties;
mod people;
pub struct ParametersValues {
population: usize,
max_time: f64,
seed: u64,
foi: f64,
infection_duration:f64,
}
```
Parameters are read using a `load-parameters.rs` module which implements the method `load_parameters_from_config` and sets the parameters as a global property, which can be accessed by the other modules.

```rust
fn main() {
let mut context = Context::new();

define_global_property!(Parameters, ParametersValues);
context.load_parameters_from_config(ParameterValues, "config.yaml");

let parameters = context.get_global_property_value(Parameters);

context.add_plan(parameters.max_time, |context| {
context.shutdown();
});
print!("{:?}", parameters);
context.execute();
}
```
45 changes: 45 additions & 0 deletions examples/parameter-loading/incidence_report.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use ixa::context::Context;
use ixa::global_properties::ContextGlobalPropertiesExt;
use ixa::people::PersonPropertyChangeEvent;
use ixa::report::ContextReportExt;
use ixa::{create_report_trait, report::Report};
use std::path::PathBuf;

use crate::InfectionStatus;
use crate::InfectionStatusType;
use serde::{Deserialize, Serialize};

use crate::Parameters;

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

create_report_trait!(IncidenceReportItem);

fn handle_infection_status_change(
context: &mut Context,
event: PersonPropertyChangeEvent<InfectionStatusType>,
) {
context.send_report(IncidenceReportItem {
time: context.get_current_time(),
person_id: format!("{}", event.person_id),
infection_status: event.current,
});
}

pub fn init(context: &mut Context) {
let parameters = context.get_global_property_value(Parameters).clone();
context
.report_options()
.directory(PathBuf::from(parameters.output_dir));
context.add_report::<IncidenceReportItem>(&parameters.output_file);
context.subscribe_to_event(
|context, event: PersonPropertyChangeEvent<InfectionStatusType>| {
handle_infection_status_change(context, event);
},
);
}
95 changes: 95 additions & 0 deletions examples/parameter-loading/infection_manager.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
use ixa::context::Context;
use ixa::define_rng;
use ixa::global_properties::ContextGlobalPropertiesExt;
use ixa::people::{ContextPeopleExt, PersonId, PersonPropertyChangeEvent};
use ixa::random::ContextRandomExt;
use rand_distr::Exp;

use crate::InfectionStatus;
use crate::InfectionStatusType;
use crate::Parameters;

define_rng!(InfectionRng);

fn schedule_recovery(context: &mut Context, person_id: PersonId) {
let parameters = context.get_global_property_value(Parameters).clone();
let infection_duration = parameters.infection_duration;
let recovery_time = context.get_current_time()
+ context.sample_distr(InfectionRng, Exp::new(1.0 / infection_duration).unwrap());
context.add_plan(recovery_time, move |context| {
context.set_person_property(person_id, InfectionStatusType, InfectionStatus::R);
});
}

fn handle_infection_status_change(
context: &mut Context,
event: PersonPropertyChangeEvent<InfectionStatusType>,
) {
if matches!(event.current, InfectionStatus::I) {
schedule_recovery(context, event.person_id);
}
}

pub fn init(context: &mut Context) {
context.subscribe_to_event(
move |context, event: PersonPropertyChangeEvent<InfectionStatusType>| {
handle_infection_status_change(context, event);
},
);
}

#[cfg(test)]
mod test {
use super::*;
use ixa::context::Context;
use ixa::define_data_plugin;
use ixa::global_properties::ContextGlobalPropertiesExt;
use ixa::people::{ContextPeopleExt, PersonPropertyChangeEvent};
use ixa::random::ContextRandomExt;
define_data_plugin!(RecoveryPlugin, usize, 0);

use crate::parameters_loader::ParametersValues;

#[test]
fn test_handle_infection_change() {
let p_values = ParametersValues {
population: 10,
max_time: 10.0,
seed: 42,
foi: 0.15,
infection_duration: 5.0,
output_dir: ".".to_string(),
output_file: ".".to_string(),
};
let mut context = Context::new();

context.set_global_property_value(Parameters, p_values);
context.init_random(42);
init(&mut context);

context.subscribe_to_event(
move |context, event: PersonPropertyChangeEvent<InfectionStatusType>| {
if matches!(event.current, InfectionStatus::R) {
*context.get_data_container_mut(RecoveryPlugin) += 1;
}
},
);

let population_size: usize = 10;
for _ in 0..population_size {
let person = context.add_person();

// This is necessary to emit an event
context.get_person_property(person, InfectionStatusType);

context.add_plan(1.0, move |context| {
context.set_person_property(person, InfectionStatusType, InfectionStatus::I);
});
}
context.execute();
assert_eq!(population_size, context.get_current_population());
let recovered_size: usize = *context.get_data_container(RecoveryPlugin).unwrap();

assert_eq!(recovered_size, population_size);
}
}
9 changes: 9 additions & 0 deletions examples/parameter-loading/input.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"population": 1000,
"max_time": 200.0,
"seed": 123,
"foi": 0.15,
"infection_duration": 5.0,
"output_dir": "examples/parameter-loading",
"output_file": "incidence"
}
7 changes: 7 additions & 0 deletions examples/parameter-loading/input.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
population = 10
max_time = 20.0
seed = 123
foi = 0.15
infection_duration = 5.0
output_dir = "examples/parameter-loading"
output_file = "incidence"
55 changes: 55 additions & 0 deletions examples/parameter-loading/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use ixa::people::ContextPeopleExt;
use ixa::random::ContextRandomExt;
use ixa::{
context::Context, define_person_property, define_person_property_with_default,
global_properties::ContextGlobalPropertiesExt,
};
use std::path::Path;

mod incidence_report;
mod infection_manager;
mod parameters_loader;
mod transmission_manager;

use crate::parameters_loader::Parameters;

use serde::{Deserialize, Serialize};

#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, Serialize, Deserialize)]
pub enum InfectionStatus {
S,
I,
R,
}
define_person_property_with_default!(InfectionStatusType, InfectionStatus, InfectionStatus::S);

fn main() {
let mut context = Context::new();
let file_path = Path::new("examples")
.join("parameter-loading")
.join("input.json");

match parameters_loader::init_parameters(&mut context, &file_path) {
Ok(()) => {
let parameters = context.get_global_property_value(Parameters).clone();
context.init_random(parameters.seed);

for _ in 0..parameters.population {
context.add_person();
}

transmission_manager::init(&mut context);
infection_manager::init(&mut context);
incidence_report::init(&mut context);

context.add_plan(parameters.max_time, |context| {
context.shutdown();
});
println!("{parameters:?}");
context.execute();
}
Err(ixa_error) => {
println!("Could not read parameters: {ixa_error}");
}
}
}
26 changes: 26 additions & 0 deletions examples/parameter-loading/parameters_loader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use ixa::context::Context;
use ixa::global_properties::ContextGlobalPropertiesExt;
use std::fmt::Debug;
use std::path::Path;

use ixa::define_global_property;
use ixa::error::IxaError;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ParametersValues {
pub population: usize,
pub max_time: f64,
pub seed: u64,
pub foi: f64,
pub infection_duration: f64,
pub output_dir: String,
pub output_file: String,
}
define_global_property!(Parameters, ParametersValues);

pub fn init_parameters(context: &mut Context, file_path: &Path) -> Result<(), IxaError> {
let parameters_json = context.load_parameters_from_json::<ParametersValues>(file_path)?;
context.set_global_property_value(Parameters, parameters_json);
Ok(())
}
20 changes: 20 additions & 0 deletions examples/parameter-loading/plot_output.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
library(tidyverse)
library(jsonlite)
dir <- file.path("examples", "parameter-loading")
params <- read_json(file.path(dir, "input.json"))
population <- params$population
foi <- params$foi

output_df <- read_csv(file.path(dir, "incidence.csv")) |>
dplyr::filter(infection_status == "I") |>
group_by(time) |>
mutate(inf = n()) |>
ungroup() |>
mutate(inf = cumsum(inf))

time_array <- 0:ceiling(max(output_df$time))

expected_susc <- population * exp(-foi * time_array)

plot(output_df$time, population - output_df$inf, ylim = c(0,population))
lines(time_array, expected_susc, col = "red")
78 changes: 78 additions & 0 deletions examples/parameter-loading/transmission_manager.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use ixa::context::Context;
use ixa::define_rng;
use ixa::global_properties::ContextGlobalPropertiesExt;
use ixa::people::ContextPeopleExt;
use ixa::random::ContextRandomExt;

use crate::InfectionStatus;
use crate::InfectionStatusType;
use crate::Parameters;
use rand_distr::Exp;

define_rng!(TransmissionRng);

fn attempt_infection(context: &mut Context) {
let population_size: usize = context.get_current_population();
let person_to_infect =
context.get_person_id(context.sample_range(TransmissionRng, 0..population_size));
let person_status: InfectionStatus =
context.get_person_property(person_to_infect, InfectionStatusType);
let parameters = context.get_global_property_value(Parameters).clone();

if matches!(person_status, InfectionStatus::S) {
context.set_person_property(person_to_infect, InfectionStatusType, InfectionStatus::I);
}

// With a food-borne illness (i.e., constant force of infection),
// each _person_ experiences an exponentially distributed
// time until infected. Here, we use a per-person force of infection derived from the population-level to represent a constant risk of infection for individuals in the population.

// An alternative implementation calculates each person's time to infection
// at the beginning of the simulation and scheudles their infection at that time.

#[allow(clippy::cast_precision_loss)]
let next_attempt_time = context.get_current_time()
+ context.sample_distr(TransmissionRng, Exp::new(parameters.foi).unwrap())
/ population_size as f64;

if next_attempt_time <= parameters.max_time {
context.add_plan(next_attempt_time, move |context| {
attempt_infection(context);
});
}
}

pub fn init(context: &mut Context) {
context.add_plan(0.0, |context| {
attempt_infection(context);
});
}

#[cfg(test)]
mod test {
use super::*;
use crate::parameters_loader::ParametersValues;
use ixa::context::Context;

#[test]
fn test_attempt_infection() {
let p_values = ParametersValues {
population: 10,
max_time: 10.0,
seed: 42,
foi: 0.15,
infection_duration: 5.0,
output_dir: ".".to_string(),
output_file: ".".to_string(),
};

let mut context = Context::new();
context.set_global_property_value(Parameters, p_values);
context.init_random(123);
let pid = context.add_person();
attempt_infection(&mut context);
let person_status = context.get_person_property(pid, InfectionStatusType);
assert_eq!(person_status, InfectionStatus::I);
context.execute();
}
}
Loading

0 comments on commit 79c1610

Please sign in to comment.