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
//! Pins the **model-specific CFL discipline** advertised in
//! `docs/rustsim-crowd.md` P2-10:
//!
//! - SFM and GCF are second-order force-based integrators that advance
//!   velocity and position with explicit/symplectic Euler. Their
//!   `Params::validate(dt)` *must* reject parameter sets where
//!   `dt * max_accel > max_speed` with [`CrowdError::CflViolation`] and
//!   *must* accept parameter sets where the bound holds.
//! - CFS, AVM, and OSM are first-order kinematic models with no
//!   explicit-Euler instability mode (speeds / positions are clamped
//!   directly from collision-free headroom or chosen utility). Their
//!   `validate(dt)` *must not* gain a CFL check that would gate
//!   downstream callers on `max_accel` / `max_speed` they don't even
//!   carry as fields.
//!
//! A future refactor that swaps any of these contracts should fail
//! loudly, not silently.

use rustsim_crowd::error::CrowdError;
use rustsim_crowd::{
    anticipation_velocity, collision_free_speed, generalized_centrifugal_force, optimal_steps,
    social_force,
};

#[test]
fn sfm_validate_enforces_cfl_bound() {
    // Drive the bound to fail by construction: max_accel * dt = 5.0 > max_speed.
    let params = social_force::Params {
        max_accel: 100.0,
        max_speed: 1.34,
        ..social_force::Params::default()
    };
    let dt = 0.05;
    match params.validate(dt) {
        Err(CrowdError::CflViolation {
            model,
            product,
            max_speed,
            ..
        }) => {
            assert_eq!(model, "SocialForce");
            assert!((product - dt * 100.0).abs() < 1e-12);
            assert_eq!(max_speed, 1.34);
        }
        other => panic!("expected CflViolation, got {other:?}"),
    }

    // And the inverse: defaults satisfy the bound.
    social_force::Params::default()
        .validate(0.05)
        .expect("default SFM params at dt=0.05 must validate");
}

#[test]
fn gcf_validate_enforces_cfl_bound() {
    let params = generalized_centrifugal_force::Params {
        max_accel: 100.0,
        max_speed: 1.34,
        ..generalized_centrifugal_force::Params::default()
    };
    let dt = 0.05;
    match params.validate(dt) {
        Err(CrowdError::CflViolation { model, .. }) => {
            assert_eq!(model, "GeneralizedCentrifugalForce");
        }
        other => panic!("expected CflViolation, got {other:?}"),
    }

    generalized_centrifugal_force::Params::default()
        .validate(0.05)
        .expect("default GCF params at dt=0.05 must validate");
}

#[test]
fn cfs_validate_skips_cfl_even_for_pathological_dt() {
    // CFS is first-order kinematic; the largest sane `dt` we expose
    // (the workspace's max-tick-window 1.0 s) must still validate.
    collision_free_speed::Params::default()
        .validate(1.0)
        .expect("CFS first-order kinematic: no CFL gate must trip");
}

#[test]
fn avm_validate_skips_cfl_even_for_pathological_dt() {
    anticipation_velocity::Params::default()
        .validate(1.0)
        .expect("AVM first-order kinematic: no CFL gate must trip");
}

#[test]
fn osm_validate_skips_cfl_even_for_pathological_dt() {
    optimal_steps::Params::default()
        .validate(1.0)
        .expect("OSM discrete-step kinematic: no CFL gate must trip");
}