use rand::rngs::StdRng;
use rand::SeedableRng;
use rustsim_core::{prelude::*, step_context::StepContext};
mod support;
use std::time::Instant;
use support::NothingSpace;
const AGENT_COUNT: u64 = 1_000_000;
const STEP_COUNT: usize = 10;
#[derive(Debug, Clone)]
struct Particle {
id: AgentId,
x: f32,
vx: f32,
}
impl Agent for Particle {
fn id(&self) -> AgentId {
self.id
}
}
type ParticleModel =
StandardModel<NothingSpace, Particle, HashMapStore<Particle>, (), StdRng, Fastest>;
fn particle_step(
agent: &mut Particle,
_ctx: &mut StepContext<'_, NothingSpace, Particle, (), StdRng, Fastest>,
) {
agent.x += agent.vx;
}
#[test]
fn one_million_agents_hashmap_store() {
let t0 = Instant::now();
let mut store = HashMapStore::new();
for i in 1..=AGENT_COUNT {
store.insert(Particle {
id: i,
x: 0.0,
vx: 0.001,
});
}
assert_eq!(store.len(), AGENT_COUNT as usize);
let populate_ms = t0.elapsed().as_millis();
eprintln!(
"[HashMapStore] Populated {} agents in {} ms",
AGENT_COUNT, populate_ms
);
let mut model = ParticleModel::new(
store,
NothingSpace,
Fastest::new(),
(),
StdRng::seed_from_u64(42),
Some(Box::new(particle_step)),
None,
true,
);
let t1 = Instant::now();
model.step_n(STEP_COUNT);
let step_ms = t1.elapsed().as_millis();
let per_step_ms = step_ms as f64 / STEP_COUNT as f64;
eprintln!(
"[HashMapStore] {} steps x {} agents in {} ms ({:.1} ms/step)",
STEP_COUNT, AGENT_COUNT, step_ms, per_step_ms
);
assert_eq!(
model.time(),
rustsim_core::types::Time::Discrete(STEP_COUNT as u64)
);
let a1 = model.agent(1).unwrap();
let expected = STEP_COUNT as f32 * 0.001;
assert!(
(a1.x - expected).abs() < 1e-5,
"agent 1: expected x={}, got x={}",
expected,
a1.x
);
drop(a1);
assert_eq!(model.agents_len(), AGENT_COUNT as usize);
let checksum: f64 = model.agents().map(|a| a.x as f64).sum();
drop(model);
let mut store2 = HashMapStore::new();
for i in 1..=AGENT_COUNT {
store2.insert(Particle {
id: i,
x: 0.0,
vx: 0.001,
});
}
let mut model2 = ParticleModel::new(
store2,
NothingSpace,
Fastest::new(),
(),
StdRng::seed_from_u64(42),
Some(Box::new(particle_step)),
None,
true,
);
model2.step_n(STEP_COUNT);
let checksum2: f64 = model2.agents().map(|a| a.x as f64).sum();
assert!(
(checksum - checksum2).abs() < 1e-6,
"determinism broken: checksum {} vs {}",
checksum,
checksum2
);
assert!(
populate_ms < 10_000,
"populating 1M agents should take < 10s (took {} ms)",
populate_ms
);
assert!(
per_step_ms < 5_000.0,
"stepping 1M agents should take < 5s/step (took {:.0} ms/step)",
per_step_ms
);
eprintln!("[HashMapStore] All assertions passed.");
eprintln!(" Populate: {} ms", populate_ms);
eprintln!(" Per-step: {:.1} ms", per_step_ms);
eprintln!(" Checksum: {:.6}", checksum);
}
type ParticleModelVec =
StandardModel<NothingSpace, Particle, VecStore<Particle>, (), StdRng, Fastest>;
#[test]
fn one_million_agents_vec_store() {
let t0 = Instant::now();
let mut store = VecStore::new();
for i in 1..=AGENT_COUNT {
store.insert(Particle {
id: i,
x: 0.0,
vx: 0.001,
});
}
assert_eq!(store.len(), AGENT_COUNT as usize);
let populate_ms = t0.elapsed().as_millis();
eprintln!(
"[VecStore] Populated {} agents in {} ms",
AGENT_COUNT, populate_ms
);
let mut model = ParticleModelVec::new(
store,
NothingSpace,
Fastest::new(),
(),
StdRng::seed_from_u64(42),
Some(Box::new(particle_step)),
None,
true,
);
let t1 = Instant::now();
model.step_n(STEP_COUNT);
let step_ms = t1.elapsed().as_millis();
let per_step_ms = step_ms as f64 / STEP_COUNT as f64;
eprintln!(
"[VecStore] {} steps x {} agents in {} ms ({:.1} ms/step)",
STEP_COUNT, AGENT_COUNT, step_ms, per_step_ms
);
assert_eq!(
model.time(),
rustsim_core::types::Time::Discrete(STEP_COUNT as u64)
);
assert_eq!(model.agents_len(), AGENT_COUNT as usize);
let a1 = model.agent(1).unwrap();
let expected = STEP_COUNT as f32 * 0.001;
assert!(
(a1.x - expected).abs() < 1e-5,
"agent 1: expected x={}, got x={}",
expected,
a1.x
);
assert!(
populate_ms < 10_000,
"populating 1M agents should take < 10s (took {} ms)",
populate_ms
);
assert!(
per_step_ms < 5_000.0,
"stepping 1M agents should take < 5s/step (took {:.0} ms/step)",
per_step_ms
);
eprintln!("[VecStore] All assertions passed.");
eprintln!(" Populate: {} ms", populate_ms);
eprintln!(" Per-step: {:.1} ms", per_step_ms);
}
#[test]
fn million_agents_with_deferred_removal() {
let mut store = HashMapStore::new();
for i in 1..=AGENT_COUNT {
store.insert(Particle {
id: i,
x: 0.0,
vx: 0.001,
});
}
fn remove_evens(
agent: &mut Particle,
ctx: &mut StepContext<'_, NothingSpace, Particle, (), StdRng, Fastest>,
) {
if agent.id.is_multiple_of(2) {
ctx.defer_remove_agent(agent.id());
} else {
agent.x += agent.vx;
}
}
let mut model = ParticleModel::new(
store,
NothingSpace,
Fastest::new(),
(),
StdRng::seed_from_u64(42),
Some(Box::new(remove_evens)),
None,
true,
);
let t0 = Instant::now();
model.step();
let ms = t0.elapsed().as_millis();
assert_eq!(
model.agents_len(),
(AGENT_COUNT / 2) as usize,
"500K agents should remain after removing evens"
);
let a1 = model.agent(1).unwrap();
assert!((a1.x - 0.001).abs() < 1e-6);
drop(a1);
assert!(model.agent(2).is_none(), "agent 2 (even) should be removed");
eprintln!(
"[Deferred removal] Removed 500K / stepped 500K in {} ms",
ms
);
assert!(
ms < 10_000,
"1 step with 500K removals should take < 10s (took {} ms)",
ms
);
}