Skip to main content

rustsim_crowd/
common.rs

1//! Shared types and trait for all pedestrian models in this crate.
2
3/// 2-D vector alias used throughout the crate.
4pub type Vec2 = [f64; 2];
5
6/// A single pedestrian agent as seen by every model in this crate.
7///
8/// This is intentionally a small, copy-friendly struct so model `step`
9/// functions can be called on `&mut [Pedestrian]` without any indirection.
10///
11/// # Construction
12///
13/// Outside this crate, **construct via [`Pedestrian::new`]**, not struct
14/// literal syntax: the type is `#[non_exhaustive]` so that adding a field
15/// in a future minor version is a non-breaking change for downstream
16/// callers. Field-access (`ped.pos`, `ped.vel = …`) remains stable and
17/// is intentionally left `pub` so the SoA-friendly hot path stays
18/// indirection-free.
19#[derive(Debug, Clone, Copy, PartialEq)]
20#[non_exhaustive]
21pub struct Pedestrian {
22    /// Position in metres.
23    pub pos: Vec2,
24    /// Velocity in m/s.
25    pub vel: Vec2,
26    /// Body radius in metres (typical pedestrian: 0.2–0.3 m).
27    pub radius: f64,
28    /// Desired free-flow walking speed in m/s (Weidmann mean: 1.34 m/s).
29    pub desired_speed: f64,
30    /// Target destination in metres.
31    pub destination: Vec2,
32}
33
34impl Pedestrian {
35    /// Constructs a [`Pedestrian`] from its five core fields.
36    ///
37    /// This is the **stable construction path** for downstream callers:
38    /// because [`Pedestrian`] is `#[non_exhaustive]`, adding a field in a
39    /// future minor version will not break callers that go through
40    /// `Pedestrian::new`. Each new field will get a paired `with_*`
41    /// setter (e.g. a hypothetical `with_target_floor`) so the
42    /// extension stays additive.
43    #[inline]
44    pub fn new(pos: Vec2, vel: Vec2, radius: f64, desired_speed: f64, destination: Vec2) -> Self {
45        Self {
46            pos,
47            vel,
48            radius,
49            desired_speed,
50            destination,
51        }
52    }
53    /// Vector from `pos` to `destination`.
54    #[inline]
55    pub fn to_destination(&self) -> Vec2 {
56        sub(self.destination, self.pos)
57    }
58
59    /// Unit vector pointing at the destination, or `[0, 0]` if already there.
60    #[inline]
61    pub fn desired_direction(&self) -> Vec2 {
62        normalize(self.to_destination())
63    }
64
65    /// Current speed `|vel|`.
66    #[inline]
67    pub fn speed(&self) -> f64 {
68        norm(self.vel)
69    }
70
71    /// Euclidean distance from `pos` to `destination`.
72    #[inline]
73    pub fn distance_to_destination(&self) -> f64 {
74        norm(self.to_destination())
75    }
76
77    /// Returns `true` if the pedestrian is inside its arrival radius.
78    #[inline]
79    pub fn has_arrived(&self, arrival_radius: f64) -> bool {
80        self.distance_to_destination() <= arrival_radius
81    }
82
83    /// Speed the pedestrian should aim for given how close it is to
84    /// its destination. Produces a smooth linear taper from the full
85    /// [`desired_speed`](Self::desired_speed) at `d ≥ arrival_radius`
86    /// down to `0` at `d = 0`, so agents **do not overshoot** their
87    /// goal and then oscillate back toward it.
88    ///
89    /// `arrival_radius ≤ 0` disables the taper (returns
90    /// [`desired_speed`](Self::desired_speed) unchanged).
91    #[inline]
92    pub fn effective_desired_speed(&self, arrival_radius: f64) -> f64 {
93        if arrival_radius <= 0.0 {
94            return self.desired_speed;
95        }
96        let d = self.distance_to_destination();
97        if d >= arrival_radius {
98            self.desired_speed
99        } else {
100            self.desired_speed * (d / arrival_radius)
101        }
102    }
103}
104
105/// A 2-D line segment describing a static obstacle (wall, barrier, etc.).
106#[derive(Debug, Clone, Copy, PartialEq)]
107pub struct WallSegment {
108    /// First endpoint in metres.
109    pub a: Vec2,
110    /// Second endpoint in metres.
111    pub b: Vec2,
112}
113
114/// Common contract for every pedestrian locomotion model in this crate.
115///
116/// Implementations advance a slice of [`Pedestrian`] in place by one timestep
117/// of `dt` seconds, considering static [`WallSegment`] obstacles and a
118/// model-specific parameter bundle.
119///
120/// All trait objects are safe; every concrete `Params` is `Clone + Debug`.
121pub trait PedestrianModel {
122    /// Model-specific parameter bundle (typically with a calibrated `Default`).
123    type Params;
124
125    /// Short human-readable name, e.g. `"Social Force"`.
126    fn name(&self) -> &'static str;
127
128    /// Advance every pedestrian in `peds` by `dt` seconds.
129    ///
130    /// `walls` is a slice of static obstacles; pass an empty slice if none.
131    fn step(&self, peds: &mut [Pedestrian], walls: &[WallSegment], params: &Self::Params, dt: f64);
132}
133
134// ---------------------------------------------------------------------------
135// Vector math primitives (crate-internal).
136// ---------------------------------------------------------------------------
137
138#[inline]
139pub(crate) fn add(a: Vec2, b: Vec2) -> Vec2 {
140    [a[0] + b[0], a[1] + b[1]]
141}
142
143#[inline]
144pub(crate) fn sub(a: Vec2, b: Vec2) -> Vec2 {
145    [a[0] - b[0], a[1] - b[1]]
146}
147
148#[inline]
149pub(crate) fn scale(a: Vec2, s: f64) -> Vec2 {
150    [a[0] * s, a[1] * s]
151}
152
153#[inline]
154pub(crate) fn dot(a: Vec2, b: Vec2) -> f64 {
155    a[0] * b[0] + a[1] * b[1]
156}
157
158#[inline]
159pub(crate) fn norm(a: Vec2) -> f64 {
160    (a[0] * a[0] + a[1] * a[1]).sqrt()
161}
162
163#[inline]
164pub(crate) fn normalize(a: Vec2) -> Vec2 {
165    let n = norm(a);
166    if n < 1e-12 {
167        [0.0, 0.0]
168    } else {
169        [a[0] / n, a[1] / n]
170    }
171}
172
173/// Returns the closest point on segment `ab` to point `p`.
174#[inline]
175pub(crate) fn closest_point_on_segment(p: Vec2, a: Vec2, b: Vec2) -> Vec2 {
176    let ab = sub(b, a);
177    let denom = dot(ab, ab);
178    if denom < 1e-18 {
179        return a;
180    }
181    let t = (dot(sub(p, a), ab) / denom).clamp(0.0, 1.0);
182    add(a, scale(ab, t))
183}
184
185// ---------------------------------------------------------------------------
186// Small helpers shared by several models.
187// ---------------------------------------------------------------------------
188
189/// Clamp the magnitude of `v` to `max_speed`. Returns `v` unchanged if its
190/// magnitude is already within bounds or near zero.
191#[inline]
192pub(crate) fn clamp_speed(v: Vec2, max_speed: f64) -> Vec2 {
193    let s = norm(v);
194    if s > max_speed && s > 1e-12 {
195        scale(v, max_speed / s)
196    } else {
197        v
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn normalize_zero_is_zero() {
207        assert_eq!(normalize([0.0, 0.0]), [0.0, 0.0]);
208    }
209
210    #[test]
211    fn closest_point_on_segment_endpoints_and_middle() {
212        let a = [0.0, 0.0];
213        let b = [10.0, 0.0];
214        assert_eq!(closest_point_on_segment([-5.0, 5.0], a, b), a);
215        assert_eq!(closest_point_on_segment([15.0, 5.0], a, b), b);
216        assert_eq!(closest_point_on_segment([5.0, 5.0], a, b), [5.0, 0.0]);
217    }
218
219    #[test]
220    fn clamp_speed_basic() {
221        let v = clamp_speed([3.0, 4.0], 2.5);
222        let s = norm(v);
223        assert!((s - 2.5).abs() < 1e-9);
224    }
225
226    #[test]
227    fn pedestrian_new_matches_literal_construction() {
228        // Pins the P2-8 construction-stability contract: `Pedestrian::new`
229        // is the future-proof construction path, and it must produce a
230        // value bit-equal to the legacy literal-syntax construction so
231        // downstream callers can migrate without observable behavioural
232        // change. (Inside this crate `#[non_exhaustive]` does not block
233        // literal syntax, so we can compare against it here.)
234        let via_new = Pedestrian::new([1.0, 2.0], [0.3, 0.4], 0.25, 1.34, [10.0, 0.0]);
235        let via_lit = Pedestrian {
236            pos: [1.0, 2.0],
237            vel: [0.3, 0.4],
238            radius: 0.25,
239            desired_speed: 1.34,
240            destination: [10.0, 0.0],
241        };
242        assert_eq!(via_new, via_lit);
243    }
244
245    #[test]
246    fn effective_desired_speed_tapers_inside_arrival_radius() {
247        let p = Pedestrian {
248            pos: [4.7, 0.0], // 0.3 m from destination
249            vel: [0.0, 0.0],
250            radius: 0.25,
251            desired_speed: 1.0,
252            destination: [5.0, 0.0],
253        };
254        // At exactly `arrival_radius`: full speed.
255        assert!((p.effective_desired_speed(0.3) - 1.0).abs() < 1e-12);
256        // At half the radius: half speed.
257        let p_half = Pedestrian {
258            pos: [4.85, 0.0],
259            ..p
260        };
261        assert!((p_half.effective_desired_speed(0.3) - 0.5).abs() < 1e-12);
262        // At the destination: zero.
263        let p_there = Pedestrian {
264            pos: [5.0, 0.0],
265            ..p
266        };
267        assert_eq!(p_there.effective_desired_speed(0.3), 0.0);
268        // arrival_radius == 0 disables the taper.
269        assert_eq!(p_there.effective_desired_speed(0.0), 1.0);
270    }
271
272    #[test]
273    fn has_arrived_reports_inside_radius() {
274        let p = Pedestrian {
275            pos: [4.9, 0.0],
276            vel: [0.0, 0.0],
277            radius: 0.25,
278            desired_speed: 1.0,
279            destination: [5.0, 0.0],
280        };
281        assert!(p.has_arrived(0.2));
282        assert!(!p.has_arrived(0.05));
283    }
284}