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
//! Cross-model parameter validation tests.

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

const DT: f64 = 0.05;

#[test]
fn all_defaults_validate_at_nominal_dt() {
    social_force::Params::default().validate(DT).unwrap();
    collision_free_speed::Params::default()
        .validate(DT)
        .unwrap();
    generalized_centrifugal_force::Params::default()
        .validate(DT)
        .unwrap();
    anticipation_velocity::Params::default()
        .validate(DT)
        .unwrap();
    optimal_steps::Params::default().validate(DT).unwrap();
}

#[test]
fn rejects_non_finite_dt() {
    let p = social_force::Params::default();
    assert!(matches!(
        p.validate(f64::NAN),
        Err(CrowdError::InvalidDt { .. })
    ));
    assert!(matches!(
        p.validate(f64::INFINITY),
        Err(CrowdError::InvalidDt { .. })
    ));
    assert!(matches!(
        p.validate(-0.01),
        Err(CrowdError::InvalidDt { .. })
    ));
    assert!(matches!(p.validate(0.0), Err(CrowdError::InvalidDt { .. })));
}

#[test]
fn rejects_non_positive_mass_in_sfm() {
    let p = social_force::Params {
        mass: 0.0,
        ..social_force::Params::default()
    };
    assert!(matches!(
        p.validate(DT),
        Err(CrowdError::NonPositiveParam { param: "mass", .. })
    ));
}

#[test]
fn rejects_negative_arrival_radius() {
    let p = social_force::Params {
        arrival_radius: -0.5,
        ..social_force::Params::default()
    };
    assert!(matches!(
        p.validate(DT),
        Err(CrowdError::NegativeParam {
            param: "arrival_radius",
            ..
        })
    ));
}

#[test]
fn flags_cfl_violation_when_dt_too_large() {
    let p = social_force::Params::default();
    // dt * 20 m/s² = 2.6 m/s > max_speed = 2.5 m/s → should fail.
    let err = p.validate(0.13).unwrap_err();
    match err {
        CrowdError::CflViolation {
            product, max_dt, ..
        } => {
            assert!(product > 2.5);
            assert!((max_dt - 0.125).abs() < 1e-9);
        }
        other => panic!("expected CflViolation, got {other:?}"),
    }
}

#[test]
fn cfl_violation_also_applies_to_gcf() {
    let p = generalized_centrifugal_force::Params::default();
    let err = p.validate(0.13).unwrap_err();
    assert!(matches!(err, CrowdError::CflViolation { .. }));
}

#[test]
fn osm_rejects_zero_num_candidates() {
    let p = optimal_steps::Params {
        num_candidates: 0,
        ..optimal_steps::Params::default()
    };
    assert!(matches!(
        p.validate(DT),
        Err(CrowdError::ZeroCount {
            param: "num_candidates",
            ..
        })
    ));
}

#[test]
fn avm_rejects_zero_num_directions() {
    let p = anticipation_velocity::Params {
        num_directions: 0,
        ..anticipation_velocity::Params::default()
    };
    assert!(matches!(
        p.validate(DT),
        Err(CrowdError::ZeroCount {
            param: "num_directions",
            ..
        })
    ));
}

#[test]
fn error_message_mentions_model_name() {
    let p = social_force::Params {
        mass: 0.0,
        ..social_force::Params::default()
    };
    let msg = p.validate(DT).unwrap_err().to_string();
    assert!(msg.contains("SocialForce"), "msg = {msg}");
    assert!(msg.contains("mass"), "msg = {msg}");
}