#![allow(clippy::type_complexity)]
use rand::rngs::StdRng;
use rand::SeedableRng;
use rustsim_core::{
event_queue::{EventContext, EventQueueModel},
prelude::*,
};
mod support;
use std::time::Instant;
use support::NothingSpace;
#[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, VecStore<Particle>, (), StdRng, ById>;
fn particle_step(
agent: &mut Particle,
_ctx: &mut StepContext<'_, NothingSpace, Particle, (), StdRng, ById>,
) {
agent.x += agent.vx;
}
#[test]
fn sustained_standard_model_soak_preserves_population_and_values() {
const AGENT_COUNT: u64 = 20_000;
const STEP_COUNT: usize = 200;
let mut store = VecStore::new();
for id in 1..=AGENT_COUNT {
store.insert(Particle {
id,
x: 0.0,
vx: 0.01 + (id % 7) as f32 * 0.001,
});
}
let mut model = ParticleModel::new(
store,
NothingSpace,
ById::new(),
(),
StdRng::seed_from_u64(42),
Some(Box::new(particle_step)),
None,
true,
);
let t0 = Instant::now();
for step in 0..STEP_COUNT {
model.step();
if (step + 1) % 50 == 0 {
assert_eq!(model.agents_len(), AGENT_COUNT as usize);
}
}
let elapsed_ms = t0.elapsed().as_millis();
assert_eq!(model.time(), Time::Discrete(STEP_COUNT as u64));
assert_eq!(model.agents_len(), AGENT_COUNT as usize);
let a1 = model.agent(1).unwrap();
let expected_a1 = STEP_COUNT as f32 * 0.011;
assert!((a1.x - expected_a1).abs() < 1e-4);
drop(a1);
let checksum: f64 = model.agents().map(|a| a.x as f64).sum();
assert!(checksum.is_finite());
assert!(checksum > 0.0);
eprintln!(
"[core soak] {} agents x {} steps completed in {} ms",
AGENT_COUNT, STEP_COUNT, elapsed_ms
);
}
#[derive(Debug, Clone)]
struct TimedAgent {
id: AgentId,
remaining: u32,
handled: u32,
}
impl Agent for TimedAgent {
fn id(&self) -> AgentId {
self.id
}
}
type TimedModel = EventQueueModel<NothingSpace, TimedAgent, VecStore<TimedAgent>, (), StdRng>;
fn timed_action(
agent: &mut TimedAgent,
ctx: &mut EventContext<'_, NothingSpace, TimedAgent, (), StdRng>,
) {
agent.handled += 1;
agent.remaining -= 1;
if agent.remaining > 0 {
ctx.add_event(agent.id, 0, 1.0);
}
}
#[test]
fn event_queue_soak_handles_large_rescheduling_volume() {
const AGENT_COUNT: u64 = 5_000;
const EVENTS_PER_AGENT: u32 = 8;
let mut store = VecStore::new();
for id in 1..=AGENT_COUNT {
store.insert(TimedAgent {
id,
remaining: EVENTS_PER_AGENT,
handled: 0,
});
}
let actions: Vec<
fn(&mut TimedAgent, &mut EventContext<'_, NothingSpace, TimedAgent, (), StdRng>),
> = vec![timed_action];
let mut model = TimedModel::new(store, NothingSpace, (), StdRng::seed_from_u64(7), actions);
for id in 1..=AGENT_COUNT {
model.add_event(id, 0, 1.0);
}
let t0 = Instant::now();
model.run_events((AGENT_COUNT as usize) * (EVENTS_PER_AGENT as usize));
let elapsed_ms = t0.elapsed().as_millis();
assert!(model.queue_is_empty());
assert!((model.time_f64() - EVENTS_PER_AGENT as f64).abs() < 1e-10);
for id in 1..=AGENT_COUNT {
let agent = model.agent(id).unwrap();
assert_eq!(agent.handled, EVENTS_PER_AGENT);
assert_eq!(agent.remaining, 0);
}
eprintln!(
"[event soak] {} agents x {} events completed in {} ms",
AGENT_COUNT, EVENTS_PER_AGENT, elapsed_ms
);
}
#[test]
#[ignore]
fn scale_profile_standard_model_large_population() {
const AGENT_COUNT: u64 = 250_000;
const STEP_COUNT: usize = 50;
let mut store = VecStore::new();
for id in 1..=AGENT_COUNT {
store.insert(Particle {
id,
x: 0.0,
vx: 0.001,
});
}
let mut model = ParticleModel::new(
store,
NothingSpace,
ById::new(),
(),
StdRng::seed_from_u64(42),
Some(Box::new(particle_step)),
None,
true,
);
let t0 = Instant::now();
model.step_n(STEP_COUNT);
let elapsed_ms = t0.elapsed().as_millis();
assert_eq!(model.agents_len(), AGENT_COUNT as usize);
assert_eq!(model.time(), Time::Discrete(STEP_COUNT as u64));
assert!((model.agent(1).unwrap().x - (STEP_COUNT as f32 * 0.001)).abs() < 1e-5);
eprintln!(
"[core scale] {} agents x {} steps completed in {} ms",
AGENT_COUNT, STEP_COUNT, elapsed_ms
);
}