use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use rustsim_crowd::{
anticipation_velocity,
broadphase::{recommended_cell_size, Scratch},
collision_free_speed,
common::{Pedestrian, WallSegment},
generalized_centrifugal_force, optimal_steps, social_force,
};
use std::time::Instant;
fn seed_counterflow(n: usize, length: f64, width: f64, seed: u64) -> Vec<Pedestrian> {
let mut rng = StdRng::seed_from_u64(seed);
(0..n)
.map(|i| {
let x: f64 = rng.gen_range(0.0..length);
let y: f64 = rng.gen_range(0.0..width);
let dir = if i % 2 == 0 { 1.0 } else { -1.0 };
let dest_x = x + dir * 10.0 * length;
Pedestrian::new([x, y], [0.0, 0.0], 0.25, 1.34, [dest_x, y])
})
.collect()
}
fn wrap_and_retarget(peds: &mut [Pedestrian], length: f64, width: f64) {
for p in peds.iter_mut() {
if p.pos[0] < 0.0 {
p.pos[0] += length;
p.destination[0] += length;
} else if p.pos[0] >= length {
p.pos[0] -= length;
p.destination[0] -= length;
}
if p.pos[1] < 0.0 {
p.pos[1] = 0.0;
p.vel[1] = 0.0;
} else if p.pos[1] >= width {
p.pos[1] = width;
p.vel[1] = 0.0;
}
}
}
#[inline]
fn assert_invariants(peds: &[Pedestrian], n: usize, speed_cap: f64, tick: usize) {
assert_eq!(peds.len(), n, "population not conserved at tick {tick}");
for (i, p) in peds.iter().enumerate() {
assert!(
p.pos[0].is_finite() && p.pos[1].is_finite(),
"non-finite position at tick {tick}, agent {i}: {:?}",
p.pos
);
assert!(
p.vel[0].is_finite() && p.vel[1].is_finite(),
"non-finite velocity at tick {tick}, agent {i}: {:?}",
p.vel
);
let speed = (p.vel[0] * p.vel[0] + p.vel[1] * p.vel[1]).sqrt();
assert!(
speed <= speed_cap,
"speed {speed} exceeds cap {speed_cap} at tick {tick}, agent {i}"
);
}
}
#[test]
fn social_force_soak_mid_scale() {
const N: usize = 2_000;
const LENGTH: f64 = 80.0;
const WIDTH: f64 = 20.0;
const DT: f64 = 0.05;
const NUM_TICKS: usize = 600;
let params = social_force::Params::default();
params
.validate(DT)
.expect("default SFM params must validate at dt = 0.05 s");
let walls: Vec<WallSegment> = Vec::new();
let mut peds = seed_counterflow(N, LENGTH, WIDTH, 0xC0FFEE);
let cutoff = social_force::neighbor_cutoff(¶ms);
let cell = recommended_cell_size(cutoff);
let mut scratch = Scratch::with_capacity(N, cell);
let speed_cap = params.max_speed * 1.5;
let t0 = Instant::now();
for tick in 0..NUM_TICKS {
social_force::step_scratch(&mut peds, &walls, ¶ms, DT, &mut scratch);
wrap_and_retarget(&mut peds, LENGTH, WIDTH);
assert_invariants(&peds, N, speed_cap, tick);
}
let elapsed_ms = t0.elapsed().as_millis();
eprintln!(
"[crowd soak] {N} agents x {NUM_TICKS} ticks ({:.1} s simulated) completed in {elapsed_ms} ms",
NUM_TICKS as f64 * DT
);
}
#[test]
#[ignore = "long-running soak test; run with --ignored in the workspace soak lane"]
fn social_force_soak_10k_one_hour() {
const N: usize = 10_000;
const LENGTH: f64 = 200.0;
const WIDTH: f64 = 50.0;
const DT: f64 = 0.05;
const NUM_TICKS: usize = 72_000;
let params = social_force::Params::default();
params.validate(DT).expect("params must validate");
let walls: Vec<WallSegment> = Vec::new();
let mut peds = seed_counterflow(N, LENGTH, WIDTH, 0xDEADBEEF);
let cutoff = social_force::neighbor_cutoff(¶ms);
let cell = recommended_cell_size(cutoff);
let mut scratch = Scratch::with_capacity(N, cell);
let speed_cap = params.max_speed * 1.5;
let t0 = Instant::now();
for tick in 0..NUM_TICKS {
social_force::step_scratch(&mut peds, &walls, ¶ms, DT, &mut scratch);
wrap_and_retarget(&mut peds, LENGTH, WIDTH);
if tick % 200 == 0 {
assert_invariants(&peds, N, speed_cap, tick);
}
}
assert_invariants(&peds, N, speed_cap, NUM_TICKS);
let elapsed_ms = t0.elapsed().as_millis();
eprintln!(
"[crowd soak 10k/1h] {N} agents x {NUM_TICKS} ticks (3600 s simulated) completed in {elapsed_ms} ms"
);
}
const KINEMATIC_SPEED_CAP: f64 = 1.34 * 1.5;
#[test]
fn collision_free_speed_soak_mid_scale() {
const N: usize = 2_000;
const LENGTH: f64 = 80.0;
const WIDTH: f64 = 20.0;
const DT: f64 = 0.05;
const NUM_TICKS: usize = 600;
let params = collision_free_speed::Params::default();
params
.validate(DT)
.expect("default CFS params must validate at dt = 0.05 s");
let walls: Vec<WallSegment> = Vec::new();
let mut peds = seed_counterflow(N, LENGTH, WIDTH, 0xCF5_0001);
let cutoff = collision_free_speed::neighbor_cutoff(¶ms);
let cell = recommended_cell_size(cutoff);
let mut scratch = Scratch::with_capacity(N, cell);
let t0 = Instant::now();
for tick in 0..NUM_TICKS {
collision_free_speed::step_scratch(&mut peds, &walls, ¶ms, DT, &mut scratch);
wrap_and_retarget(&mut peds, LENGTH, WIDTH);
assert_invariants(&peds, N, KINEMATIC_SPEED_CAP, tick);
}
let elapsed_ms = t0.elapsed().as_millis();
eprintln!(
"[crowd soak CFS] {N} agents x {NUM_TICKS} ticks ({:.1} s simulated) completed in {elapsed_ms} ms",
NUM_TICKS as f64 * DT
);
}
#[test]
fn anticipation_velocity_soak_mid_scale() {
const N: usize = 2_000;
const LENGTH: f64 = 80.0;
const WIDTH: f64 = 20.0;
const DT: f64 = 0.05;
const NUM_TICKS: usize = 600;
let params = anticipation_velocity::Params::default();
params
.validate(DT)
.expect("default AVM params must validate at dt = 0.05 s");
let walls: Vec<WallSegment> = Vec::new();
let mut peds = seed_counterflow(N, LENGTH, WIDTH, 0xA_0001);
let cutoff = anticipation_velocity::neighbor_cutoff(¶ms);
let cell = recommended_cell_size(cutoff);
let mut scratch = Scratch::with_capacity(N, cell);
let t0 = Instant::now();
for tick in 0..NUM_TICKS {
anticipation_velocity::step_scratch(&mut peds, &walls, ¶ms, DT, &mut scratch);
wrap_and_retarget(&mut peds, LENGTH, WIDTH);
assert_invariants(&peds, N, KINEMATIC_SPEED_CAP, tick);
}
let elapsed_ms = t0.elapsed().as_millis();
eprintln!(
"[crowd soak AVM] {N} agents x {NUM_TICKS} ticks ({:.1} s simulated) completed in {elapsed_ms} ms",
NUM_TICKS as f64 * DT
);
}
#[test]
fn generalized_centrifugal_force_soak_mid_scale() {
const N: usize = 2_000;
const LENGTH: f64 = 80.0;
const WIDTH: f64 = 20.0;
const DT: f64 = 0.05;
const NUM_TICKS: usize = 600;
let params = generalized_centrifugal_force::Params::default();
params
.validate(DT)
.expect("default GCF params must validate at dt = 0.05 s");
let walls: Vec<WallSegment> = Vec::new();
let mut peds = seed_counterflow(N, LENGTH, WIDTH, 0x6CF_0001);
let cutoff = generalized_centrifugal_force::neighbor_cutoff(¶ms);
let cell = recommended_cell_size(cutoff);
let mut scratch = Scratch::with_capacity(N, cell);
let speed_cap = params.max_speed * 1.5;
let t0 = Instant::now();
for tick in 0..NUM_TICKS {
generalized_centrifugal_force::step_scratch(&mut peds, &walls, ¶ms, DT, &mut scratch);
wrap_and_retarget(&mut peds, LENGTH, WIDTH);
assert_invariants(&peds, N, speed_cap, tick);
}
let elapsed_ms = t0.elapsed().as_millis();
eprintln!(
"[crowd soak GCF] {N} agents x {NUM_TICKS} ticks ({:.1} s simulated) completed in {elapsed_ms} ms",
NUM_TICKS as f64 * DT
);
}
#[test]
fn optimal_steps_soak_mid_scale() {
const N: usize = 2_000;
const LENGTH: f64 = 80.0;
const WIDTH: f64 = 20.0;
const DT: f64 = 0.4;
const NUM_STEPS: usize = 600;
let params = optimal_steps::Params::default();
params
.validate(DT)
.expect("default OSM params must validate at dt = 0.4 s");
let walls: Vec<WallSegment> = Vec::new();
let mut peds = seed_counterflow(N, LENGTH, WIDTH, 0x05_0001);
let cutoff = optimal_steps::neighbor_cutoff(¶ms);
let cell = recommended_cell_size(cutoff);
let mut scratch = Scratch::with_capacity(N, cell);
let t0 = Instant::now();
for tick in 0..NUM_STEPS {
optimal_steps::step_scratch(&mut peds, &walls, ¶ms, DT, &mut scratch);
wrap_and_retarget(&mut peds, LENGTH, WIDTH);
assert_invariants(&peds, N, KINEMATIC_SPEED_CAP, tick);
}
let elapsed_ms = t0.elapsed().as_millis();
eprintln!(
"[crowd soak OSM] {N} agents x {NUM_STEPS} steps ({:.1} s simulated) completed in {elapsed_ms} ms",
NUM_STEPS as f64 * DT
);
}
#[test]
#[ignore = "long-running soak test; run with --ignored in the workspace soak lane"]
fn collision_free_speed_soak_10k_one_hour() {
const N: usize = 10_000;
const LENGTH: f64 = 200.0;
const WIDTH: f64 = 50.0;
const DT: f64 = 0.05;
const NUM_TICKS: usize = 72_000;
let params = collision_free_speed::Params::default();
params.validate(DT).expect("CFS params must validate");
let walls: Vec<WallSegment> = Vec::new();
let mut peds = seed_counterflow(N, LENGTH, WIDTH, 0xCF5_BEEF);
let cutoff = collision_free_speed::neighbor_cutoff(¶ms);
let cell = recommended_cell_size(cutoff);
let mut scratch = Scratch::with_capacity(N, cell);
let t0 = Instant::now();
for tick in 0..NUM_TICKS {
collision_free_speed::step_scratch(&mut peds, &walls, ¶ms, DT, &mut scratch);
wrap_and_retarget(&mut peds, LENGTH, WIDTH);
if tick % 200 == 0 {
assert_invariants(&peds, N, KINEMATIC_SPEED_CAP, tick);
}
}
assert_invariants(&peds, N, KINEMATIC_SPEED_CAP, NUM_TICKS);
let elapsed_ms = t0.elapsed().as_millis();
eprintln!(
"[crowd soak CFS 10k/1h] {N} agents x {NUM_TICKS} ticks (3600 s simulated) completed in {elapsed_ms} ms"
);
}
#[test]
#[ignore = "long-running soak test; run with --ignored in the workspace soak lane"]
fn anticipation_velocity_soak_10k_one_hour() {
const N: usize = 10_000;
const LENGTH: f64 = 200.0;
const WIDTH: f64 = 50.0;
const DT: f64 = 0.05;
const NUM_TICKS: usize = 72_000;
let params = anticipation_velocity::Params::default();
params.validate(DT).expect("AVM params must validate");
let walls: Vec<WallSegment> = Vec::new();
let mut peds = seed_counterflow(N, LENGTH, WIDTH, 0xA_BEEF);
let cutoff = anticipation_velocity::neighbor_cutoff(¶ms);
let cell = recommended_cell_size(cutoff);
let mut scratch = Scratch::with_capacity(N, cell);
let t0 = Instant::now();
for tick in 0..NUM_TICKS {
anticipation_velocity::step_scratch(&mut peds, &walls, ¶ms, DT, &mut scratch);
wrap_and_retarget(&mut peds, LENGTH, WIDTH);
if tick % 200 == 0 {
assert_invariants(&peds, N, KINEMATIC_SPEED_CAP, tick);
}
}
assert_invariants(&peds, N, KINEMATIC_SPEED_CAP, NUM_TICKS);
let elapsed_ms = t0.elapsed().as_millis();
eprintln!(
"[crowd soak AVM 10k/1h] {N} agents x {NUM_TICKS} ticks (3600 s simulated) completed in {elapsed_ms} ms"
);
}
#[test]
#[ignore = "long-running soak test; run with --ignored in the workspace soak lane"]
fn generalized_centrifugal_force_soak_10k_one_hour() {
const N: usize = 10_000;
const LENGTH: f64 = 200.0;
const WIDTH: f64 = 50.0;
const DT: f64 = 0.05;
const NUM_TICKS: usize = 72_000;
let params = generalized_centrifugal_force::Params::default();
params.validate(DT).expect("GCF params must validate");
let walls: Vec<WallSegment> = Vec::new();
let mut peds = seed_counterflow(N, LENGTH, WIDTH, 0x6CF_BEEF);
let cutoff = generalized_centrifugal_force::neighbor_cutoff(¶ms);
let cell = recommended_cell_size(cutoff);
let mut scratch = Scratch::with_capacity(N, cell);
let speed_cap = params.max_speed * 1.5;
let t0 = Instant::now();
for tick in 0..NUM_TICKS {
generalized_centrifugal_force::step_scratch(&mut peds, &walls, ¶ms, DT, &mut scratch);
wrap_and_retarget(&mut peds, LENGTH, WIDTH);
if tick % 200 == 0 {
assert_invariants(&peds, N, speed_cap, tick);
}
}
assert_invariants(&peds, N, speed_cap, NUM_TICKS);
let elapsed_ms = t0.elapsed().as_millis();
eprintln!(
"[crowd soak GCF 10k/1h] {N} agents x {NUM_TICKS} ticks (3600 s simulated) completed in {elapsed_ms} ms"
);
}
#[test]
#[ignore = "long-running soak test; run with --ignored in the workspace soak lane"]
fn optimal_steps_soak_one_hour() {
const N: usize = 5_000;
const LENGTH: f64 = 200.0;
const WIDTH: f64 = 50.0;
const DT: f64 = 0.4;
const NUM_STEPS: usize = 9_000;
let params = optimal_steps::Params::default();
params.validate(DT).expect("OSM params must validate");
let walls: Vec<WallSegment> = Vec::new();
let mut peds = seed_counterflow(N, LENGTH, WIDTH, 0x05_BEEF);
let cutoff = optimal_steps::neighbor_cutoff(¶ms);
let cell = recommended_cell_size(cutoff);
let mut scratch = Scratch::with_capacity(N, cell);
let t0 = Instant::now();
for tick in 0..NUM_STEPS {
optimal_steps::step_scratch(&mut peds, &walls, ¶ms, DT, &mut scratch);
wrap_and_retarget(&mut peds, LENGTH, WIDTH);
if tick % 100 == 0 {
assert_invariants(&peds, N, KINEMATIC_SPEED_CAP, tick);
}
}
assert_invariants(&peds, N, KINEMATIC_SPEED_CAP, NUM_STEPS);
let elapsed_ms = t0.elapsed().as_millis();
eprintln!(
"[crowd soak OSM 5k/1h] {N} agents x {NUM_STEPS} steps (3600 s simulated) completed in {elapsed_ms} ms"
);
}
#[test]
fn layered_soak_mid_scale() {
use rustsim_crowd::threed::{
step_layered_scratch, ConnectorKind, FloorTransition, LayeredScratch, LayeredSpace,
Pedestrian3D,
};
const N: usize = 200;
const HALF_TARGETING_F1: usize = 100;
const DT: f64 = 0.05;
const NUM_TICKS: usize = 600;
let mut space = LayeredSpace::new();
space.set_floor(0, 0.0);
space.set_floor(1, 4.0);
space.connectors.push(FloorTransition {
id: 1,
kind: ConnectorKind::Stair,
from_floor: 0,
from_pos: [10.0, 0.0],
to_floor: 1,
to_pos: [10.0, 0.0],
boarding_radius: 1.0,
travel_time: 10.0,
});
let mut peds: Vec<Pedestrian3D> = (0..N)
.map(|i| {
let y = (i as f64 - N as f64 / 2.0) * 0.4;
let base = Pedestrian::new([0.0, y], [0.0, 0.0], 0.25, 1.34, [10.0, y]);
if i < HALF_TARGETING_F1 {
Pedestrian3D::heading_to_floor(base, 0, 1)
} else {
Pedestrian3D::grounded(base, 0)
}
})
.collect();
let params = social_force::Params::default();
params.validate(DT).expect("SFM params must validate");
let mut scratch = LayeredScratch::with_capacity(N);
let t0 = Instant::now();
for tick in 0..NUM_TICKS {
#[allow(deprecated)]
step_layered_scratch(
&mut peds,
&space,
social_force::step,
¶ms,
DT,
&mut scratch,
);
assert_eq!(peds.len(), N, "layered population drift at tick {tick}");
for (i, p) in peds.iter().enumerate() {
assert!(
p.base.pos[0].is_finite() && p.base.pos[1].is_finite(),
"non-finite planar pos at tick {tick}, agent {i}: {:?}",
p.base.pos
);
assert!(
p.base.vel[0].is_finite() && p.base.vel[1].is_finite(),
"non-finite planar vel at tick {tick}, agent {i}: {:?}",
p.base.vel
);
assert!(
p.floor == 0 || p.floor == 1,
"agent {i} drifted off the layered space (floor = {}) at tick {tick}",
p.floor
);
if let Some(active) = &p.transition {
assert!(
active.remaining.is_finite() && active.remaining >= 0.0,
"agent {i} transition remaining={} at tick {tick}",
active.remaining
);
}
}
}
let elapsed_ms = t0.elapsed().as_millis();
eprintln!(
"[crowd soak layered] {N} agents x {NUM_TICKS} ticks ({:.1} s simulated) completed in {elapsed_ms} ms",
NUM_TICKS as f64 * DT
);
}
#[cfg(feature = "rayon")]
#[test]
fn social_force_rayon_bit_exact_soak() {
const N: usize = 256;
const LENGTH: f64 = 40.0;
const WIDTH: f64 = 10.0;
const DT: f64 = 0.05;
const NUM_TICKS: usize = 3_000;
let params = social_force::Params::default();
params.validate(DT).expect("SFM params must validate");
let walls: Vec<WallSegment> = Vec::new();
let mut peds_serial = seed_counterflow(N, LENGTH, WIDTH, 0x5AFE);
let mut peds_par = peds_serial.clone();
let cutoff = social_force::neighbor_cutoff(¶ms);
let cell = recommended_cell_size(cutoff);
let mut scratch_serial = Scratch::with_capacity(N, cell);
let mut scratch_par = Scratch::with_capacity(N, cell);
let speed_cap = params.max_speed * 1.5;
let t0 = Instant::now();
for tick in 0..NUM_TICKS {
social_force::step_scratch(&mut peds_serial, &walls, ¶ms, DT, &mut scratch_serial);
social_force::step_scratch_par(&mut peds_par, &walls, ¶ms, DT, &mut scratch_par);
wrap_and_retarget(&mut peds_serial, LENGTH, WIDTH);
wrap_and_retarget(&mut peds_par, LENGTH, WIDTH);
for (i, (a, b)) in peds_serial.iter().zip(peds_par.iter()).enumerate() {
assert_eq!(
a.pos, b.pos,
"rayon vs serial pos drift at tick {tick}, agent {i}: serial={:?} par={:?}",
a.pos, b.pos
);
assert_eq!(
a.vel, b.vel,
"rayon vs serial vel drift at tick {tick}, agent {i}: serial={:?} par={:?}",
a.vel, b.vel
);
}
if tick % 100 == 0 {
assert_invariants(&peds_par, N, speed_cap, tick);
}
}
assert_invariants(&peds_par, N, speed_cap, NUM_TICKS);
let elapsed_ms = t0.elapsed().as_millis();
eprintln!(
"[crowd soak SFM rayon] {N} agents x {NUM_TICKS} ticks ({:.1} s simulated, bit-exact vs serial) completed in {elapsed_ms} ms",
NUM_TICKS as f64 * DT
);
}
#[test]
fn step_scratch_store_soak_mid_scale() {
use rustsim_core::prelude::VecStore;
use rustsim_core::store::AgentStore;
use rustsim_crowd::prelude::{step_scratch_store, CrowdAgent, SocialForceModel};
const N: usize = 1_000;
const LENGTH: f64 = 60.0;
const WIDTH: f64 = 15.0;
const DT: f64 = 0.05;
const NUM_TICKS: usize = 600;
let params = social_force::Params::default();
params.validate(DT).expect("SFM params must validate");
let seed_peds = seed_counterflow(N, LENGTH, WIDTH, 0x0570_BEEF);
let mut store: VecStore<CrowdAgent> = VecStore::new();
for (i, p) in seed_peds.iter().enumerate() {
store.insert(CrowdAgent {
id: i as u64 + 1,
ped: *p,
});
}
let walls: Vec<WallSegment> = Vec::new();
let cutoff = social_force::neighbor_cutoff(¶ms);
let cell = recommended_cell_size(cutoff);
let mut scratch = Scratch::with_capacity(N, cell);
let mut buf: Vec<Pedestrian> = Vec::with_capacity(N);
let model = SocialForceModel;
let speed_cap = params.max_speed * 1.5;
let t0 = Instant::now();
for tick in 0..NUM_TICKS {
step_scratch_store(
&model,
&mut store,
&walls,
¶ms,
DT,
&mut scratch,
&mut buf,
);
buf.clear();
for &id in &store.iter_ids() {
buf.push(store.get(id).expect("id present").ped);
}
wrap_and_retarget(&mut buf, LENGTH, WIDTH);
for (i, &id) in store.iter_ids().iter().enumerate() {
let mut entry = store.get_mut(id).expect("id present");
entry.ped = buf[i];
}
if tick % 50 == 0 {
let ids = store.iter_ids();
assert_eq!(
ids.len(),
N,
"store population drift at tick {tick}: {} != {N}",
ids.len()
);
for &id in &ids {
let p = store.get(id).expect("id present").ped;
assert!(
p.pos[0].is_finite() && p.pos[1].is_finite(),
"non-finite store pos at tick {tick}, id {id}: {:?}",
p.pos
);
assert!(
p.vel[0].is_finite() && p.vel[1].is_finite(),
"non-finite store vel at tick {tick}, id {id}: {:?}",
p.vel
);
let speed = (p.vel[0] * p.vel[0] + p.vel[1] * p.vel[1]).sqrt();
assert!(
speed <= speed_cap,
"store speed {speed} exceeds cap {speed_cap} at tick {tick}, id {id}"
);
}
}
}
let elapsed_ms = t0.elapsed().as_millis();
eprintln!(
"[crowd soak store] {N} agents x {NUM_TICKS} ticks ({:.1} s simulated, integrated drive) completed in {elapsed_ms} ms",
NUM_TICKS as f64 * DT
);
}
#[test]
fn crowd_observer_soak_mid_scale() {
use rustsim_core::prelude::VecStore;
use rustsim_core::store::AgentStore;
use rustsim_crowd::prelude::{step_scratch_store_observed, CrowdAgent, SocialForceModel};
const N: usize = 500;
const LENGTH: f64 = 40.0;
const WIDTH: f64 = 10.0;
const DT: f64 = 0.05;
const NUM_TICKS: usize = 600;
let params = social_force::Params::default();
params.validate(DT).expect("SFM params must validate");
let seed_peds = seed_counterflow(N, LENGTH, WIDTH, 0x0B55_0AC0);
let mut store: VecStore<CrowdAgent> = VecStore::new();
for (i, p) in seed_peds.iter().enumerate() {
store.insert(CrowdAgent {
id: i as u64 + 1,
ped: *p,
});
}
let walls: Vec<WallSegment> = Vec::new();
let cutoff = social_force::neighbor_cutoff(¶ms);
let cell = recommended_cell_size(cutoff);
let mut scratch = Scratch::with_capacity(N, cell);
let mut buf: Vec<Pedestrian> = Vec::with_capacity(N);
let model = SocialForceModel;
let mut observer_calls: u64 = 0;
let mut last_id_seen: u64 = 0;
let mut max_finite_pos: f64 = 0.0;
let t0 = Instant::now();
for _tick in 0..NUM_TICKS {
step_scratch_store_observed(
&model,
&mut store,
&walls,
¶ms,
DT,
&mut scratch,
&mut buf,
&mut |id, ped: &Pedestrian| {
observer_calls += 1;
last_id_seen = id;
debug_assert!(
ped.pos[0].is_finite() && ped.pos[1].is_finite(),
"observer received non-finite pos for id {id}"
);
let mag = ped.pos[0].abs().max(ped.pos[1].abs());
if mag.is_finite() && mag > max_finite_pos {
max_finite_pos = mag;
}
},
);
}
let expected = (N as u64) * (NUM_TICKS as u64);
assert_eq!(
observer_calls, expected,
"observer call count {observer_calls} != expected {expected} (N={N}, ticks={NUM_TICKS})"
);
assert!(
last_id_seen >= 1 && last_id_seen <= N as u64,
"observer last id {last_id_seen} out of range [1, {N}]"
);
let elapsed_ms = t0.elapsed().as_millis();
eprintln!(
"[crowd soak observer] {N} agents x {NUM_TICKS} ticks: {observer_calls} hook calls, max |pos|={max_finite_pos:.2}, completed in {elapsed_ms} ms"
);
}
#[test]
fn layered_observer_soak_mid_scale() {
use rustsim_crowd::threed::{
step_layered_scratch_observed, ConnectorKind, FloorTransition, LayeredScratch,
LayeredSpace, Pedestrian3D,
};
const N: usize = 200;
const HALF_TARGETING_F1: usize = 100;
const DT: f64 = 0.05;
const NUM_TICKS: usize = 600;
let mut space = LayeredSpace::new();
space.set_floor(0, 0.0);
space.set_floor(1, 4.0);
space.connectors.push(FloorTransition {
id: 1,
kind: ConnectorKind::Stair,
from_floor: 0,
from_pos: [10.0, 0.0],
to_floor: 1,
to_pos: [10.0, 0.0],
boarding_radius: 1.0,
travel_time: 10.0,
});
let mut peds: Vec<Pedestrian3D> = (0..N)
.map(|i| {
let y = (i as f64 - N as f64 / 2.0) * 0.4;
let base = Pedestrian::new([0.0, y], [0.0, 0.0], 0.25, 1.34, [10.0, y]);
if i < HALF_TARGETING_F1 {
Pedestrian3D::heading_to_floor(base, 0, 1)
} else {
Pedestrian3D::grounded(base, 0)
}
})
.collect();
let params = social_force::Params::default();
params.validate(DT).expect("SFM params must validate");
let mut scratch = LayeredScratch::with_capacity(N);
let mut total_calls: u64 = 0;
let mut tick_indices: Vec<usize> = Vec::with_capacity(N);
let mut nonfinite_observed: u64 = 0;
let mut bad_floor_observed: u64 = 0;
let t0 = Instant::now();
for tick in 0..NUM_TICKS {
tick_indices.clear();
#[allow(deprecated)]
step_layered_scratch_observed(
&mut peds,
&space,
social_force::step,
¶ms,
DT,
&mut scratch,
&mut |idx: usize, p: &Pedestrian3D| {
total_calls += 1;
tick_indices.push(idx);
if !(p.base.pos[0].is_finite()
&& p.base.pos[1].is_finite()
&& p.base.vel[0].is_finite()
&& p.base.vel[1].is_finite())
{
nonfinite_observed += 1;
}
if p.floor != 0 && p.floor != 1 {
bad_floor_observed += 1;
}
},
);
assert_eq!(
tick_indices.len(),
N,
"tick {tick}: observed {} agents, expected {N}",
tick_indices.len()
);
for (expected, &got) in tick_indices.iter().enumerate() {
assert_eq!(
got, expected,
"tick {tick}: observation index {got} at slot {expected} broke 0..N stable order"
);
}
}
let expected = (N as u64) * (NUM_TICKS as u64);
assert_eq!(
total_calls, expected,
"layered observer call count {total_calls} != expected {expected}"
);
assert_eq!(
nonfinite_observed, 0,
"layered observer received {nonfinite_observed} non-finite pos/vel rows"
);
assert_eq!(
bad_floor_observed, 0,
"layered observer received {bad_floor_observed} rows with floor ∉ {{0, 1}}"
);
let elapsed_ms = t0.elapsed().as_millis();
eprintln!(
"[crowd soak layered-observer] {N} agents x {NUM_TICKS} ticks: {total_calls} hook calls, completed in {elapsed_ms} ms"
);
}