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
//! Parameter and runtime errors for [`rustsim-crowd`](crate).
//!
//! The primary use site is [`CrowdError`] returned by each model's
//! `Params::validate(dt)` method. Validating once at the start of a
//! simulation catches the three classes of misconfiguration that
//! otherwise surface as silent physics corruption:
//!
//! 1. **Non-positive physical parameters** (mass ≤ 0, range ≤ 0, etc.):
//!    division by zero or negative relaxation, producing NaN
//!    velocities within a few ticks.
//! 2. **CFL-like instability** (`dt * max_accel > max_speed`): a single
//!    explicit-Euler tick can exceed the speed cap in one integration
//!    step, which is survivable thanks to `clamp_speed` but is a
//!    smell suggesting either a too-coarse `dt` or a too-stiff
//!    interaction.
//! 3. **Non-finite `dt`**: forwarded straight into the integrator.
//!
//! All validators are cheap (a handful of comparisons) and deterministic.

use thiserror::Error;

/// Errors surfaced by [`rustsim-crowd`](crate) parameter validation.
///
/// This enum is deliberately flat (no nested error types, no `Box<dyn
/// Error>`) so callers can match on it exhaustively. Every variant
/// carries the model name as its first field so a downstream error
/// message tells the operator which model's `Params` is misconfigured
/// without having to walk a stack trace.
#[derive(Debug, Clone, PartialEq, Error)]
pub enum CrowdError {
    /// A strictly-positive parameter was given a zero or negative value.
    #[error(
        "{model}: parameter `{param}` must be > 0, got {value} (zero/negative values \
         produce NaN velocities within a few ticks)"
    )]
    NonPositiveParam {
        /// Which model's `Params` failed validation (e.g. `"SocialForce"`).
        model: &'static str,
        /// Name of the offending parameter (e.g. `"mass"`, `"tau"`).
        param: &'static str,
        /// The actual value that was supplied.
        value: f64,
    },

    /// A non-negative parameter was given a negative value.
    #[error("{model}: parameter `{param}` must be >= 0, got {value}")]
    NegativeParam {
        /// Which model's `Params` failed validation.
        model: &'static str,
        /// Name of the offending parameter.
        param: &'static str,
        /// The actual value that was supplied.
        value: f64,
    },

    /// A count-like parameter (number of candidates, number of directions)
    /// was given a zero value when it must be ≥ 1.
    #[error("{model}: parameter `{param}` must be >= 1, got 0")]
    ZeroCount {
        /// Which model's `Params` failed validation.
        model: &'static str,
        /// Name of the offending parameter.
        param: &'static str,
    },

    /// Explicit-Euler CFL-like condition violated.
    ///
    /// For force-based models (Social Force, Generalized Centrifugal
    /// Force) the per-tick velocity change is bounded by
    /// `dt * max_accel`. If that bound exceeds `max_speed` the model
    /// relies entirely on the post-integration `clamp_speed` call to
    /// keep trajectories physical, which masks stiff interactions and
    /// hides numerical blow-ups.
    #[error(
        "{model}: CFL violation — dt * max_accel = {product} m/s exceeds \
         max_speed = {max_speed} m/s; reduce `dt` to at most {max_dt} s or \
         raise `max_speed`"
    )]
    CflViolation {
        /// Which model's `Params` failed validation.
        model: &'static str,
        /// `dt * max_accel` for the offending configuration (m/s).
        product: f64,
        /// `max_speed` for the offending configuration (m/s).
        max_speed: f64,
        /// The largest `dt` that satisfies `dt * max_accel <= max_speed`.
        max_dt: f64,
    },

    /// `dt` is NaN, infinite, or non-positive.
    #[error(
        "{model}: dt must be a finite positive number, got {dt}; every \
         step_* entry point assumes a real-time tick duration"
    )]
    InvalidDt {
        /// Which model's `Params` was being validated.
        model: &'static str,
        /// The actual `dt` that was supplied.
        dt: f64,
    },
}

/// Internal helper: assert a parameter is `> 0` or return a
/// [`CrowdError::NonPositiveParam`].
#[inline]
pub(crate) fn require_positive(
    model: &'static str,
    param: &'static str,
    value: f64,
) -> Result<(), CrowdError> {
    if value.is_finite() && value > 0.0 {
        Ok(())
    } else {
        Err(CrowdError::NonPositiveParam {
            model,
            param,
            value,
        })
    }
}

/// Internal helper: assert a parameter is `>= 0` or return a
/// [`CrowdError::NegativeParam`].
#[inline]
pub(crate) fn require_nonneg(
    model: &'static str,
    param: &'static str,
    value: f64,
) -> Result<(), CrowdError> {
    if value.is_finite() && value >= 0.0 {
        Ok(())
    } else {
        Err(CrowdError::NegativeParam {
            model,
            param,
            value,
        })
    }
}

/// Internal helper: assert a count is `>= 1`.
#[inline]
pub(crate) fn require_count(
    model: &'static str,
    param: &'static str,
    value: usize,
) -> Result<(), CrowdError> {
    if value >= 1 {
        Ok(())
    } else {
        Err(CrowdError::ZeroCount { model, param })
    }
}

/// Internal helper: assert `dt` is a finite positive number.
#[inline]
pub(crate) fn require_dt(model: &'static str, dt: f64) -> Result<(), CrowdError> {
    if dt.is_finite() && dt > 0.0 {
        Ok(())
    } else {
        Err(CrowdError::InvalidDt { model, dt })
    }
}