rustsim_crowd/broadphase.rs
1//! Broadphase acceleration for crowd models.
2//!
3//! Every force-based or velocity-based model in this crate is
4//! fundamentally a pair-wise interaction: pedestrian *i* only needs to
5//! consider pedestrians *j* within a small cutoff radius where the
6//! interaction term is non-negligible. The naïve double loop is
7//! `O(n^2)`; wrapping `rustsim-broadphase::UniformGrid2` around it
8//! turns every `step` into `O(n + n * k)` where `k` is the average
9//! per-agent neighbour count, which is typically 5–20 for realistic
10//! crowd densities.
11//!
12//! Each model picks its own cutoff through the function below:
13//!
14//! - [`NeighborGrid::rebuild`] drops the previous frame and re-inserts
15//! every pedestrian by its current position. Callers own the grid,
16//! so one allocation per *simulation*, not per *tick*.
17//! - [`NeighborGrid::for_each_neighbor`] hands the caller `(j, pos_j,
18//! ped_j)` for every pedestrian inside the cutoff radius of a source
19//! position, skipping the source index itself.
20//!
21//! The grid keys are `u64` agent indices cast from `usize`. Pedestrian
22//! slices must fit in `u64::MAX` — trivially satisfied in practice.
23
24use rustsim_broadphase::UniformGrid2;
25
26use crate::common::{Pedestrian, Vec2};
27
28/// Per-tick neighbour grid for the crowd models.
29///
30/// Typical usage:
31///
32/// ```ignore
33/// let mut grid = NeighborGrid::new(1.0);
34/// for _step in 0..num_steps {
35/// grid.rebuild(&peds);
36/// social_force::step_with_grid(&mut peds, &walls, ¶ms, dt, &grid);
37/// }
38/// ```
39#[derive(Debug, Clone)]
40pub struct NeighborGrid {
41 grid: UniformGrid2,
42}
43
44impl NeighborGrid {
45 /// Construct a grid with the given cell size (metres).
46 ///
47 /// Choose `cell_size` close to the dominant model cutoff — e.g.
48 /// `1.0` m for Social Force with default `b_ped`, `3.0` m for
49 /// Optimal Steps with default `ped_range`, or the output of
50 /// [`recommended_cell_size`].
51 ///
52 /// # Panics
53 /// Panics if `cell_size` is not strictly positive.
54 pub fn new(cell_size: f64) -> Self {
55 Self {
56 grid: UniformGrid2::new(cell_size),
57 }
58 }
59
60 /// Drop every entry.
61 #[inline]
62 pub fn clear(&mut self) {
63 self.grid.clear();
64 }
65
66 /// Clear the grid and re-insert every pedestrian keyed by its
67 /// slice index cast to `u64`.
68 pub fn rebuild(&mut self, peds: &[Pedestrian]) {
69 self.grid.clear();
70 for (i, p) in peds.iter().enumerate() {
71 self.grid.insert(i as u64, p.pos);
72 }
73 }
74
75 /// Invoke `f(j, pos_j, ped_j)` for every pedestrian within
76 /// `radius` of `pos_i`, skipping index `i`.
77 ///
78 /// `peds` must be the same slice that was passed to
79 /// [`rebuild`](Self::rebuild); the grid stores only indices and
80 /// positions, so the caller provides the slice for the actual
81 /// pedestrian record lookup.
82 #[inline]
83 pub fn for_each_neighbor<F>(&self, i: usize, radius: f64, peds: &[Pedestrian], mut f: F)
84 where
85 F: FnMut(usize, &Pedestrian),
86 {
87 let pos_i = peds[i].pos;
88 self.grid.for_each_within(pos_i, radius, |k, _pos_j| {
89 let j = k as usize;
90 if j == i {
91 return;
92 }
93 if j >= peds.len() {
94 // Stale entry from a previous rebuild that spanned a
95 // larger slice — defensive guard, never triggered in
96 // the intended `rebuild → step` sequence.
97 return;
98 }
99 f(j, &peds[j]);
100 });
101 }
102
103 /// Underlying grid, for diagnostics or custom queries.
104 #[inline]
105 pub fn inner(&self) -> &UniformGrid2 {
106 &self.grid
107 }
108}
109
110/// Suggested cell size for a given interaction cutoff radius.
111///
112/// Uniform hash-grids prune best when the cell edge matches the query
113/// radius; anything smaller wastes cells, anything larger wastes
114/// comparisons. Returns `radius` clamped to at least `0.1` m so
115/// pathological inputs never produce zero-sized cells.
116#[inline]
117pub fn recommended_cell_size(radius: f64) -> f64 {
118 radius.max(0.1)
119}
120
121/// Reusable scratch buffers for the crowd models.
122///
123/// Every `step_*` path in this crate needs exactly one `Vec<Vec2>` of
124/// length `peds.len()` (for next-tick accelerations, velocities, or
125/// positions) and one [`NeighborGrid`] rebuilt against the current
126/// pedestrian slice. `Scratch` bundles both so a caller can allocate
127/// **once per simulation** instead of twice per tick.
128///
129/// ```ignore
130/// let mut scratch = Scratch::with_capacity(peds.len(), 1.0);
131/// for _ in 0..num_steps {
132/// social_force::step_scratch(&mut peds, &walls, ¶ms, dt, &mut scratch);
133/// }
134/// ```
135///
136/// `Scratch` is not tied to a specific model: the same instance can
137/// drive `social_force`, `collision_free_speed`, `anticipation_velocity`,
138/// `generalized_centrifugal_force`, or `optimal_steps` step-by-step,
139/// provided the grid's `cell_size` is a sensible cap on the largest
140/// [`neighbor_cutoff`](crate::social_force::neighbor_cutoff) in play.
141#[derive(Debug, Clone)]
142pub struct Scratch {
143 /// Per-agent working buffer (re-used as accelerations, velocities,
144 /// or next-step positions depending on the caller).
145 pub(crate) buf: Vec<Vec2>,
146 /// Neighbour grid rebuilt against the pedestrian slice each tick.
147 pub grid: NeighborGrid,
148}
149
150impl Scratch {
151 /// Empty scratch with a grid of the given cell size (metres).
152 ///
153 /// # Panics
154 /// Panics if `cell_size` is not strictly positive.
155 #[inline]
156 pub fn new(cell_size: f64) -> Self {
157 Self {
158 buf: Vec::new(),
159 grid: NeighborGrid::new(cell_size),
160 }
161 }
162
163 /// Scratch pre-sized for `n` pedestrians and a grid of `cell_size`.
164 #[inline]
165 pub fn with_capacity(n: usize, cell_size: f64) -> Self {
166 Self {
167 buf: Vec::with_capacity(n),
168 grid: NeighborGrid::new(cell_size),
169 }
170 }
171
172 /// Clear the buffers without releasing their capacity.
173 #[inline]
174 pub fn clear(&mut self) {
175 self.buf.clear();
176 self.grid.clear();
177 }
178
179 /// Resize `buf` to length `n` (zero-filled) without freeing capacity
180 /// and rebuild `grid` against `peds`. Called by every `step_scratch`.
181 #[inline]
182 pub(crate) fn prepare(&mut self, peds: &[Pedestrian]) {
183 let n = peds.len();
184 self.buf.clear();
185 self.buf.resize(n, [0.0, 0.0]);
186 self.grid.rebuild(peds);
187 }
188
189 /// Split-borrow the scratch buffer and the read-only grid.
190 #[inline]
191 pub(crate) fn split(&mut self) -> (&mut [Vec2], &NeighborGrid) {
192 (&mut self.buf, &self.grid)
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 fn ped_at(x: f64, y: f64) -> Pedestrian {
201 Pedestrian {
202 pos: [x, y],
203 vel: [0.0, 0.0],
204 radius: 0.25,
205 desired_speed: 1.34,
206 destination: [x + 10.0, y],
207 }
208 }
209
210 #[test]
211 fn rebuild_and_query_returns_only_nearby() {
212 let peds = vec![
213 ped_at(0.0, 0.0),
214 ped_at(0.3, 0.0),
215 ped_at(5.0, 0.0),
216 ped_at(0.0, 0.5),
217 ];
218 let mut grid = NeighborGrid::new(1.0);
219 grid.rebuild(&peds);
220 let mut hits = Vec::new();
221 grid.for_each_neighbor(0, 1.0, &peds, |j, _| hits.push(j));
222 hits.sort();
223 assert_eq!(hits, vec![1, 3]);
224 }
225
226 #[test]
227 fn skips_self_index() {
228 let peds = vec![ped_at(0.0, 0.0), ped_at(0.1, 0.0)];
229 let mut grid = NeighborGrid::new(1.0);
230 grid.rebuild(&peds);
231 let mut hits = Vec::new();
232 grid.for_each_neighbor(0, 5.0, &peds, |j, _| hits.push(j));
233 assert_eq!(hits, vec![1]);
234 }
235
236 #[test]
237 fn empty_slice_is_safe() {
238 let peds: Vec<Pedestrian> = Vec::new();
239 let mut grid = NeighborGrid::new(1.0);
240 grid.rebuild(&peds);
241 // No assertion needed — just must not panic.
242 }
243
244 #[test]
245 fn scratch_reuses_capacity_across_ticks() {
246 let peds = vec![ped_at(0.0, 0.0), ped_at(0.5, 0.0), ped_at(1.0, 0.0)];
247 let mut scratch = Scratch::with_capacity(3, 1.0);
248 scratch.prepare(&peds);
249 assert_eq!(scratch.buf.len(), 3);
250 let cap_after_first = scratch.buf.capacity();
251 // Second tick must not re-allocate.
252 scratch.prepare(&peds);
253 assert_eq!(scratch.buf.len(), 3);
254 assert_eq!(scratch.buf.capacity(), cap_after_first);
255 }
256}