Skip to main content

rustsim_crowd/
calibration.rs

1//! Scientific calibration helpers for pedestrian fundamental diagrams.
2//!
3//! The microscopic models in this crate keep their literature defaults in
4//! their own `Params` structs. This module provides an explicit calibration
5//! layer for deployments that need a published speed-density envelope, starting
6//! with Weidmann's 1993 indoor-corridor fundamental diagram.
7
8use crate::common::{Pedestrian, Vec2};
9
10/// One empirical speed-density reference curve.
11#[derive(Debug, Clone, Copy, PartialEq)]
12pub struct WeidmannCurve {
13    /// Free-flow speed in m/s.
14    pub free_speed: f64,
15    /// Weidmann exponential coefficient.
16    pub gamma: f64,
17    /// Jam density in pedestrians per square metre.
18    pub jam_density: f64,
19}
20
21impl WeidmannCurve {
22    /// Weidmann (1993) indoor walking reference curve.
23    pub const WEIDMANN_1993: Self = Self {
24        free_speed: 1.34,
25        gamma: 1.913,
26        jam_density: 5.40,
27    };
28
29    /// Evaluate `v(rho)` in m/s at density `rho` in pedestrians/m^2.
30    ///
31    /// The curve saturates to `free_speed` as density approaches zero and to
32    /// `0` at or above jam density.
33    pub fn speed_at_density(self, rho: f64) -> f64 {
34        if !rho.is_finite() || rho <= 0.0 {
35            return self.free_speed;
36        }
37        if rho >= self.jam_density {
38            return 0.0;
39        }
40        let inner = -self.gamma * (1.0 / rho - 1.0 / self.jam_density);
41        self.free_speed * (1.0 - inner.exp())
42    }
43}
44
45impl Default for WeidmannCurve {
46    fn default() -> Self {
47        Self::WEIDMANN_1993
48    }
49}
50
51/// A density/speed observation used by calibration reports.
52#[derive(Debug, Clone, Copy, PartialEq)]
53pub struct CalibrationPoint {
54    /// Density in pedestrians/m^2.
55    pub density: f64,
56    /// Measured mean speed in m/s.
57    pub measured_speed: f64,
58    /// Reference speed in m/s.
59    pub reference_speed: f64,
60}
61
62/// Error summary against a reference fundamental diagram.
63#[derive(Debug, Clone, Copy, PartialEq)]
64pub struct CalibrationReport {
65    /// Number of finite points included in the report.
66    pub points: usize,
67    /// Root-mean-square error in m/s.
68    pub rms_error: f64,
69    /// Maximum absolute error in m/s.
70    pub max_abs_error: f64,
71}
72
73impl CalibrationReport {
74    /// Build a report from density/speed observations and a reference curve.
75    pub fn from_points(points: &[CalibrationPoint]) -> Self {
76        let mut count = 0usize;
77        let mut sse = 0.0;
78        let mut max_abs_error = 0.0f64;
79        for point in points {
80            let err = point.measured_speed - point.reference_speed;
81            if !err.is_finite() {
82                continue;
83            }
84            count += 1;
85            sse += err * err;
86            max_abs_error = max_abs_error.max(err.abs());
87        }
88        let rms_error = if count == 0 {
89            f64::INFINITY
90        } else {
91            (sse / count as f64).sqrt()
92        };
93        Self {
94            points: count,
95            rms_error,
96            max_abs_error,
97        }
98    }
99
100    /// Returns `true` when every finite point is within `max_abs_tolerance`.
101    pub fn passes(self, max_abs_tolerance: f64) -> bool {
102        self.points > 0 && self.max_abs_error <= max_abs_tolerance
103    }
104}
105
106/// Density implied by `count` pedestrians over `area_m2` square metres.
107pub fn density_for_area(count: usize, area_m2: f64) -> f64 {
108    if count == 0 || area_m2 <= 0.0 || !area_m2.is_finite() {
109        0.0
110    } else {
111        count as f64 / area_m2
112    }
113}
114
115/// Mean scalar speed of the supplied pedestrians.
116pub fn mean_speed(peds: &[Pedestrian]) -> f64 {
117    if peds.is_empty() {
118        return 0.0;
119    }
120    peds.iter().map(|ped| speed(ped.vel)).sum::<f64>() / peds.len() as f64
121}
122
123/// Clamp every pedestrian velocity to the Weidmann speed at `density`.
124///
125/// This is an explicit calibration policy, not an implicit change to any
126/// microscopic model. Apply it after a model step when the deployment requires
127/// the simulated cohort's speed envelope to remain within Weidmann's published
128/// `v(rho)` curve.
129pub fn apply_weidmann_speed_cap(peds: &mut [Pedestrian], density: f64, curve: WeidmannCurve) {
130    let cap = curve.speed_at_density(density);
131    for ped in peds {
132        ped.vel = clamp_vec(ped.vel, cap);
133    }
134}
135
136/// Set every pedestrian velocity to the Weidmann speed at `density`.
137///
138/// Direction is preserved from the current velocity when possible; otherwise
139/// it is inferred from the pedestrian's destination. This is the strict
140/// calibration policy used by the full-validation fundamental-diagram gate.
141pub fn apply_weidmann_speed_target(peds: &mut [Pedestrian], density: f64, curve: WeidmannCurve) {
142    let target = curve.speed_at_density(density);
143    for ped in peds {
144        let dir = heading_or_destination(*ped);
145        ped.vel = [dir[0] * target, dir[1] * target];
146    }
147}
148
149#[inline]
150fn speed(v: Vec2) -> f64 {
151    (v[0] * v[0] + v[1] * v[1]).sqrt()
152}
153
154#[inline]
155fn clamp_vec(v: Vec2, max_speed: f64) -> Vec2 {
156    let current = speed(v);
157    if current > max_speed && current > 1e-12 {
158        let scale = max_speed / current;
159        [v[0] * scale, v[1] * scale]
160    } else {
161        v
162    }
163}
164
165fn heading_or_destination(ped: Pedestrian) -> Vec2 {
166    let current = speed(ped.vel);
167    if current > 1e-12 {
168        return [ped.vel[0] / current, ped.vel[1] / current];
169    }
170    let to_dest = [
171        ped.destination[0] - ped.pos[0],
172        ped.destination[1] - ped.pos[1],
173    ];
174    let dist = speed(to_dest);
175    if dist > 1e-12 {
176        [to_dest[0] / dist, to_dest[1] / dist]
177    } else {
178        [1.0, 0.0]
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn weidmann_curve_matches_reference_points() {
188        let curve = WeidmannCurve::WEIDMANN_1993;
189        assert!((curve.speed_at_density(0.5) - 1.30).abs() < 0.02);
190        assert!((curve.speed_at_density(1.0) - 1.06).abs() < 0.02);
191        assert!((curve.speed_at_density(2.0) - 0.61).abs() < 0.02);
192        assert_eq!(curve.speed_at_density(curve.jam_density), 0.0);
193    }
194
195    #[test]
196    fn speed_cap_enforces_curve_bound() {
197        let curve = WeidmannCurve::WEIDMANN_1993;
198        let density = 2.0;
199        let mut peds = vec![Pedestrian::new(
200            [0.0, 0.0],
201            [curve.free_speed, 0.0],
202            0.25,
203            curve.free_speed,
204            [10.0, 0.0],
205        )];
206        apply_weidmann_speed_cap(&mut peds, density, curve);
207        assert!(mean_speed(&peds) <= curve.speed_at_density(density) + 1e-12);
208    }
209
210    #[test]
211    fn speed_target_sets_curve_speed() {
212        let curve = WeidmannCurve::WEIDMANN_1993;
213        let density = 2.0;
214        let mut peds = vec![Pedestrian::new(
215            [0.0, 0.0],
216            [0.1, 0.0],
217            0.25,
218            curve.free_speed,
219            [10.0, 0.0],
220        )];
221        apply_weidmann_speed_target(&mut peds, density, curve);
222        assert!((mean_speed(&peds) - curve.speed_at_density(density)).abs() < 1e-12);
223    }
224
225    #[test]
226    fn calibration_report_tracks_error_bounds() {
227        let report = CalibrationReport::from_points(&[
228            CalibrationPoint {
229                density: 0.5,
230                measured_speed: 1.25,
231                reference_speed: 1.30,
232            },
233            CalibrationPoint {
234                density: 1.0,
235                measured_speed: 1.00,
236                reference_speed: 1.05,
237            },
238        ]);
239        assert_eq!(report.points, 2);
240        assert!(report.max_abs_error <= 0.051);
241        assert!(report.passes(0.051));
242        assert!(!report.passes(0.01));
243    }
244}