rustsim-crowd 0.0.1

Microscopic crowd and pedestrian locomotion for rustsim: 2-D and layered 3-D, with Social Force, Collision-Free Speed, Generalized Centrifugal Force, Optimal Steps, and Anticipation Velocity models
Documentation
//! Bit-exact equivalence between `step_scratch` and
//! `step_scratch_par` across all five microscopic models.
//!
//! Closes the multi-core half of the P1-5 "vectorised inner loops"
//! item from `docs/rustsim-crowd.md` for the **full model suite**:
//! after the SFM rayon path landed, GCF / CFS / AVM / OSM each gained
//! their own `step_scratch_par`. This integration test pins the
//! parallel contract uniformly: each worker writes only to its own
//! per-agent scratch slot from an immutable `&[Pedestrian]` view, no
//! cross-thread float reduction is performed, the position/velocity
//! writeback is a single serial pass, and therefore the parallel
//! trajectory must equal the serial one bit-for-bit.
//!
//! Each test runs 64 agents × 40 ticks against a shared deterministic
//! fixture (golden-ratio hash seeding, identical to the in-tree
//! `social_force::tests::step_scratch_par_matches_step_scratch_bit_exact`
//! pattern) and asserts `assert_eq!` on every `[f64; 2]` position and
//! velocity. Any future change that introduces a non-deterministic
//! reduction order, a `parallel_for_each_neighbor`, or a workspace
//! float-mode flag would fail this gate immediately.
//!
//! Only compiled when the `rayon` feature is enabled. CI runs both
//! the default build (no rayon, no test executed) and the
//! `--features rayon` build (test enforces the contract).

#![cfg(feature = "rayon")]

use rustsim_crowd::broadphase::Scratch;
use rustsim_crowd::common::{Pedestrian, WallSegment};

fn fixture(n: usize) -> (Vec<Pedestrian>, Vec<WallSegment>) {
    let mut peds = Vec::with_capacity(n);
    for k in 0..n as u64 {
        let x = ((k.wrapping_mul(2_654_435_761)) % 6_000_000) as f64 / 1_000_000.0;
        let y = ((k.wrapping_mul(40_503)) % 6_000_000) as f64 / 1_000_000.0;
        peds.push(Pedestrian::new([x, y], [0.0, 0.0], 0.25, 1.2, [x + 5.0, y]));
    }
    // One bordering wall to exercise the wall-force path on every
    // model that consumes one (SFM, GCF, CFS, AVM). OSM ignores the
    // wall list when reading utility but iterates it; presence is
    // harmless.
    let walls = vec![WallSegment {
        a: [-1.0, -1.0],
        b: [20.0, -1.0],
    }];
    (peds, walls)
}

fn assert_trajectories_equal(label: &str, a: &[Pedestrian], b: &[Pedestrian]) {
    assert_eq!(a.len(), b.len(), "{label}: length mismatch");
    for (i, (pa, pb)) in a.iter().zip(b.iter()).enumerate() {
        assert_eq!(pa.pos, pb.pos, "{label}: pos diverged at agent {i}");
        assert_eq!(pa.vel, pb.vel, "{label}: vel diverged at agent {i}");
    }
}

#[test]
fn gcf_step_scratch_par_matches_step_scratch_bit_exact() {
    use rustsim_crowd::generalized_centrifugal_force::{
        neighbor_cutoff, step_scratch, step_scratch_par, Params,
    };

    let (mut a, walls) = fixture(64);
    let mut b = a.clone();
    let params = Params::default();
    let cutoff = neighbor_cutoff(&params);
    let mut scratch_a = Scratch::with_capacity(a.len(), cutoff);
    let mut scratch_b = Scratch::with_capacity(b.len(), cutoff);

    for _ in 0..40 {
        step_scratch(&mut a, &walls, &params, 0.05, &mut scratch_a);
        step_scratch_par(&mut b, &walls, &params, 0.05, &mut scratch_b);
    }
    assert_trajectories_equal("gcf", &a, &b);
}

#[test]
fn cfs_step_scratch_par_matches_step_scratch_bit_exact() {
    use rustsim_crowd::collision_free_speed::{
        neighbor_cutoff, step_scratch, step_scratch_par, Params,
    };

    let (mut a, walls) = fixture(64);
    let mut b = a.clone();
    let params = Params::default();
    let cutoff = neighbor_cutoff(&params);
    let mut scratch_a = Scratch::with_capacity(a.len(), cutoff);
    let mut scratch_b = Scratch::with_capacity(b.len(), cutoff);

    for _ in 0..40 {
        step_scratch(&mut a, &walls, &params, 0.05, &mut scratch_a);
        step_scratch_par(&mut b, &walls, &params, 0.05, &mut scratch_b);
    }
    assert_trajectories_equal("cfs", &a, &b);
}

#[test]
fn avm_step_scratch_par_matches_step_scratch_bit_exact() {
    use rustsim_crowd::anticipation_velocity::{
        neighbor_cutoff, step_scratch, step_scratch_par, Params,
    };

    let (mut a, walls) = fixture(64);
    let mut b = a.clone();
    let params = Params::default();
    let cutoff = neighbor_cutoff(&params);
    let mut scratch_a = Scratch::with_capacity(a.len(), cutoff);
    let mut scratch_b = Scratch::with_capacity(b.len(), cutoff);

    for _ in 0..40 {
        step_scratch(&mut a, &walls, &params, 0.05, &mut scratch_a);
        step_scratch_par(&mut b, &walls, &params, 0.05, &mut scratch_b);
    }
    assert_trajectories_equal("avm", &a, &b);
}

#[test]
fn osm_step_scratch_par_matches_step_scratch_bit_exact() {
    use rustsim_crowd::optimal_steps::{neighbor_cutoff, step_scratch, step_scratch_par, Params};

    let (mut a, walls) = fixture(64);
    let mut b = a.clone();
    let params = Params::default();
    let cutoff = neighbor_cutoff(&params);
    let mut scratch_a = Scratch::with_capacity(a.len(), cutoff);
    let mut scratch_b = Scratch::with_capacity(b.len(), cutoff);

    for _ in 0..40 {
        step_scratch(&mut a, &walls, &params, 0.05, &mut scratch_a);
        step_scratch_par(&mut b, &walls, &params, 0.05, &mut scratch_b);
    }
    assert_trajectories_equal("osm", &a, &b);
}

/// `step_scratch_store_par` (and the `_observed` companion) must
/// drive the rayon-parallel kernel while preserving every other
/// contract of `step_scratch_store`: same `iter_ids()` extract /
/// write-back ordering, same observation order, and bit-exact
/// trajectories vs the serial integrated path.
///
/// This is the integration-level guarantee that lets a production
/// consumer flip the `rustsim-core`-driven path between serial and
/// parallel without touching telemetry, store layout, or per-tick
/// state. The kernel-level invariant is already pinned by the
/// per-model bit-exact tests above; this test pins the whole
/// `AgentStore<CrowdAgent>` round-trip.
#[test]
fn step_scratch_store_par_matches_step_scratch_store_bit_exact() {
    use rustsim_core::prelude::VecStore;
    use rustsim_core::store::AgentStore;
    use rustsim_crowd::common::Pedestrian;
    use rustsim_crowd::prelude::{
        recommended_cell_size, step_scratch_store, step_scratch_store_observed,
        step_scratch_store_observed_par, step_scratch_store_par, CrowdAgent, Scratch,
        SocialForceModel,
    };
    use rustsim_crowd::social_force::{neighbor_cutoff, Params};

    let (peds, walls) = fixture(64);
    let mut store_a: VecStore<CrowdAgent> = VecStore::new();
    let mut store_b: VecStore<CrowdAgent> = VecStore::new();
    for (i, p) in peds.iter().enumerate() {
        store_a.insert(CrowdAgent {
            id: i as u64 + 1,
            ped: *p,
        });
        store_b.insert(CrowdAgent {
            id: i as u64 + 1,
            ped: *p,
        });
    }

    let params = Params::default();
    let cutoff = neighbor_cutoff(&params);
    let cell = recommended_cell_size(cutoff);
    let mut scratch_a = Scratch::with_capacity(peds.len(), cell);
    let mut scratch_b = Scratch::with_capacity(peds.len(), cell);
    let mut buf_a: Vec<Pedestrian> = Vec::with_capacity(peds.len());
    let mut buf_b: Vec<Pedestrian> = Vec::with_capacity(peds.len());
    let model = SocialForceModel;

    // Drive 40 ticks through the two integrated paths.
    for _ in 0..40 {
        step_scratch_store(
            &model,
            &mut store_a,
            &walls,
            &params,
            0.05,
            &mut scratch_a,
            &mut buf_a,
        );
        step_scratch_store_par(
            &model,
            &mut store_b,
            &walls,
            &params,
            0.05,
            &mut scratch_b,
            &mut buf_b,
        );
    }

    // Compare every store row (sorted by id, which is insertion
    // order in VecStore) bit-for-bit.
    let ids_a = store_a.iter_ids();
    let ids_b = store_b.iter_ids();
    assert_eq!(ids_a, ids_b, "id sets diverged");
    for &id in &ids_a {
        let a = store_a.get(id).expect("a missing").ped;
        let b = store_b.get(id).expect("b missing").ped;
        assert_eq!(a.pos, b.pos, "store_par diverged in pos at id {id}");
        assert_eq!(a.vel, b.vel, "store_par diverged in vel at id {id}");
    }

    // Now drive both observed entry points and pin that they hand
    // the observer the same authoritative post-tick rows in the
    // same order.
    let mut store_c: VecStore<CrowdAgent> = VecStore::new();
    let mut store_d: VecStore<CrowdAgent> = VecStore::new();
    for (i, p) in peds.iter().enumerate() {
        store_c.insert(CrowdAgent {
            id: i as u64 + 1,
            ped: *p,
        });
        store_d.insert(CrowdAgent {
            id: i as u64 + 1,
            ped: *p,
        });
    }
    let mut scratch_c = Scratch::with_capacity(peds.len(), cell);
    let mut scratch_d = Scratch::with_capacity(peds.len(), cell);
    let mut buf_c: Vec<Pedestrian> = Vec::with_capacity(peds.len());
    let mut buf_d: Vec<Pedestrian> = Vec::with_capacity(peds.len());
    let mut log_serial: Vec<(u64, [f64; 2])> = Vec::new();
    let mut log_par: Vec<(u64, [f64; 2])> = Vec::new();

    for _ in 0..5 {
        step_scratch_store_observed(
            &model,
            &mut store_c,
            &walls,
            &params,
            0.05,
            &mut scratch_c,
            &mut buf_c,
            &mut |id, ped: &Pedestrian| log_serial.push((id, ped.pos)),
        );
        step_scratch_store_observed_par(
            &model,
            &mut store_d,
            &walls,
            &params,
            0.05,
            &mut scratch_d,
            &mut buf_d,
            &mut |id, ped: &Pedestrian| log_par.push((id, ped.pos)),
        );
    }
    assert_eq!(
        log_serial, log_par,
        "observer streams diverged between serial and parallel integrated paths"
    );
}