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
//! Long-run stability + scale soak test for the CUDA SFM hot path.
//!
//! Companion to `tests/soak_scale.rs` (the CPU soak lane) and the
//! empirical pin behind the "scales to millions of agents" claim for
//! [`rustsim_crowd::cuda::social_force::CudaResident::step_grid`].
//! Feature-gated on `cuda`; on hosts without a working driver the
//! tests skip cleanly (stderr log + early return) rather than fail,
//! so they are safe to include unconditionally in CI.
//!
//! Two tests share one seeding helper:
//!
//! - `cuda_grid_soak_mid_scale` (**unconditional**) — 10 000 agents ×
//!   300 ticks = 15 s simulated at dt = 0.05 s, uniform grid, random
//!   counter-flow. Catches any fast NaN propagation, cell-indexing
//!   overflow, or population loss. Small enough to run in the default
//!   soak CI lane.
//! - `cuda_grid_soak_one_million_short` (**`#[ignore]`**) — 1 000 000
//!   agents × 10 ticks. Pins the million-agent throughput claim on
//!   real hardware. Runs opt-in via
//!   `cargo test -p rustsim-crowd --features cuda --test cuda_soak_scale \
//!       --release -- --ignored --nocapture`.
//!
//! Invariants asserted on every tick:
//! - population is conserved (`peds.len()` unchanged);
//! - no `pos` / `vel` component is NaN or Inf;
//! - per-agent speed stays bounded by `max_speed * 1.5` (the 1.5×
//!   margin catches a single-tick clamp-slip without masking a real
//!   runaway).

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

use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use rustsim_crowd::common::{Pedestrian, WallSegment};
use rustsim_crowd::cuda::social_force as sfm_cuda;
use rustsim_crowd::social_force;

/// Seed `n` pedestrians in a large rectangular corridor with
/// deterministic counter-flow intent (half walking +x, half -x).
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 = if dir > 0.0 { length + 1000.0 } else { -1000.0 };
            Pedestrian::new([x, y], [dir * 1.0, 0.0], 0.25, 1.34, [dest_x, y])
        })
        .collect()
}

/// Shared soak harness: upload, enable_grid, step_grid for `ticks`
/// iterations, download, and assert every invariant at the end.
fn run_soak(n: usize, length: f64, width: f64, ticks: usize, seed: u64) {
    let peds_initial = seed_counterflow(n, length, width, seed);
    // Two long walls bounding the corridor. Keeping them sparse means
    // wall forces don't dominate and any failure mode is attributable
    // to pair iteration / grid construction.
    let walls = vec![
        WallSegment {
            a: [0.0, -0.5],
            b: [length, -0.5],
        },
        WallSegment {
            a: [0.0, width + 0.5],
            b: [length, width + 0.5],
        },
    ];
    let params = social_force::Params::default();
    let dt = 0.05;

    let mut resident = match sfm_cuda::CudaResident::upload(&peds_initial, &walls) {
        Ok(r) => r,
        Err(e) => {
            eprintln!("skipping cuda soak test (n={n}, ticks={ticks}): no CUDA device ({e})");
            return;
        }
    };

    let cutoff = social_force::neighbor_cutoff(&params);
    // Pad the grid bounds generously so the counter-flow can drift
    // without clipping into the boundary cells over `ticks * dt` s.
    let drift = params.max_speed * dt * ticks as f64 + 10.0;
    let origin = [-drift, -(width * 0.5 + 5.0)];
    let extent_x = length + 2.0 * drift;
    let extent_y = width + 2.0 * (width * 0.5 + 5.0);
    let cfg = sfm_cuda::GridConfig {
        origin,
        cell_size: cutoff,
        dims: (
            (extent_x / cutoff).ceil() as u32 + 1,
            (extent_y / cutoff).ceil() as u32 + 1,
        ),
        cutoff_sq: cutoff * cutoff,
    };
    resident
        .enable_grid(cfg)
        .expect("enable_grid failed on soak scene");

    for t in 0..ticks {
        resident
            .step_grid(&params, dt)
            .unwrap_or_else(|e| panic!("step_grid failed at tick {t}: {e}"));
    }

    let mut peds: Vec<Pedestrian> = Vec::with_capacity(n);
    resident.download(&mut peds).expect("download failed");

    // Invariants.
    assert_eq!(peds.len(), n, "population drifted during soak");
    let speed_ceiling = params.max_speed * 1.5;
    for (i, p) in peds.iter().enumerate() {
        assert!(
            p.pos[0].is_finite() && p.pos[1].is_finite(),
            "agent {i} position not finite after soak: {:?}",
            p.pos
        );
        assert!(
            p.vel[0].is_finite() && p.vel[1].is_finite(),
            "agent {i} velocity not finite after soak: {:?}",
            p.vel
        );
        let speed = (p.vel[0] * p.vel[0] + p.vel[1] * p.vel[1]).sqrt();
        assert!(
            speed <= speed_ceiling,
            "agent {i} speed {speed:.3} m/s exceeds max_speed*1.5={speed_ceiling:.3}"
        );
    }
}

#[test]
fn cuda_grid_soak_mid_scale() {
    // 10 000 agents × 300 ticks = 15 s simulated. Sized to fit the
    // default soak CI lane.
    run_soak(10_000, 200.0, 40.0, 300, 0xC0FFEE);
}

#[test]
#[ignore]
fn cuda_grid_soak_one_million_short() {
    // 1 000 000 agents × 10 ticks. Short-run million-agent pin:
    // long enough to surface NaN propagation and cell-indexing
    // overflow, short enough to return in a few minutes on a
    // workstation GPU. Use `--release --ignored` to run.
    run_soak(1_000_000, 2000.0, 400.0, 10, 0xBADC0DE);
}