Skip to content
Open
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
15 changes: 11 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [
"ixa-*",
"examples/basic-infection",
"examples/births-deaths",
"examples/network-random",
"examples/profiling",
"integration-tests/ixa-*",
]
Expand Down Expand Up @@ -32,7 +33,10 @@ ixa-derive = { version = "2.0.0", path = "ixa-derive" }
seq-macro = "^0.3.5"
paste = "^1.0.15"
# We avoid the `priority` feature of `ctor`. See https://github.com/mmastrac/linktime/issues/454.
ctor = { version = "^1.0.1", default-features = false, features = ["std", "proc_macro"] }
ctor = { version = "^1.0.1", default-features = false, features = [
"std",
"proc_macro",
] }
clap = { version = "^4.5.26", features = ["derive"] }
clap-markdown = "0.1.5"
log = "^0.4.22"
Expand All @@ -48,8 +52,6 @@ assert_approx_eq = "^1.1.0"
strum = { version = "^0.28.0", features = ["derive"] }
quote = "^1.0.38"
syn = { version = "^2.0.95" }
proc-macro2 = "1.0.93"
proc-macro-crate = "3.3.0"
delegate = "^0.13.3"
web-sys = "^0.3.77"
fern = "^0.7.1"
Expand Down Expand Up @@ -123,7 +125,11 @@ log4rs = { workspace = true, optional = true }
fern = { workspace = true, optional = true }
# Required here only to enable the js backend:
wasm-bindgen = { workspace = true, optional = true }
web-sys = { workspace = true, optional = true, features = ["console", "Performance", "Window"] }
web-sys = { workspace = true, optional = true, features = [
"console",
"Performance",
"Window",
] }

[dev-dependencies]
anyhow.workspace = true
Expand All @@ -136,6 +142,7 @@ tempfile.workspace = true
# Example Libraries
ixa_example_basic_infection = { path = "examples/basic-infection" }
ixa_example_births_deaths = { path = "examples/births-deaths" }
ixa_example_network_random = { path = "examples/network-random" }

[workspace.lints.rust]
mismatched_lifetime_syntaxes = "allow"
Expand Down
26 changes: 26 additions & 0 deletions examples/network-random/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "ixa_example_network_random"
version = "0.1.0"
repository.workspace = true
license.workspace = true
edition.workspace = true
homepage.workspace = true
authors.workspace = true

publish = false

[dependencies]
ixa = { path = "../../" }

serde.workspace = true
rand_distr.workspace = true
itertools = "0.15.0"
rand.workspace = true
rust-igraph = "0.7.0"

[lints]
workspace = true

[[bin]]
name = "network_random"
path = "main.rs"
3 changes: 3 additions & 0 deletions examples/network-random/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Example: Random connection network

Minimal example of a random network model. At the start of the simulation, in `network.rs`, the random graph is built with [`rust-igraph`](https://totoro-jam.github.io/rust-igraph/) and then translted into instantiated ixa entities, `Person` and `Edge`. In `infection.rs`, upon infection, that infectee's connections are scheduled for the next generation of onward infection. For simplicity, there is a single generation interval used for all infector-infectee pairs, essentially producing discrete generations.
9 changes: 9 additions & 0 deletions examples/network-random/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"ixa_example_network_random.Parameters": {
"generation_interval": 1.0,
"population_size": 100,
"connection_p": 0.1,
"network_seed": 100,
"n_initial_infected": 1
}
}
10 changes: 10 additions & 0 deletions examples/network-random/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use ixa::runner::run_with_args;
use ixa_example_network_random::init;

fn main() {
run_with_args(|context, _, _| {
init(context);
Ok(())
})
.unwrap();
}
97 changes: 97 additions & 0 deletions examples/network-random/src/infection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
use ixa::impl_property;
use ixa::log::info;
use ixa::prelude::*;
use serde::{Deserialize, Serialize};

use crate::parameters::Parameters;
use crate::{network, Person, PersonId};

#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, Serialize, Deserialize)]
pub enum DiseaseStatus {
S,
I,
}

impl_property!(DiseaseStatus, Person, default_const = DiseaseStatus::S);

define_rng!(InfectionRng);

fn infect(context: &mut Context, infector: Option<PersonId>, infectee: PersonId) {
let generation_interval = context
.get_global_property_value(Parameters)
.unwrap()
.generation_interval;

if DiseaseStatus::S == context.get_property(infectee) {
info!("{infector:?} infected {infectee:?}");

context.set_property(infectee, DiseaseStatus::I);

// schedule onward infections: this infectee becomes the next infector
let next_infector = infectee;
for next_infectee in network::get_connections(context, infectee) {
// only schedule infections if the potential infectee is currently susceptible
if DiseaseStatus::S == context.get_property(next_infectee) {
schedule_relative!(
context,
generation_interval,
infect,
Some(next_infector),
next_infectee
);
}
}
} else {
info!("{infector:?} could not infect {infectee:?}, who was already infected");
}
}

pub fn init(context: &mut Context, n_initial_infections: usize) {
for infectee in context.sample_entities(InfectionRng, with!(Person), n_initial_infections) {
infect(context, None, infectee);
}
}

#[cfg(test)]
mod tests {
use ixa::context::Context;

use super::*;
use crate::network;
use crate::parameters::ParametersValues;

#[test]
fn test_disease_status() {
let mut context = Context::new();
context.init_random(42);

// note that size * p == 1 is the critical value
let parameters = ParametersValues {
generation_interval: 1.0,
population_size: 100,
connection_p: 0.02,
network_seed: 128381,
n_initial_infected: 1,
};
context
.set_global_property_value(Parameters, parameters.clone())
.unwrap();

network::init(
&mut context,
parameters.population_size,
parameters.connection_p,
parameters.network_seed,
);
init(&mut context, parameters.n_initial_infected);

context.execute();

let n_i = context.query_entity_count(with!(Person, DiseaseStatus::I));
let n_s = context.query_entity_count(with!(Person, DiseaseStatus::S));

assert_eq!(n_i + n_s, parameters.population_size);
// in this particular example, 77 people are infected
assert_eq!(n_i, 77);
}
}
22 changes: 22 additions & 0 deletions examples/network-random/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use ixa::prelude::*;

pub mod infection;
pub mod network;
pub mod parameters;

define_entity!(Person);

pub fn init(context: &mut Context) {
// Load parameters from json
let parameters = parameters::init(context);

// Load network
network::init(
context,
parameters.population_size,
parameters.connection_p,
parameters.network_seed,
);

infection::init(context, parameters.n_initial_infected);
}
78 changes: 78 additions & 0 deletions examples/network-random/src/network.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use std::hash::Hash;

use ixa::prelude::*;
use rust_igraph::Graph;

use crate::{Person, PersonId};

define_entity!(Edge);
define_property!(struct Node1(PersonId), Edge);
define_property!(struct Node2(PersonId), Edge);

pub fn get_connections(context: &Context, person_id: PersonId) -> Vec<PersonId> {
context
.query(with!(Edge, Node1(person_id)))
.into_iter()
.map(|edge| context.get_property::<Edge, Node2>(edge).0)
.collect()
}

pub fn instantiate_person_network(context: &mut Context, g: Graph) {
let n_people = g.vcount();

let person_ids: Vec<PersonId> = (0..n_people)
.map(|_| context.add_entity(with!(Person)).unwrap())
.collect();

for (from, to) in g.edges() {
let p1 = person_ids[from as usize];
let p2 = person_ids[to as usize];
context
.add_entity(with!(Edge, Node1(p1), Node2(p2)))
.unwrap();

// if the graph is undirected, add the other (directed) edge
if !g.is_directed() {
context
.add_entity(with!(Edge, Node1(p2), Node2(p1)))
.unwrap();
}
}
}

pub fn init(context: &mut Context, population_size: usize, connection_p: f64, seed: u64) {
// ideally, we could use some ixa-provided rng for this seed
let g = Graph::erdos_renyi(population_size as u32, connection_p, seed).unwrap();
instantiate_person_network(context, g);
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_instantiate_network() {
// set up a manual network, with a certain number of edges
let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (1, 2)], false, Some(6)).unwrap();

// turn that network in people entities
let mut context = Context::new();
instantiate_person_network(&mut context, g);

// count how many connections each person has
let mut n_connections: Vec<usize> = context
.query(with!(Person))
.into_iter()
.map(|person_id| get_connections(&context, person_id).len())
.collect();

n_connections.sort();

// we expect:
// - person 0 has 3 connections
// - persons 1 & 2 have 2
// - person 3 has 1
// - person 4 & 6 have 0
assert_eq!(n_connections, vec![0, 0, 1, 2, 2, 3]);
}
}
27 changes: 27 additions & 0 deletions examples/network-random/src/parameters.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use std::fmt::Debug;
use std::path::PathBuf;

use ixa::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParametersValues {
pub generation_interval: f64,
pub population_size: usize,
pub connection_p: f64,
pub network_seed: u64,
pub n_initial_infected: usize,
}
define_global_property!(Parameters, ParametersValues);

pub fn init(context: &mut Context) -> ParametersValues {
let file_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("config.json");
context
.load_global_properties(&file_path)
.expect(format!("could not load parameters from {:?}", file_path).as_str());

context
.get_global_property_value(Parameters)
.unwrap()
.clone()
}
Loading