From 64a66984a565f6bff18ef9fcfc8d80e80a7f6de9 Mon Sep 17 00:00:00 2001 From: Scott Olesen Date: Wed, 15 Apr 2026 19:09:17 -0400 Subject: [PATCH 01/11] Put contacts into network file --- examples/network-hhmodel/main.rs | 2 +- examples/network-hhmodel/network.rs | 57 ++++++++++++++++++++ examples/network-hhmodel/seir.rs | 82 +++++++++-------------------- 3 files changed, 84 insertions(+), 57 deletions(-) diff --git a/examples/network-hhmodel/main.rs b/examples/network-hhmodel/main.rs index fee887dc..e6d14b76 100644 --- a/examples/network-hhmodel/main.rs +++ b/examples/network-hhmodel/main.rs @@ -43,6 +43,6 @@ fn initialize(context: &mut Context) { let to_infect: Vec = vec![context.sample_entity(MainRng, Person).unwrap()]; #[allow(clippy::vec_init_then_push)] - seir::init(context, &to_infect); + seir::init(context, &to_infect, 1.0); context.execute(); } diff --git a/examples/network-hhmodel/network.rs b/examples/network-hhmodel/network.rs index b5e1cac8..b2227a4e 100644 --- a/examples/network-hhmodel/network.rs +++ b/examples/network-hhmodel/network.rs @@ -1,15 +1,19 @@ use ixa::network::edge::EdgeType; use ixa::prelude::*; use ixa::{HashSet, HashSetExt}; +use rand_distr::Bernoulli; use serde::Deserialize; use crate::loader::{open_csv, HouseholdId, Id}; +use crate::parameters::Parameters; use crate::{Person, PersonId}; define_edge_type!(struct Household, Person); define_edge_type!(struct AgeUnder5, Person); define_edge_type!(struct Age5to17, Person); +define_rng!(NetworkRng); + #[derive(Deserialize, Debug)] struct EdgeRecord { v1: u16, @@ -58,6 +62,59 @@ fn load_edge_list>(context: &mut Context, file_name: &str, } } +fn sar_to_beta(sar: f64, infectious_period: f64) -> f64 { + 1.0 - (1.0 - sar).powf(1.0 / infectious_period) +} + +/// Get all the contacts a person will have over a certain duration +pub fn get_contacts(context: &Context, person_id: PersonId, duration: f64) -> HashSet { + let parameters = context + .get_global_property_value(Parameters) + .unwrap() + .clone(); + + // Probability of contact during the duration. Note that this assumes that the duration is not too high! + let p_hh = duration * sar_to_beta(parameters.sar, parameters.incubation_period); + let p_other = duration + * sar_to_beta( + parameters.sar / parameters.between_hh_transmission_reduction, + parameters.incubation_period, + ); + + let mut infectees = HashSet::new(); + infectees.extend( + get_contacts_by_edge_type::(context, person_id) + .iter() + .filter(|_| context.sample_distr(NetworkRng, Bernoulli::new(p_hh).unwrap())), + ); + + infectees.extend( + get_contacts_by_edge_type::(context, person_id) + .iter() + .filter(|_| context.sample_distr(NetworkRng, Bernoulli::new(p_other).unwrap())), + ); + + infectees.extend( + get_contacts_by_edge_type::(context, person_id) + .iter() + .filter(|_| context.sample_distr(NetworkRng, Bernoulli::new(p_other).unwrap())), + ); + + infectees +} + +/// Get the contacts by edge type +fn get_contacts_by_edge_type + 'static>( + context: &Context, + person_id: PersonId, +) -> HashSet { + context + .get_edges::(person_id) + .iter() + .map(|e| e.neighbor) + .collect() +} + pub fn init(context: &mut Context, people: &[PersonId]) { // Create dense household networks create_household_networks(context, people); diff --git a/examples/network-hhmodel/seir.rs b/examples/network-hhmodel/seir.rs index cc6be56f..249fa414 100644 --- a/examples/network-hhmodel/seir.rs +++ b/examples/network-hhmodel/seir.rs @@ -1,11 +1,11 @@ use ixa::log::info; -use ixa::network::edge::EdgeType; use ixa::prelude::*; use ixa::{impl_property, ExecutionPhase}; -use rand_distr::{Bernoulli, Gamma}; +use rand_distr::Gamma; use serde::{Deserialize, Serialize}; +use std::collections::HashSet; -use crate::network::{Age5to17, AgeUnder5, Household}; +use crate::network::get_contacts; use crate::parameters::Parameters; use crate::{Person, PersonId}; @@ -27,36 +27,18 @@ define_property!( default_const = InfectedBy(None) ); -fn sar_to_beta(sar: f64, infectious_period: f64) -> f64 { - 1.0 - (1.0 - sar).powf(1.0 / infectious_period) -} - fn calculate_waiting_time(context: &Context, shape: f64, mean_period: f64) -> f64 { let d = Gamma::new(shape, mean_period / shape).unwrap(); context.sample_distr(SeirRng, d) } -fn expose_network>(context: &mut Context, beta: f64) { - let infectious_people = context.query((DiseaseStatus::I,)).to_owned_vec(); - - for infectious in infectious_people { - let edges = context.get_matching_edges::(infectious, |context, edge| { - context.match_entity(edge.neighbor, (DiseaseStatus::S,)) - }); - - for e in edges { - if context.sample_distr(SeirRng, Bernoulli::new(beta).unwrap()) { - context.set_property(e.neighbor, DiseaseStatus::E); - info!( - "Person {} exposed person {} at time {}.", - infectious, - e.neighbor, - context.get_current_time() - ); - context.set_property(e.neighbor, InfectedBy(Some(infectious))); - } - } - } +fn expose(context: &mut Context, infector: PersonId, infectee: PersonId) { + info!( + "{infector:?} exposed {infectee:?} at time {}.", + context.get_current_time() + ); + context.set_property(infectee, DiseaseStatus::E); + context.set_property(infectee, with!(InfectedBy, Some(infector))); } fn schedule_waiting_event( @@ -104,34 +86,22 @@ fn schedule_recovery(context: &mut Context, person_id: PersonId) { ); } -pub fn init(context: &mut Context, initial_infections: &Vec) { +pub fn init(context: &mut Context, initial_infections: &Vec, period: f64) { context.add_periodic_plan_with_phase( - 1.0, - |context| { - let parameters = context - .get_global_property_value(Parameters) - .unwrap() - .clone(); - - // infect the networks - expose_network::( - context, - sar_to_beta(parameters.sar, parameters.incubation_period), - ); - expose_network::( - context, - sar_to_beta( - parameters.sar / parameters.between_hh_transmission_reduction, - parameters.incubation_period, - ), - ); - expose_network::( - context, - sar_to_beta( - parameters.sar / parameters.between_hh_transmission_reduction, - parameters.incubation_period, - ), - ); + period, + move |context| { + // get all infector-infectee pairs + let mut pairs = HashSet::new(); + for infector in context.query(with!(Person, DiseaseStatus::I)) { + for infectee in get_contacts(context, infector, period) { + pairs.insert((infector, infectee)); + } + } + + // do the exposures + for (infector, infectee) in pairs { + expose(context, infector, infectee) + } }, ExecutionPhase::Normal, ); @@ -191,7 +161,7 @@ mod tests { to_infect.extend(people); }); - init(&mut context, &to_infect); + init(&mut context, &to_infect, 1.0); context.execute(); From 0c54cd697a350b4e05a82e6269daab6a89271562 Mon Sep 17 00:00:00 2001 From: Scott Olesen Date: Thu, 16 Apr 2026 14:21:02 -0400 Subject: [PATCH 02/11] fixup --- examples/network-hhmodel/seir.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/network-hhmodel/seir.rs b/examples/network-hhmodel/seir.rs index 249fa414..d315c322 100644 --- a/examples/network-hhmodel/seir.rs +++ b/examples/network-hhmodel/seir.rs @@ -38,7 +38,7 @@ fn expose(context: &mut Context, infector: PersonId, infectee: PersonId) { context.get_current_time() ); context.set_property(infectee, DiseaseStatus::E); - context.set_property(infectee, with!(InfectedBy, Some(infector))); + context.set_property(infectee, InfectedBy(Some(infector))); } fn schedule_waiting_event( From 291e4fcd971cbeb9ea3fa757363b95656e88eedd Mon Sep 17 00:00:00 2001 From: Scott Olesen Date: Thu, 16 Apr 2026 14:34:24 -0400 Subject: [PATCH 03/11] rustfmt --- examples/network-hhmodel/seir.rs | 3 ++- src/lib.rs | 6 +----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/examples/network-hhmodel/seir.rs b/examples/network-hhmodel/seir.rs index d315c322..8c1db42a 100644 --- a/examples/network-hhmodel/seir.rs +++ b/examples/network-hhmodel/seir.rs @@ -1,9 +1,10 @@ +use std::collections::HashSet; + use ixa::log::info; use ixa::prelude::*; use ixa::{impl_property, ExecutionPhase}; use rand_distr::Gamma; use serde::{Deserialize, Serialize}; -use std::collections::HashSet; use crate::network::get_contacts; use crate::parameters::Parameters; diff --git a/src/lib.rs b/src/lib.rs index b99154dc..1c673b1e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -78,15 +78,11 @@ pub mod hashing; pub mod numeric; // Re-export for macros -pub use bincode; -pub use csv; -pub use ctor; pub use ixa_derive::{ impl_make_canonical, impl_people_make_canonical, reorder_closure, sorted_tag, sorted_value_type, unreorder_closure, }; -pub use paste; -pub use rand; +pub use {bincode, csv, ctor, paste, rand}; // Deterministic hashing data structures pub use crate::hashing::{HashMap, HashMapExt, HashSet, HashSetExt}; From d0a65bca54fa0e23cd38702b9d9e882eb4cd3e3e Mon Sep 17 00:00:00 2001 From: Scott Olesen Date: Thu, 16 Apr 2026 14:39:20 -0400 Subject: [PATCH 04/11] fixup: hashst --- examples/network-hhmodel/seir.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/network-hhmodel/seir.rs b/examples/network-hhmodel/seir.rs index 8c1db42a..3d6de463 100644 --- a/examples/network-hhmodel/seir.rs +++ b/examples/network-hhmodel/seir.rs @@ -1,8 +1,6 @@ -use std::collections::HashSet; - use ixa::log::info; use ixa::prelude::*; -use ixa::{impl_property, ExecutionPhase}; +use ixa::{impl_property, ExecutionPhase, HashSet, HashSetExt}; use rand_distr::Gamma; use serde::{Deserialize, Serialize}; From 5f1fb07d5a8ee113fe5969f29312fa2992fe05f2 Mon Sep 17 00:00:00 2001 From: Scott Olesen Date: Thu, 16 Apr 2026 15:18:14 -0400 Subject: [PATCH 05/11] fixup: linting --- src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 1c673b1e..b99154dc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -78,11 +78,15 @@ pub mod hashing; pub mod numeric; // Re-export for macros +pub use bincode; +pub use csv; +pub use ctor; pub use ixa_derive::{ impl_make_canonical, impl_people_make_canonical, reorder_closure, sorted_tag, sorted_value_type, unreorder_closure, }; -pub use {bincode, csv, ctor, paste, rand}; +pub use paste; +pub use rand; // Deterministic hashing data structures pub use crate::hashing::{HashMap, HashMapExt, HashSet, HashSetExt}; From 99e6f4d5c5994b22fc7c4e147ced798a20562d8d Mon Sep 17 00:00:00 2001 From: Scott Olesen Date: Mon, 20 Apr 2026 12:02:13 -0400 Subject: [PATCH 06/11] bug: expose only the susceptible --- examples/network-hhmodel/seir.rs | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/examples/network-hhmodel/seir.rs b/examples/network-hhmodel/seir.rs index 3d6de463..b9594bc4 100644 --- a/examples/network-hhmodel/seir.rs +++ b/examples/network-hhmodel/seir.rs @@ -32,12 +32,15 @@ fn calculate_waiting_time(context: &Context, shape: f64, mean_period: f64) -> f6 } fn expose(context: &mut Context, infector: PersonId, infectee: PersonId) { - info!( - "{infector:?} exposed {infectee:?} at time {}.", - context.get_current_time() - ); - context.set_property(infectee, DiseaseStatus::E); - context.set_property(infectee, InfectedBy(Some(infector))); + let infectee_status: DiseaseStatus = context.get_property(infectee); + if infectee_status == DiseaseStatus::S { + info!( + "{infector:?} exposed {infectee:?} at time {}.", + context.get_current_time() + ); + context.set_property(infectee, DiseaseStatus::E); + context.set_property(infectee, InfectedBy(Some(infector))); + } } fn schedule_waiting_event( @@ -47,15 +50,15 @@ fn schedule_waiting_event( mean_period: f64, new_status: DiseaseStatus, ) { - let ct = context.get_current_time(); - let waiting_time = calculate_waiting_time(context, shape, mean_period); + let t = context.get_current_time() + calculate_waiting_time(context, shape, mean_period); - context.add_plan(ct + waiting_time, move |context| { + context.add_plan(t, move |context| { + trace!("{person_id:?} changed to disease state {new_status:?} at t={t:?}"); context.set_property(person_id, new_status); }); } -fn schedule_infection(context: &mut Context, person_id: PersonId) { +fn schedule_infectiousness(context: &mut Context, person_id: PersonId) { let parameters = context .get_global_property_value(Parameters) .unwrap() @@ -107,7 +110,7 @@ pub fn init(context: &mut Context, initial_infections: &Vec, period: f context.subscribe_to_event( move |context, event: PropertyChangeEvent| match event.current { - DiseaseStatus::E => schedule_infection(context, event.entity_id), + DiseaseStatus::E => schedule_infectiousness(context, event.entity_id), DiseaseStatus::I => schedule_recovery(context, event.entity_id), _ => (), }, From 92b2ca5b581d0034436abc39ee178168616c9891 Mon Sep 17 00:00:00 2001 From: Scott Olesen Date: Mon, 20 Apr 2026 12:28:59 -0400 Subject: [PATCH 07/11] fixup --- examples/network-hhmodel/incidence_report.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/network-hhmodel/incidence_report.rs b/examples/network-hhmodel/incidence_report.rs index 874d9232..f0a806d7 100644 --- a/examples/network-hhmodel/incidence_report.rs +++ b/examples/network-hhmodel/incidence_report.rs @@ -158,7 +158,7 @@ mod test { let to_infect: Vec = vec![context.sample_entity(MainRng, Person).unwrap()]; #[allow(clippy::vec_init_then_push)] - seir::init(&mut context, &to_infect); + seir::init(&mut context, &to_infect, 1.0); context.execute(); } From a87226e14f1b6614b64c5d6f2bbcb8fc55ecc175 Mon Sep 17 00:00:00 2001 From: Scott Olesen Date: Tue, 28 Apr 2026 12:55:53 -0400 Subject: [PATCH 08/11] Rework example --- examples/network-hhmodel/network.rs | 155 ++++++++++++---------------- 1 file changed, 68 insertions(+), 87 deletions(-) diff --git a/examples/network-hhmodel/network.rs b/examples/network-hhmodel/network.rs index b2227a4e..50d2129c 100644 --- a/examples/network-hhmodel/network.rs +++ b/examples/network-hhmodel/network.rs @@ -1,26 +1,29 @@ -use ixa::network::edge::EdgeType; use ixa::prelude::*; use ixa::{HashSet, HashSetExt}; use rand_distr::Bernoulli; -use serde::Deserialize; use crate::loader::{open_csv, HouseholdId, Id}; use crate::parameters::Parameters; -use crate::{Person, PersonId}; +use crate::PersonId; -define_edge_type!(struct Household, Person); -define_edge_type!(struct AgeUnder5, Person); -define_edge_type!(struct Age5to17, Person); +define_entity!(Edge); +// relative transmission rate +define_property!(struct RR(f64), Edge); +define_property!(struct Node1(PersonId), Edge); +define_property!(struct Node2(PersonId), Edge); define_rng!(NetworkRng); -#[derive(Deserialize, Debug)] -struct EdgeRecord { - v1: u16, - v2: u16, +fn add_bidi_edge(context: &mut Context, p1: PersonId, p2: PersonId, rr: RR) { + context + .add_entity(with!(Edge, Node1(p1), Node2(p2), rr)) + .unwrap(); + context + .add_entity(with!(Edge, Node2(p1), Node1(p2), rr)) + .unwrap(); } -fn create_household_networks(context: &mut Context, people: &[PersonId]) { +fn create_household_networks(context: &mut Context, people: &[PersonId], rr: RR) { let mut households = HashSet::new(); for person_id in people { let household_id: HouseholdId = context.get_property(*person_id); @@ -32,33 +35,31 @@ fn create_household_networks(context: &mut Context, people: &[PersonId]) { // create a dense network while let Some(person) = members.pop() { for other_person in &members { - context - .add_edge_bidi(person, *other_person, 1.0, Household) - .unwrap(); + add_bidi_edge(context, person, *other_person, rr); } } } } } -fn load_edge_list>(context: &mut Context, file_name: &str, inner: ET) { +fn load_edge_list(context: &mut Context, file_name: &str, rr: RR) { let mut reader = open_csv(file_name); for result in reader.deserialize() { - let record: EdgeRecord = result.expect("Failed to parse edge"); + let record: (u16, u16) = result.expect("Failed to parse edge"); let mut p1_vec = Vec::new(); - context.with_query_results((Id(record.v1),), &mut |people| { + context.with_query_results((Id(record.0),), &mut |people| { p1_vec = people.to_owned_vec() }); assert_eq!(p1_vec.len(), 1); let p1 = p1_vec[0]; let mut p2_vec = Vec::new(); - context.with_query_results((Id(record.v2),), &mut |people| { + context.with_query_results((Id(record.1),), &mut |people| { p2_vec = people.to_owned_vec() }); assert_eq!(p2_vec.len(), 1); let p2 = p2_vec[0]; - context.add_edge_bidi(p1, p2, 1.0, inner.clone()).unwrap(); + add_bidi_edge(context, p1, p2, rr); } } @@ -66,103 +67,83 @@ fn sar_to_beta(sar: f64, infectious_period: f64) -> f64 { 1.0 - (1.0 - sar).powf(1.0 / infectious_period) } -/// Get all the contacts a person will have over a certain duration +/// Get all the effective contacts a person will have over a certain duration pub fn get_contacts(context: &Context, person_id: PersonId, duration: f64) -> HashSet { let parameters = context .get_global_property_value(Parameters) .unwrap() .clone(); - // Probability of contact during the duration. Note that this assumes that the duration is not too high! - let p_hh = duration * sar_to_beta(parameters.sar, parameters.incubation_period); - let p_other = duration - * sar_to_beta( - parameters.sar / parameters.between_hh_transmission_reduction, - parameters.incubation_period, - ); - - let mut infectees = HashSet::new(); - infectees.extend( - get_contacts_by_edge_type::(context, person_id) - .iter() - .filter(|_| context.sample_distr(NetworkRng, Bernoulli::new(p_hh).unwrap())), - ); - - infectees.extend( - get_contacts_by_edge_type::(context, person_id) - .iter() - .filter(|_| context.sample_distr(NetworkRng, Bernoulli::new(p_other).unwrap())), - ); - - infectees.extend( - get_contacts_by_edge_type::(context, person_id) - .iter() - .filter(|_| context.sample_distr(NetworkRng, Bernoulli::new(p_other).unwrap())), - ); - - infectees -} + // Base probability of contact during the duration. Note that this assumes that the duration is not too high! + let base_p = duration * sar_to_beta(parameters.sar, parameters.incubation_period); -/// Get the contacts by edge type -fn get_contacts_by_edge_type + 'static>( - context: &Context, - person_id: PersonId, -) -> HashSet { context - .get_edges::(person_id) - .iter() - .map(|e| e.neighbor) + // get all people this person could contact + .query_result_iterator(with!(Edge, Node1(person_id))) + .filter_map(|e| { + // extract risk ratio of transmission and contactee from edge + let rr: RR = context.get_property(e); + let node2: Node2 = context.get_property(e); + + // check if they actually make contact + match context.sample_distr(NetworkRng, Bernoulli::new(base_p * rr.0).unwrap()) { + false => None, + true => Some(node2.0), + } + }) .collect() } pub fn init(context: &mut Context, people: &[PersonId]) { - // Create dense household networks - create_household_networks(context, people); + let parameters = context + .get_global_property_value(Parameters) + .unwrap() + .clone(); - // Add U5 edges from csv - load_edge_list(context, "AgeUnder5Edges.csv", AgeUnder5); + // relative risk of transmission between (vs. within) households + let rr = 1.0 / parameters.between_hh_transmission_reduction; - // Add U18 edges from csv - load_edge_list(context, "Age5to17Edges.csv", Age5to17); + // Create dense household networks + create_household_networks(context, people, RR(1.0)); + // Add other edges from csv's with lower transmission rate + load_edge_list(context, "AgeUnder5Edges.csv", RR(rr)); + load_edge_list(context, "Age5to17Edges.csv", RR(rr)); } #[cfg(test)] mod tests { - use super::*; - use crate::{loader, network}; + use crate::{loader, network, Person}; - const N_SIZE_12: usize = 1; - const N_SIZE_11: usize = 1; - const N_SIZE_3: usize = 122; - - #[test] - fn test_expected_12_member_household() { + // Assert that person with `id` has `n` contacts (i.e., edges going from + // them, and also edges going to them) + fn assert_has_n_contacts(id: u16, n: usize) { let mut context = Context::new(); context.init_random(42); let people = loader::init(&mut context); network::init(&mut context, &people); - let deg11 = context.find_entities_by_degree::(11); - assert_eq!(deg11.len(), 12 * N_SIZE_12); + + // `id` is the data ID, the one in the csv's + // `pid` is the integer inside Person(pid) + let person: Person = context.query(with!(Person, Id(id))).into_iter().next(); + let pid: usize = person.id(); + let n_to = context.query_entity_count(with!(Edge, Node1(pid))); + let n_from = context.query_entity_count(with!(Edge, Node2(pid))); + assert_eq!(n_to, n); + assert_eq!(n_from, n); } #[test] - fn test_expected_11_member_household() { - let mut context = Context::new(); - context.init_random(42); - let people = loader::init(&mut context); - network::init(&mut context, &people); - let deg10 = context.find_entities_by_degree::(10); - assert_eq!(deg10.len(), 11 * N_SIZE_11); + fn test_person_826() { + // Person 826 is in a household of 5 with no other contacts. + // There should be 4 edges going from them, and 4 going to them. + assert_has_n_contacts(826, 4); } #[test] - fn test_expected_3_member_household() { - let mut context = Context::new(); - context.init_random(42); - let people = loader::init(&mut context); - network::init(&mut context, &people); - let deg10 = context.find_entities_by_degree::(2); - assert_eq!(deg10.len(), 3 * N_SIZE_3); + fn test_person_243() { + // Person 243 is in a household of size 5, with 4 other contacts, + // for 8 total contacts. + assert_has_n_contacts(243, 8); } } From 8378d1a0bfab2055813913db133d935f069308ac Mon Sep 17 00:00:00 2001 From: "Beau B. Bruce" Date: Tue, 28 Apr 2026 16:04:02 -0400 Subject: [PATCH 09/11] network model suggestions --- examples/network-hhmodel/loader.rs | 14 ++----- examples/network-hhmodel/main.rs | 4 +- examples/network-hhmodel/network.rs | 59 ++++++++++++++--------------- 3 files changed, 34 insertions(+), 43 deletions(-) diff --git a/examples/network-hhmodel/loader.rs b/examples/network-hhmodel/loader.rs index f3b0220c..749802da 100644 --- a/examples/network-hhmodel/loader.rs +++ b/examples/network-hhmodel/loader.rs @@ -5,7 +5,7 @@ use ixa::impl_property; use ixa::prelude::*; use serde::{Deserialize, Serialize}; -use crate::{example_dir, Person, PersonId}; +use crate::{example_dir, Person}; #[derive(Serialize, Deserialize, Copy, Clone, PartialEq, Eq, Debug, Hash)] pub struct Id(pub u16); @@ -39,32 +39,24 @@ struct PeopleRecord { household_id: HouseholdId, } -fn create_person_from_record(context: &mut Context, record: &PeopleRecord) -> PersonId { - context - .add_entity((record.id, record.age_group, record.sex, record.household_id)) - .unwrap() -} - pub fn open_csv(file_name: &str) -> Reader { let current_dir = example_dir(); let file_path = current_dir.join(file_name); csv::Reader::from_path(file_path).unwrap() } -pub fn init(context: &mut Context) -> Vec { +pub fn init(context: &mut Context) { // Load csv and deserialize records let mut reader = open_csv("Households.csv"); - let mut people = Vec::new(); for result in reader.deserialize() { let record: PeopleRecord = result.expect("Failed to parse record"); - people.push(create_person_from_record(context, &record)); + let _ = context.add_entity((record.id, record.age_group, record.sex, record.household_id)); } context.index_property::(); context.index_property::(); - people } #[cfg(test)] diff --git a/examples/network-hhmodel/main.rs b/examples/network-hhmodel/main.rs index e6d14b76..ee730082 100644 --- a/examples/network-hhmodel/main.rs +++ b/examples/network-hhmodel/main.rs @@ -27,14 +27,14 @@ fn initialize(context: &mut Context) { context.init_random(1); // Load people from csv and set up some base properties - let people = loader::init(context); + loader::init(context); // Load parameters from json let file_path = example_dir().join("config.json"); context.load_global_properties(&file_path).unwrap(); // Load network - network::init(context, &people); + network::init(context); // Initialize incidence report incidence_report::init(context).unwrap(); diff --git a/examples/network-hhmodel/network.rs b/examples/network-hhmodel/network.rs index 50d2129c..108bb5b4 100644 --- a/examples/network-hhmodel/network.rs +++ b/examples/network-hhmodel/network.rs @@ -4,17 +4,17 @@ use rand_distr::Bernoulli; use crate::loader::{open_csv, HouseholdId, Id}; use crate::parameters::Parameters; -use crate::PersonId; +use crate::Person; define_entity!(Edge); // relative transmission rate define_property!(struct RR(f64), Edge); -define_property!(struct Node1(PersonId), Edge); -define_property!(struct Node2(PersonId), Edge); +define_property!(struct Node1(EntityId), Edge); +define_property!(struct Node2(EntityId), Edge); define_rng!(NetworkRng); -fn add_bidi_edge(context: &mut Context, p1: PersonId, p2: PersonId, rr: RR) { +fn add_bidi_edge(context: &mut Context, p1: EntityId, p2: EntityId, rr: RR) { context .add_entity(with!(Edge, Node1(p1), Node2(p2), rr)) .unwrap(); @@ -23,12 +23,13 @@ fn add_bidi_edge(context: &mut Context, p1: PersonId, p2: PersonId, rr: RR) { .unwrap(); } -fn create_household_networks(context: &mut Context, people: &[PersonId], rr: RR) { +fn create_household_networks(context: &mut Context, rr: RR) { let mut households = HashSet::new(); - for person_id in people { - let household_id: HouseholdId = context.get_property(*person_id); + let people: Vec> = context.get_entity_iterator().collect(); + for person in people { + let household_id: HouseholdId = context.get_property(person); if households.insert(household_id) { - let mut members: Vec = Vec::new(); + let mut members = Vec::new(); context.with_query_results((household_id,), &mut |results| { members = results.to_owned_vec() }); @@ -68,7 +69,7 @@ fn sar_to_beta(sar: f64, infectious_period: f64) -> f64 { } /// Get all the effective contacts a person will have over a certain duration -pub fn get_contacts(context: &Context, person_id: PersonId, duration: f64) -> HashSet { +pub fn get_contacts(context: &Context, person: EntityId, duration: f64) -> Vec> { let parameters = context .get_global_property_value(Parameters) .unwrap() @@ -77,24 +78,23 @@ pub fn get_contacts(context: &Context, person_id: PersonId, duration: f64) -> Ha // Base probability of contact during the duration. Note that this assumes that the duration is not too high! let base_p = duration * sar_to_beta(parameters.sar, parameters.incubation_period); - context - // get all people this person could contact - .query_result_iterator(with!(Edge, Node1(person_id))) - .filter_map(|e| { - // extract risk ratio of transmission and contactee from edge - let rr: RR = context.get_property(e); - let node2: Node2 = context.get_property(e); - - // check if they actually make contact - match context.sample_distr(NetworkRng, Bernoulli::new(base_p * rr.0).unwrap()) { - false => None, - true => Some(node2.0), + let mut contacts: Vec> = Vec::new(); + + for edge in context.query(with!(Edge, Node1(person))) { + let RR(rr): RR = context.get_property(edge); + let Node2(person2): Node2 = context.get_property(edge); + + if context.sample_distr(NetworkRng, Bernoulli::new(base_p * rr).unwrap()) { + if !contacts.contains(&person2) { + contacts.push(person2); } - }) - .collect() + } + } + + contacts } -pub fn init(context: &mut Context, people: &[PersonId]) { +pub fn init(context: &mut Context) { let parameters = context .get_global_property_value(Parameters) .unwrap() @@ -104,7 +104,7 @@ pub fn init(context: &mut Context, people: &[PersonId]) { let rr = 1.0 / parameters.between_hh_transmission_reduction; // Create dense household networks - create_household_networks(context, people, RR(1.0)); + create_household_networks(context, RR(1.0)); // Add other edges from csv's with lower transmission rate load_edge_list(context, "AgeUnder5Edges.csv", RR(rr)); load_edge_list(context, "Age5to17Edges.csv", RR(rr)); @@ -121,14 +121,13 @@ mod tests { let mut context = Context::new(); context.init_random(42); let people = loader::init(&mut context); - network::init(&mut context, &people); + network::init(&mut context); // `id` is the data ID, the one in the csv's // `pid` is the integer inside Person(pid) - let person: Person = context.query(with!(Person, Id(id))).into_iter().next(); - let pid: usize = person.id(); - let n_to = context.query_entity_count(with!(Edge, Node1(pid))); - let n_from = context.query_entity_count(with!(Edge, Node2(pid))); + let person = context.query(with!(Person, Id(id))).into_iter().next().unwrap(); + let n_to = context.query_entity_count(with!(Edge, Node1(person))); + let n_from = context.query_entity_count(with!(Edge, Node2(person))); assert_eq!(n_to, n); assert_eq!(n_from, n); } From 2f954f6fd899c77860899f0a6aed979272183e17 Mon Sep 17 00:00:00 2001 From: Beau Bruce Date: Tue, 28 Apr 2026 20:52:54 -0400 Subject: [PATCH 10/11] fixup: fix tests --- examples/network-hhmodel/incidence_report.rs | 2 +- examples/network-hhmodel/loader.rs | 33 ++++------ examples/network-hhmodel/network.rs | 63 ++++++++++---------- examples/network-hhmodel/seir.rs | 4 +- 4 files changed, 48 insertions(+), 54 deletions(-) diff --git a/examples/network-hhmodel/incidence_report.rs b/examples/network-hhmodel/incidence_report.rs index f0a806d7..7aad3ae9 100644 --- a/examples/network-hhmodel/incidence_report.rs +++ b/examples/network-hhmodel/incidence_report.rs @@ -146,7 +146,7 @@ mod test { .unwrap(); let people = loader::init(&mut context); - network::init(&mut context, &people); + network::init(&mut context); incidence_report::init(&mut context).unwrap(); context.subscribe_to_event( diff --git a/examples/network-hhmodel/loader.rs b/examples/network-hhmodel/loader.rs index 749802da..7e980b76 100644 --- a/examples/network-hhmodel/loader.rs +++ b/examples/network-hhmodel/loader.rs @@ -62,7 +62,7 @@ pub fn init(context: &mut Context) { #[cfg(test)] mod tests { use ixa::context::Context; - use ixa::random::ContextRandomExt; + use ixa::random::{ContextRandomExt}; use super::*; @@ -79,26 +79,17 @@ mod tests { #[test] fn test_some_people_load_correctly() { let mut context = Context::new(); - context.init_random(42); + init(&mut context); + + // only one with the given id + assert_eq!(context.query_entity_count(with!(Person, Id(676))), 1); + assert_eq!(context.query_entity_count(with!(Person, Id(213))), 1); + assert_eq!(context.query_entity_count(with!(Person, Id(1591))), 1); + + // test exist fully specified + assert_eq!(context.query_entity_count((Id(676), AgeGroup::Age18to64, Sex::Female, HouseholdId(1))), 1); + assert_eq!(context.query_entity_count((Id(213), AgeGroup::AgeUnder5, Sex::Female, HouseholdId(162))), 1); + assert_eq!(context.query_entity_count((Id(1591), AgeGroup::Age65Plus, Sex::Male, HouseholdId(496))), 1); - let people = init(&mut context); - - let person = people[0]; - assert!(context.match_entity( - person, - (Id(676), AgeGroup::Age18to64, Sex::Female, HouseholdId(1)) - )); - - let person = people[246]; - assert!(context.match_entity( - person, - (Id(213), AgeGroup::AgeUnder5, Sex::Female, HouseholdId(162)) - )); - - let person = people[1591]; - assert!(context.match_entity( - person, - (Id(1591), AgeGroup::Age65Plus, Sex::Male, HouseholdId(496)) - )); } } diff --git a/examples/network-hhmodel/network.rs b/examples/network-hhmodel/network.rs index 108bb5b4..f83239cf 100644 --- a/examples/network-hhmodel/network.rs +++ b/examples/network-hhmodel/network.rs @@ -16,10 +16,10 @@ define_rng!(NetworkRng); fn add_bidi_edge(context: &mut Context, p1: EntityId, p2: EntityId, rr: RR) { context - .add_entity(with!(Edge, Node1(p1), Node2(p2), rr)) + .add_entity((Node1(p1), Node2(p2), rr)) .unwrap(); context - .add_entity(with!(Edge, Node2(p1), Node1(p2), rr)) + .add_entity((Node1(p2), Node2(p1), rr)) .unwrap(); } @@ -29,14 +29,13 @@ fn create_household_networks(context: &mut Context, rr: RR) { for person in people { let household_id: HouseholdId = context.get_property(person); if households.insert(household_id) { - let mut members = Vec::new(); - context.with_query_results((household_id,), &mut |results| { - members = results.to_owned_vec() - }); + let members: Vec> = context.query((household_id,)).into_iter().collect(); + // create a dense network - while let Some(person) = members.pop() { - for other_person in &members { - add_bidi_edge(context, person, *other_person, rr); + for i in 0..(members.len() - 1) { + for j in (i + 1)..members.len() { + assert!(i != j); + add_bidi_edge(context, members[i], members[j], rr); } } } @@ -48,19 +47,11 @@ fn load_edge_list(context: &mut Context, file_name: &str, rr: RR) { for result in reader.deserialize() { let record: (u16, u16) = result.expect("Failed to parse edge"); - let mut p1_vec = Vec::new(); - context.with_query_results((Id(record.0),), &mut |people| { - p1_vec = people.to_owned_vec() - }); + let p1_vec: Vec> = context.query(((Id(record.0)),)).into_iter().collect(); assert_eq!(p1_vec.len(), 1); - let p1 = p1_vec[0]; - let mut p2_vec = Vec::new(); - context.with_query_results((Id(record.1),), &mut |people| { - p2_vec = people.to_owned_vec() - }); + let p2_vec: Vec> = context.query(((Id(record.1)),)).into_iter().collect(); assert_eq!(p2_vec.len(), 1); - let p2 = p2_vec[0]; - add_bidi_edge(context, p1, p2, rr); + add_bidi_edge(context, p1_vec[0], p2_vec[0], rr); } } @@ -80,7 +71,7 @@ pub fn get_contacts(context: &Context, person: EntityId, duration: f64) let mut contacts: Vec> = Vec::new(); - for edge in context.query(with!(Edge, Node1(person))) { + for edge in context.query((Node1(person), )) { let RR(rr): RR = context.get_property(edge); let Node2(person2): Node2 = context.get_property(edge); @@ -114,20 +105,32 @@ pub fn init(context: &mut Context) { mod tests { use super::*; use crate::{loader, network, Person}; + use crate::parameters::ParametersValues; // Assert that person with `id` has `n` contacts (i.e., edges going from // them, and also edges going to them) fn assert_has_n_contacts(id: u16, n: usize) { let mut context = Context::new(); context.init_random(42); - let people = loader::init(&mut context); + loader::init(&mut context); + let parameters = ParametersValues { + incubation_period: 8.0, + infectious_period: 27.0, + sar: 1.0, + shape: 15.0, + infection_duration: 5.0, + between_hh_transmission_reduction: 1.0, + data_dir: "examples/network-hhmodel/tests".to_owned(), + output_dir: "examples/network-hhmodel/tests".to_owned(), + }; + context + .set_global_property_value(Parameters, parameters) + .unwrap(); network::init(&mut context); - // `id` is the data ID, the one in the csv's - // `pid` is the integer inside Person(pid) - let person = context.query(with!(Person, Id(id))).into_iter().next().unwrap(); - let n_to = context.query_entity_count(with!(Edge, Node1(person))); - let n_from = context.query_entity_count(with!(Edge, Node2(person))); + let person = context.query((Id(id),)).into_iter().next().unwrap(); + let n_to = context.query_entity_count((Node1(person),)); + let n_from = context.query_entity_count((Node2(person), )); assert_eq!(n_to, n); assert_eq!(n_from, n); } @@ -141,8 +144,8 @@ mod tests { #[test] fn test_person_243() { - // Person 243 is in a household of size 5, with 4 other contacts, - // for 8 total contacts. - assert_has_n_contacts(243, 8); + // Person 243 is in a household of size 6, with 3 other contacts, + // for 9 total contacts. + assert_has_n_contacts(243, 9); } } diff --git a/examples/network-hhmodel/seir.rs b/examples/network-hhmodel/seir.rs index b9594bc4..ae8bffcb 100644 --- a/examples/network-hhmodel/seir.rs +++ b/examples/network-hhmodel/seir.rs @@ -138,7 +138,7 @@ mod tests { context.init_random(42); - let people = loader::init(&mut context); + loader::init(&mut context); // set sar and between_hh_transmission_reduction to 1.0 so that // beta is 1.0 @@ -156,7 +156,7 @@ mod tests { .set_global_property_value(Parameters, parameters) .unwrap(); - network::init(&mut context, &people); + network::init(&mut context); let mut to_infect = Vec::::new(); context.with_query_results((Id(71),), &mut |people| { From 7bc42fe46d53b5c5e12906eab93d48ac691a9f63 Mon Sep 17 00:00:00 2001 From: "Beau B. Bruce" Date: Wed, 29 Apr 2026 08:03:11 -0400 Subject: [PATCH 11/11] fix: assert! -> debug_assert! --- examples/network-hhmodel/network.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/network-hhmodel/network.rs b/examples/network-hhmodel/network.rs index f83239cf..dd067f42 100644 --- a/examples/network-hhmodel/network.rs +++ b/examples/network-hhmodel/network.rs @@ -34,7 +34,7 @@ fn create_household_networks(context: &mut Context, rr: RR) { // create a dense network for i in 0..(members.len() - 1) { for j in (i + 1)..members.len() { - assert!(i != j); + debug_assert!(i != j); add_bidi_edge(context, members[i], members[j], rr); } }