#![allow(clippy::type_complexity)]
use rand::rngs::StdRng;
use rand::SeedableRng;
use rustsim_core::{
collect::collect_step,
interaction::{self, PositionedAgent},
prelude::*,
};
mod support;
use support::{Grid2D, GridPos2, NothingSpace};
#[derive(Debug, Clone)]
struct Counter {
id: AgentId,
count: u64,
}
impl Agent for Counter {
fn id(&self) -> AgentId {
self.id
}
}
#[derive(Debug, Clone)]
struct GridBot {
id: AgentId,
pos: GridPos2,
#[allow(dead_code)]
energy: i32,
}
impl Agent for GridBot {
fn id(&self) -> AgentId {
self.id
}
}
impl PositionedAgent for GridBot {
type Position = GridPos2;
fn position(&self) -> &GridPos2 {
&self.pos
}
fn set_position(&mut self, p: GridPos2) {
self.pos = p;
}
}
type CounterModel =
StandardModel<NothingSpace, Counter, HashMapStore<Counter>, (), StdRng, Fastest>;
type GridModel = StandardModel<Grid2D, GridBot, HashMapStore<GridBot>, (), StdRng, Fastest>;
fn counting_step(
agent: &mut Counter,
_ctx: &mut StepContext<'_, NothingSpace, Counter, (), StdRng, Fastest>,
) {
agent.count += 1;
}
#[test]
fn time_starts_at_zero_and_increments_each_step() {
let mut store = HashMapStore::new();
store.insert(Counter { id: 1, count: 0 });
let mut model = CounterModel::new(
store,
NothingSpace,
Fastest::new(),
(),
StdRng::seed_from_u64(0),
Some(Box::new(counting_step)),
None,
true,
);
assert_eq!(
model.time(),
rustsim_core::types::Time::Discrete(0),
"Julia: abmtime(model) == 0 initially"
);
model.step();
assert_eq!(
model.time(),
rustsim_core::types::Time::Discrete(1),
"Julia: step! increments time by 1"
);
model.step_n(4);
assert_eq!(
model.time(),
rustsim_core::types::Time::Discrete(5),
"Julia: after 5 steps, time == 5"
);
}
#[test]
fn agents_first_true_runs_agents_before_model() {
type OrderModel = StandardModel<
NothingSpace,
Counter,
HashMapStore<Counter>,
Vec<&'static str>,
StdRng,
Fastest,
>;
fn agent_step_af(
agent: &mut Counter,
ctx: &mut StepContext<'_, NothingSpace, Counter, Vec<&'static str>, StdRng, Fastest>,
) {
ctx.properties_mut().push("agent");
agent.count += 1;
}
fn model_step_af(model: &mut OrderModel) {
model.properties_mut().push("model");
}
let mut store = HashMapStore::new();
store.insert(Counter { id: 1, count: 0 });
let mut model = OrderModel::new(
store,
NothingSpace,
Fastest::new(),
Vec::new(),
StdRng::seed_from_u64(0),
Some(Box::new(agent_step_af)),
Some(model_step_af),
true,
);
model.step();
assert_eq!(
model.properties().as_slice(),
&["agent", "model"],
"Julia: agents_first=true -> agent step then model step"
);
}
#[test]
fn agents_first_false_runs_model_before_agents() {
type OrderModel = StandardModel<
NothingSpace,
Counter,
HashMapStore<Counter>,
Vec<&'static str>,
StdRng,
Fastest,
>;
fn agent_step_mf(
agent: &mut Counter,
ctx: &mut StepContext<'_, NothingSpace, Counter, Vec<&'static str>, StdRng, Fastest>,
) {
ctx.properties_mut().push("agent");
agent.count += 1;
}
fn model_step_mf(model: &mut OrderModel) {
model.properties_mut().push("model");
}
let mut store = HashMapStore::new();
store.insert(Counter { id: 1, count: 0 });
let mut model = OrderModel::new(
store,
NothingSpace,
Fastest::new(),
Vec::new(),
StdRng::seed_from_u64(0),
Some(Box::new(agent_step_mf)),
Some(model_step_mf),
false,
);
model.step();
assert_eq!(
model.properties().as_slice(),
&["model", "agent"],
"Julia: agents_first=false -> model step then agent step"
);
}
type RemovableModel = StandardModel<NothingSpace, Counter, HashMapStore<Counter>, (), StdRng, ById>;
fn remove_self_if_even(
agent: &mut Counter,
ctx: &mut StepContext<'_, NothingSpace, Counter, (), StdRng, ById>,
) {
if agent.id.is_multiple_of(2) {
ctx.defer_remove_agent(agent.id());
} else {
agent.count += 1;
}
}
#[test]
fn removed_agents_skipped_during_step() {
let mut store = HashMapStore::new();
for i in 1..=4 {
store.insert(Counter { id: i, count: 0 });
}
let mut model = RemovableModel::new(
store,
NothingSpace,
ById::new(),
(),
StdRng::seed_from_u64(0),
Some(Box::new(remove_self_if_even)),
None,
true,
);
model.step();
assert!(model.agent(2).is_none(), "agent 2 removed itself");
assert!(model.agent(4).is_none(), "agent 4 removed itself");
assert_eq!(model.agent(1).unwrap().count, 1);
assert_eq!(model.agent(3).unwrap().count, 1);
}
#[test]
fn nearby_ids_except_excludes_self() {
let store = HashMapStore::new();
let grid = Grid2D::new(5, 5, false);
let mut model = GridModel::new(
store,
grid,
Fastest::new(),
(),
StdRng::seed_from_u64(0),
None,
None,
true,
);
let a1 = GridBot {
id: model.next_id(),
pos: (2, 2),
energy: 10,
};
let a2 = GridBot {
id: model.next_id(),
pos: (2, 3),
energy: 5,
};
interaction::add_agent(&mut model, a1).unwrap();
interaction::add_agent(&mut model, a2).unwrap();
let all = interaction::nearby_ids(&model, &(2, 2), 1);
assert!(
all.contains(&1),
"position-based includes agent at that position"
);
assert!(all.contains(&2));
let without_self = interaction::nearby_ids_except(&model, &(2, 2), 1, 1);
assert!(
!without_self.contains(&1),
"Julia: nearby_ids(agent, model, r) excludes agent.id"
);
assert!(without_self.contains(&2));
}
fn model_only_step(model: &mut CounterModel) {
let ids: Vec<AgentId> = model.agents().map(|a| a.id()).collect();
for id in ids {
if let Some(mut a) = model.agent_mut(id) {
a.count += 10;
}
}
}
#[test]
fn model_only_step_works_without_agent_step() {
let mut store = HashMapStore::new();
store.insert(Counter { id: 1, count: 0 });
store.insert(Counter { id: 2, count: 0 });
let mut model = CounterModel::new(
store,
NothingSpace,
Fastest::new(),
(),
StdRng::seed_from_u64(0),
None,
Some(model_only_step),
true,
);
model.step_n(3);
assert_eq!(model.time(), rustsim_core::types::Time::Discrete(3));
assert_eq!(model.agent(1).unwrap().count, 30);
assert_eq!(model.agent(2).unwrap().count, 30);
}
type RandomCounterModel =
StandardModel<NothingSpace, Counter, HashMapStore<Counter>, (), StdRng, Randomly>;
fn counting_step_random(
agent: &mut Counter,
_ctx: &mut StepContext<'_, NothingSpace, Counter, (), StdRng, Randomly>,
) {
agent.count += 1;
}
#[test]
fn deterministic_rng_same_seed_same_result() {
fn run_sim(seed: u64) -> Vec<u64> {
let mut store = HashMapStore::new();
for i in 1..=5 {
store.insert(Counter { id: i, count: 0 });
}
let mut model = RandomCounterModel::new(
store,
NothingSpace,
Randomly::new(),
(),
StdRng::seed_from_u64(seed),
Some(Box::new(counting_step_random)),
None,
true,
);
model.step_n(10);
let mut results: Vec<(AgentId, u64)> = model.agents().map(|a| (a.id, a.count)).collect();
results.sort_by_key(|r| r.0);
results.iter().map(|r| r.1).collect()
}
let run1 = run_sim(42);
let run2 = run_sim(42);
assert_eq!(run1, run2, "Julia: same seed produces identical results");
}
#[test]
fn next_id_auto_increments() {
let store: HashMapStore<Counter> = HashMapStore::new();
let mut model = CounterModel::new(
store,
NothingSpace,
Fastest::new(),
(),
StdRng::seed_from_u64(0),
None,
None,
true,
);
let id1 = model.next_id();
let id2 = model.next_id();
let id3 = model.next_id();
assert_eq!(id1, 1, "Julia: nextid starts at 1");
assert_eq!(id2, 2);
assert_eq!(id3, 3);
}
#[test]
fn data_collection_mirrors_julia_run() {
let mut store = HashMapStore::new();
for i in 1..=3 {
store.insert(Counter { id: i, count: 0 });
}
let mut model = CounterModel::new(
store,
NothingSpace,
Fastest::new(),
(),
StdRng::seed_from_u64(0),
Some(Box::new(counting_step)),
None,
true,
);
let mut agent_data: Vec<(rustsim_core::types::Time, u64, u64)> = Vec::new();
let mut model_data: Vec<(rustsim_core::types::Time, usize)> = Vec::new();
let ids: Vec<AgentId> = model.agents().map(|a| a.id()).collect();
collect_step(
&model,
&ids,
Some(&|a: &Counter, m: &CounterModel| (m.time(), a.id, a.count)),
Some(&|m: &CounterModel| (m.time(), m.agents().count())),
&mut agent_data,
&mut model_data,
);
for _ in 0..3 {
model.step();
let ids: Vec<AgentId> = model.agents().map(|a| a.id()).collect();
collect_step(
&model,
&ids,
Some(&|a: &Counter, m: &CounterModel| (m.time(), a.id, a.count)),
Some(&|m: &CounterModel| (m.time(), m.agents().count())),
&mut agent_data,
&mut model_data,
);
}
assert_eq!(agent_data.len(), 12);
assert_eq!(model_data.len(), 4);
let t0: Vec<_> = agent_data
.iter()
.filter(|r| r.0 == rustsim_core::types::Time::Discrete(0))
.collect();
assert!(t0.iter().all(|r| r.2 == 0));
let t3: Vec<_> = agent_data
.iter()
.filter(|r| r.0 == rustsim_core::types::Time::Discrete(3))
.collect();
assert!(t3.iter().all(|r| r.2 == 3));
}
#[test]
fn by_property_scheduler_orders_greatest_first() {
let mut store = HashMapStore::new();
store.insert(Counter { id: 1, count: 10 });
store.insert(Counter { id: 2, count: 30 });
store.insert(Counter { id: 3, count: 20 });
let model = StandardModel::<
NothingSpace,
Counter,
HashMapStore<Counter>,
(),
StdRng,
ByProperty<_>,
>::new(
store,
NothingSpace,
ByProperty::new(|a: &Counter| a.count),
(),
StdRng::seed_from_u64(0),
None,
None,
true,
);
let mut sched = ByProperty::new(|a: &Counter| a.count);
let mut buf = Vec::new();
sched.schedule_into(&model, &mut buf);
assert_eq!(
buf,
vec![2, 3, 1],
"Julia: ByProperty orders agents with greatest property first"
);
}
#[test]
fn by_id_scheduler_sorts_ascending() {
let mut store = HashMapStore::new();
store.insert(Counter { id: 5, count: 0 });
store.insert(Counter { id: 1, count: 0 });
store.insert(Counter { id: 3, count: 0 });
let model =
StandardModel::<NothingSpace, Counter, HashMapStore<Counter>, (), StdRng, ById>::new(
store,
NothingSpace,
ById::new(),
(),
StdRng::seed_from_u64(0),
None,
None,
true,
);
let mut sched = ById::new();
let mut buf = Vec::new();
sched.schedule_into(&model, &mut buf);
assert_eq!(
buf,
vec![1, 3, 5],
"Julia: ByID() returns IDs sorted ascending"
);
}
use rustsim_core::event_queue::{EventContext, EventQueueModel};
type EQModel = EventQueueModel<NothingSpace, Counter, HashMapStore<Counter>, (), StdRng>;
fn eq_action(agent: &mut Counter, _ctx: &mut EventContext<'_, NothingSpace, Counter, (), StdRng>) {
agent.count += 1;
}
#[test]
fn event_queue_processes_chronologically() {
let mut store = HashMapStore::new();
store.insert(Counter { id: 1, count: 0 });
let actions: Vec<fn(&mut Counter, &mut EventContext<'_, NothingSpace, Counter, (), StdRng>)> =
vec![eq_action];
let mut model = EQModel::new(store, NothingSpace, (), StdRng::seed_from_u64(0), actions);
model.add_event(1, 0, 3.0);
model.add_event(1, 0, 1.0);
model.add_event(1, 0, 2.0);
model.step_event();
assert!(
(model.time_f64() - 1.0).abs() < 1e-10,
"Julia: earliest event processed first"
);
model.step_event();
assert!((model.time_f64() - 2.0).abs() < 1e-10);
model.step_event();
assert!((model.time_f64() - 3.0).abs() < 1e-10);
assert_eq!(model.agent(1).unwrap().count, 3);
}
#[test]
fn event_queue_step_until_respects_boundary() {
let mut store = HashMapStore::new();
store.insert(Counter { id: 1, count: 0 });
let actions: Vec<fn(&mut Counter, &mut EventContext<'_, NothingSpace, Counter, (), StdRng>)> =
vec![eq_action];
let mut model = EQModel::new(store, NothingSpace, (), StdRng::seed_from_u64(0), actions);
model.add_event(1, 0, 1.0);
model.add_event(1, 0, 2.0);
model.add_event(1, 0, 5.0);
model.step_until(3.0);
assert_eq!(
model.agent(1).unwrap().count,
2,
"Julia: only events with time <= stop_time fire"
);
assert!(
(model.time_f64() - 3.0).abs() < 1e-10,
"Julia: model time advances to stop_time"
);
assert_eq!(model.queue_len(), 1, "one event remains at t=5");
}
#[test]
fn event_queue_skips_removed_agents() {
let mut store = HashMapStore::new();
store.insert(Counter { id: 1, count: 0 });
store.insert(Counter { id: 2, count: 0 });
let actions: Vec<fn(&mut Counter, &mut EventContext<'_, NothingSpace, Counter, (), StdRng>)> =
vec![eq_action];
let mut model = EQModel::new(store, NothingSpace, (), StdRng::seed_from_u64(0), actions);
model.add_event(1, 0, 1.0);
model.add_event(2, 0, 2.0);
model.remove_agent(1);
model.step_event();
assert!((model.time_f64() - 1.0).abs() < 1e-10);
model.step_event();
assert_eq!(model.agent(2).unwrap().count, 1);
}