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}