rustsim-traffic 0.0.1

Transport-domain semantics for rustsim: multimodal movement, controls, and routing metadata
Documentation
//! Pedestrian-specific link properties and the Weidmann (1993) fundamental
//! diagram for pedestrian dynamics.
//!
//! # Weidmann 1993 Model
//!
//! The speed-density relationship for pedestrian flows:
//!
//! $$v(\rho) = v_0 \cdot \left(1 - \exp\!\left(-\gamma \cdot \left(\frac{1}{\rho} - \frac{1}{\rho_{\text{jam}}}\right)\right)\right)$$
//!
//! where:
//! - $v_0$ = free-flow walking speed (m/s), typically 1.34 m/s
//! - $\gamma$ = 1.913 (Weidmann empirical constant)
//! - $\rho_{\text{jam}}$ = 5.4 ped/m² (maximum observed pedestrian density)
//!
//! # Example
//!
//! ```
//! use rustsim_traffic::pedestrian_links::{weidmann_speed, weidmann_flow, PedestrianLinkProperties};
//!
//! // Free-flow at zero density
//! let v = weidmann_speed(1.34, 0.0);
//! assert!((v - 1.34).abs() < 1e-6);
//!
//! // Speed drops with density
//! let v = weidmann_speed(1.34, 2.0);
//! assert!(v < 1.34);
//! assert!(v > 0.0);
//!
//! // Pedestrian corridor link
//! let link = PedestrianLinkProperties::corridor(50.0, 3.0);
//! assert!((link.length_m - 50.0).abs() < 1e-6);
//! ```

// ---------------------------------------------------------------------------
// Weidmann 1993 constants
// ---------------------------------------------------------------------------

/// Weidmann empirical constant (dimensionless).
pub const WEIDMANN_GAMMA: f64 = 1.913;

/// Jam density (ped/m²) — maximum observed pedestrian density.
pub const WEIDMANN_RHO_JAM: f64 = 5.4;

/// Default free-flow walking speed (m/s) — Weidmann 1993 mean.
pub const WEIDMANN_FREE_FLOW_SPEED: f64 = 1.34;

// ---------------------------------------------------------------------------
// Weidmann speed-density functions
// ---------------------------------------------------------------------------

/// Compute the expected walking speed (m/s) for a given density using
/// the Weidmann (1993) fundamental diagram.
///
/// Returns `v0` when density is near zero, and approaches zero at jam
/// density. Negative densities are clamped to zero.
pub fn weidmann_speed(v0: f64, density_ped_per_m2: f64) -> f64 {
    let rho = density_ped_per_m2.max(0.0);
    if rho < 1e-6 {
        return v0;
    }
    if rho >= WEIDMANN_RHO_JAM {
        return 0.0;
    }
    let factor = 1.0 - (-WEIDMANN_GAMMA * (1.0 / rho - 1.0 / WEIDMANN_RHO_JAM)).exp();
    v0 * factor.clamp(0.0, 1.0)
}

/// Compute the expected pedestrian flow (ped/m/s) for a given density:
/// $q(\rho) = \rho \cdot v(\rho)$.
pub fn weidmann_flow(v0: f64, density_ped_per_m2: f64) -> f64 {
    density_ped_per_m2.max(0.0) * weidmann_speed(v0, density_ped_per_m2)
}

/// Compute the density factor for speed adaptation: `v(ρ) / v0`.
///
/// This is the multiplicative factor applied to the desired speed to
/// account for crowd density. Returns 1.0 at zero density, approaches
/// 0.0 at jam density.
pub fn weidmann_density_factor(density_ped_per_m2: f64) -> f64 {
    let rho = density_ped_per_m2.max(0.0);
    if rho < 1e-6 {
        return 1.0;
    }
    if rho >= WEIDMANN_RHO_JAM {
        return 0.0;
    }
    let factor = 1.0 - (-WEIDMANN_GAMMA * (1.0 / rho - 1.0 / WEIDMANN_RHO_JAM)).exp();
    factor.clamp(0.0, 1.0)
}

// ---------------------------------------------------------------------------
// Pedestrian link properties
// ---------------------------------------------------------------------------

/// Physical properties of a pedestrian link (corridor, walkway, stairway).
///
/// This is a pedestrian-specific counterpart to the vehicular
/// [`LinkProperties`](crate::types::LinkProperties). It stores the geometric
/// and behavioral parameters relevant to pedestrian movement.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PedestrianLinkProperties {
    /// Geometric length in meters.
    pub length_m: f64,
    /// Effective width in meters (usable walking width).
    pub width_m: f64,
    /// Free-flow walking speed in m/s.
    pub free_flow_speed_mps: f64,
    /// Speed limit in m/s (ceiling on desired speed).
    pub speed_limit_mps: f64,
    /// Maximum number of pedestrians allowed simultaneously.
    /// `0` means unlimited.
    pub capacity: u32,
    /// Link classification for mode filtering and routing.
    pub link_class: PedestrianLinkClass,
}

/// Classification of pedestrian links.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PedestrianLinkClass {
    /// Standard corridor or hallway.
    Corridor,
    /// Walkable floor polygon (open area).
    WalkableFloor,
    /// Stairway (may affect speed).
    Stairway,
    /// Ramp or slope.
    Ramp,
    /// Escalator (mechanized movement).
    Escalator,
    /// Crosswalk or pedestrian crossing.
    Crossing,
}

impl PedestrianLinkProperties {
    /// Create properties for a standard corridor.
    pub fn corridor(length_m: f64, width_m: f64) -> Self {
        Self {
            length_m,
            width_m,
            free_flow_speed_mps: WEIDMANN_FREE_FLOW_SPEED,
            speed_limit_mps: 2.0,
            capacity: 0,
            link_class: PedestrianLinkClass::Corridor,
        }
    }

    /// Create properties for a walkable floor polygon.
    pub fn floor(length_m: f64, width_m: f64) -> Self {
        Self {
            length_m,
            width_m,
            free_flow_speed_mps: WEIDMANN_FREE_FLOW_SPEED,
            speed_limit_mps: 2.0,
            capacity: 0,
            link_class: PedestrianLinkClass::WalkableFloor,
        }
    }

    /// Create properties for a stairway (reduced free-flow speed).
    pub fn stairway(length_m: f64, width_m: f64) -> Self {
        Self {
            length_m,
            width_m,
            free_flow_speed_mps: 0.6,
            speed_limit_mps: 1.0,
            capacity: 0,
            link_class: PedestrianLinkClass::Stairway,
        }
    }

    /// Override free-flow speed.
    pub fn with_free_flow_speed(mut self, speed_mps: f64) -> Self {
        self.free_flow_speed_mps = speed_mps;
        self
    }

    /// Override speed limit.
    pub fn with_speed_limit(mut self, speed_mps: f64) -> Self {
        self.speed_limit_mps = speed_mps;
        self
    }

    /// Set maximum capacity.
    pub fn with_capacity(mut self, capacity: u32) -> Self {
        self.capacity = capacity;
        self
    }

    /// Compute Weidmann speed for a given number of pedestrians on this link.
    ///
    /// Density is computed as `count / (length_m × width_m)`.
    pub fn speed_for_count(&self, count: usize) -> f64 {
        let area = self.length_m * self.width_m;
        if area <= 0.0 {
            return self.free_flow_speed_mps;
        }
        let density = count as f64 / area;
        weidmann_speed(self.free_flow_speed_mps, density).min(self.speed_limit_mps)
    }

    /// Free-flow traversal time in seconds.
    pub fn free_flow_time_s(&self) -> f64 {
        if self.free_flow_speed_mps <= 0.0 {
            return f64::INFINITY;
        }
        self.length_m / self.free_flow_speed_mps
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn weidmann_free_flow_at_zero_density() {
        let v = weidmann_speed(1.34, 0.0);
        assert!((v - 1.34).abs() < 1e-6);
    }

    #[test]
    fn weidmann_speed_decreases_with_density() {
        let v0 = 1.34;
        let v_low = weidmann_speed(v0, 0.5);
        let v_mid = weidmann_speed(v0, 2.0);
        let v_high = weidmann_speed(v0, 4.0);
        assert!(v_low > v_mid);
        assert!(v_mid > v_high);
        assert!(v_high > 0.0);
    }

    #[test]
    fn weidmann_zero_at_jam_density() {
        let v = weidmann_speed(1.34, WEIDMANN_RHO_JAM);
        assert!((v - 0.0).abs() < 1e-6);
    }

    #[test]
    fn weidmann_speed_never_negative() {
        for d in [0.0, 0.1, 1.0, 3.0, 5.0, 5.4, 10.0, 100.0] {
            assert!(weidmann_speed(1.34, d) >= 0.0);
        }
    }

    #[test]
    fn weidmann_speed_never_exceeds_free_flow() {
        for d in [0.0, 0.01, 0.1, 0.5, 1.0, 2.0, 5.0] {
            assert!(weidmann_speed(1.34, d) <= 1.34 + 1e-12);
        }
    }

    #[test]
    fn weidmann_flow_peaks_at_intermediate_density() {
        let v0 = 1.34;
        let flow_low = weidmann_flow(v0, 0.1);
        let flow_mid = weidmann_flow(v0, 1.5);
        let flow_high = weidmann_flow(v0, 5.0);
        let flow_zero = weidmann_flow(v0, 0.0);
        assert_eq!(flow_zero, 0.0);
        assert!(flow_mid > flow_low);
        assert!(flow_mid > flow_high);
    }

    #[test]
    fn density_factor_range() {
        assert!((weidmann_density_factor(0.0) - 1.0).abs() < 1e-6);
        assert!((weidmann_density_factor(WEIDMANN_RHO_JAM) - 0.0).abs() < 1e-6);
        let mid = weidmann_density_factor(2.0);
        assert!(mid > 0.0 && mid < 1.0);
    }

    #[test]
    fn pedestrian_corridor_defaults() {
        let link = PedestrianLinkProperties::corridor(50.0, 3.0);
        assert!((link.length_m - 50.0).abs() < 1e-6);
        assert!((link.width_m - 3.0).abs() < 1e-6);
        assert!((link.free_flow_speed_mps - WEIDMANN_FREE_FLOW_SPEED).abs() < 1e-6);
        assert_eq!(link.capacity, 0);
        assert_eq!(link.link_class, PedestrianLinkClass::Corridor);
    }

    #[test]
    fn stairway_slower_than_corridor() {
        let corridor = PedestrianLinkProperties::corridor(50.0, 2.0);
        let stairway = PedestrianLinkProperties::stairway(50.0, 2.0);
        assert!(stairway.free_flow_speed_mps < corridor.free_flow_speed_mps);
    }

    #[test]
    fn speed_for_count_uses_weidmann() {
        let link = PedestrianLinkProperties::corridor(100.0, 2.0);
        let speed_empty = link.speed_for_count(0);
        let speed_crowded = link.speed_for_count(500);
        assert!((speed_empty - WEIDMANN_FREE_FLOW_SPEED).abs() < 1e-6);
        assert!(speed_crowded < speed_empty);
    }

    #[test]
    fn free_flow_time_computed() {
        let link = PedestrianLinkProperties::corridor(100.0, 2.0);
        let expected = 100.0 / WEIDMANN_FREE_FLOW_SPEED;
        assert!((link.free_flow_time_s() - expected).abs() < 1e-6);
    }
}