1use crate::broadphase::{NeighborGrid, Scratch};
26use crate::common::{
27 add, closest_point_on_segment, norm, normalize, scale, sub, Pedestrian, PedestrianModel, Vec2,
28 WallSegment,
29};
30
31#[derive(Debug, Clone, Copy, PartialEq)]
33pub struct Params {
34 pub num_candidates: usize,
36 pub stride_at_free_flow: f64,
39 pub cone_half_angle: f64,
42 pub target_weight: f64,
44 pub ped_strength: f64,
46 pub ped_range: f64,
48 pub wall_strength: f64,
50 pub wall_range: f64,
52 pub arrival_radius: f64,
56}
57
58impl Default for Params {
59 fn default() -> Self {
60 Self {
61 num_candidates: 24,
62 stride_at_free_flow: 0.5,
63 cone_half_angle: std::f64::consts::FRAC_PI_2,
64 target_weight: 1.0,
65 ped_strength: 6.0,
66 ped_range: 0.4,
67 wall_strength: 8.0,
68 wall_range: 0.3,
69 arrival_radius: 0.3,
70 }
71 }
72}
73
74impl Params {
75 pub fn validate(&self, dt: f64) -> Result<(), crate::error::CrowdError> {
81 use crate::error::{require_count, require_dt, require_nonneg, require_positive};
82 const M: &str = "OptimalSteps";
83 require_dt(M, dt)?;
84 require_count(M, "num_candidates", self.num_candidates)?;
85 require_positive(M, "stride_at_free_flow", self.stride_at_free_flow)?;
86 require_positive(M, "cone_half_angle", self.cone_half_angle)?;
87 require_nonneg(M, "target_weight", self.target_weight)?;
88 require_nonneg(M, "ped_strength", self.ped_strength)?;
89 require_positive(M, "ped_range", self.ped_range)?;
90 require_nonneg(M, "wall_strength", self.wall_strength)?;
91 require_positive(M, "wall_range", self.wall_range)?;
92 require_nonneg(M, "arrival_radius", self.arrival_radius)?;
93 Ok(())
94 }
95}
96
97#[derive(Debug, Clone, Copy, Default)]
99pub struct OptimalSteps;
100
101impl PedestrianModel for OptimalSteps {
102 type Params = Params;
103
104 fn name(&self) -> &'static str {
105 "Optimal Steps"
106 }
107
108 fn step(&self, peds: &mut [Pedestrian], walls: &[WallSegment], params: &Params, dt: f64) {
109 #[allow(deprecated)]
110 step(peds, walls, params, dt);
111 }
112}
113
114#[deprecated(
123 since = "0.0.3",
124 note = "O(n²) reference path with per-tick heap allocation; use \
125 `step_scratch` (zero-alloc) or `step_with_grid` (broadphase) \
126 instead. See docs/rustsim-crowd.md P1-7."
127)]
128#[allow(clippy::needless_range_loop)]
129pub fn step(peds: &mut [Pedestrian], walls: &[WallSegment], params: &Params, dt: f64) {
130 let n = peds.len();
131 let mut new_pos = vec![[0.0f64; 2]; n];
132
133 for i in 0..n {
134 let p = &peds[i];
135 let stride = stride_length(p, params, dt);
136 new_pos[i] = best_candidate(i, peds, walls, params, stride);
137 }
138
139 for (p, np) in peds.iter_mut().zip(new_pos.iter()) {
140 let delta = sub(*np, p.pos);
141 if dt > 0.0 {
143 p.vel = scale(delta, 1.0 / dt);
144 }
145 p.pos = *np;
146 }
147}
148
149#[inline]
158pub fn neighbor_cutoff(params: &Params) -> f64 {
159 8.0 * params.ped_range + params.stride_at_free_flow + 1.0
160}
161
162#[allow(clippy::needless_range_loop)]
165pub fn step_with_grid(
166 peds: &mut [Pedestrian],
167 walls: &[WallSegment],
168 params: &Params,
169 dt: f64,
170 grid: &NeighborGrid,
171) {
172 let n = peds.len();
173 let cutoff = neighbor_cutoff(params);
174 let mut new_pos = vec![[0.0f64; 2]; n];
175
176 for i in 0..n {
177 let p = &peds[i];
178 let stride = stride_length(p, params, dt);
179 new_pos[i] = best_candidate_grid(i, peds, walls, params, stride, grid, cutoff);
180 }
181
182 for (p, np) in peds.iter_mut().zip(new_pos.iter()) {
183 let delta = sub(*np, p.pos);
184 if dt > 0.0 {
185 p.vel = scale(delta, 1.0 / dt);
186 }
187 p.pos = *np;
188 }
189}
190
191#[allow(clippy::needless_range_loop)]
195pub fn step_scratch(
196 peds: &mut [Pedestrian],
197 walls: &[WallSegment],
198 params: &Params,
199 dt: f64,
200 scratch: &mut Scratch,
201) {
202 let n = peds.len();
203 let cutoff = neighbor_cutoff(params);
204 scratch.prepare(peds);
205 let (new_pos, grid) = scratch.split();
206
207 for i in 0..n {
208 let p = &peds[i];
209 let stride = stride_length(p, params, dt);
210 new_pos[i] = best_candidate_grid(i, peds, walls, params, stride, grid, cutoff);
211 }
212
213 for (p, np) in peds.iter_mut().zip(new_pos.iter()) {
214 let delta = sub(*np, p.pos);
215 if dt > 0.0 {
216 p.vel = scale(delta, 1.0 / dt);
217 }
218 p.pos = *np;
219 }
220}
221
222#[cfg(feature = "rayon")]
229#[allow(clippy::needless_range_loop)]
230pub fn step_scratch_par(
231 peds: &mut [Pedestrian],
232 walls: &[WallSegment],
233 params: &Params,
234 dt: f64,
235 scratch: &mut Scratch,
236) {
237 use rayon::prelude::*;
238
239 let cutoff = neighbor_cutoff(params);
240 scratch.prepare(peds);
241 let (new_pos, grid) = scratch.split();
242 let peds_ro: &[Pedestrian] = peds;
243
244 new_pos.par_iter_mut().enumerate().for_each(|(i, p_slot)| {
245 let p = &peds_ro[i];
246 let stride = stride_length(p, params, dt);
247 *p_slot = best_candidate_grid(i, peds_ro, walls, params, stride, grid, cutoff);
248 });
249
250 for (p, np) in peds.iter_mut().zip(new_pos.iter()) {
251 let delta = sub(*np, p.pos);
252 if dt > 0.0 {
253 p.vel = scale(delta, 1.0 / dt);
254 }
255 p.pos = *np;
256 }
257}
258
259fn utility_grid(
261 candidate: Vec2,
262 self_idx: usize,
263 peds: &[Pedestrian],
264 walls: &[WallSegment],
265 params: &Params,
266 grid: &NeighborGrid,
267 cutoff: f64,
268) -> f64 {
269 let p = &peds[self_idx];
270 let to_target = norm(sub(p.destination, candidate));
271 let mut score = params.target_weight * to_target;
272
273 grid.for_each_neighbor(self_idx, cutoff, peds, |_j, q| {
274 let d = norm(sub(candidate, q.pos));
275 let clearance = (d - (p.radius + q.radius)).max(0.0);
276 score += params.ped_strength * (-clearance / params.ped_range).exp();
277 });
278
279 for w in walls {
280 let closest = closest_point_on_segment(candidate, w.a, w.b);
281 let d = norm(sub(candidate, closest));
282 let clearance = (d - p.radius).max(0.0);
283 score += params.wall_strength * (-clearance / params.wall_range).exp();
284 }
285
286 score
287}
288
289fn best_candidate_grid(
291 self_idx: usize,
292 peds: &[Pedestrian],
293 walls: &[WallSegment],
294 params: &Params,
295 stride: f64,
296 grid: &NeighborGrid,
297 cutoff: f64,
298) -> Vec2 {
299 let p = &peds[self_idx];
300 let mut best_pos = p.pos;
301 let mut best_score = utility_grid(p.pos, self_idx, peds, walls, params, grid, cutoff);
302
303 let e_dest = p.desired_direction();
304 let cos_cutoff = params.cone_half_angle.cos();
305 let dest_present = e_dest != [0.0, 0.0];
306
307 for offset in candidate_offsets(stride, params.num_candidates) {
308 if dest_present {
309 let dir = normalize(offset);
310 if dir[0] * e_dest[0] + dir[1] * e_dest[1] < cos_cutoff {
311 continue;
312 }
313 }
314 let candidate = add(p.pos, offset);
315 let score = utility_grid(candidate, self_idx, peds, walls, params, grid, cutoff);
316 if score < best_score {
317 best_score = score;
318 best_pos = candidate;
319 }
320 }
321
322 best_pos
323}
324
325#[inline]
327pub fn stride_length(p: &Pedestrian, params: &Params, dt: f64) -> f64 {
328 let tapered = p.effective_desired_speed(params.arrival_radius);
333 let intended = tapered.max(p.speed());
334 let stride_free = params.stride_at_free_flow.max(1e-3);
335 let stride = stride_free * (intended / p.desired_speed.max(1e-6));
336 stride.min(intended * dt)
338}
339
340pub fn candidate_offsets(stride: f64, num: usize) -> Vec<Vec2> {
343 let mut out = Vec::with_capacity(num);
344 if num == 0 || stride <= 0.0 {
345 return out;
346 }
347 let two_pi = std::f64::consts::TAU;
348 for k in 0..num {
349 let theta = (k as f64) * two_pi / (num as f64);
350 out.push([stride * theta.cos(), stride * theta.sin()]);
351 }
352 out
353}
354
355pub fn utility(
357 candidate: Vec2,
358 self_idx: usize,
359 peds: &[Pedestrian],
360 walls: &[WallSegment],
361 params: &Params,
362) -> f64 {
363 let p = &peds[self_idx];
364 let to_target = norm(sub(p.destination, candidate));
365 let mut score = params.target_weight * to_target;
366
367 for (j, q) in peds.iter().enumerate() {
368 if j == self_idx {
369 continue;
370 }
371 let d = norm(sub(candidate, q.pos));
372 let clearance = (d - (p.radius + q.radius)).max(0.0);
373 score += params.ped_strength * (-clearance / params.ped_range).exp();
374 }
375
376 for w in walls {
377 let closest = closest_point_on_segment(candidate, w.a, w.b);
378 let d = norm(sub(candidate, closest));
379 let clearance = (d - p.radius).max(0.0);
380 score += params.wall_strength * (-clearance / params.wall_range).exp();
381 }
382
383 score
384}
385
386pub fn best_candidate(
388 self_idx: usize,
389 peds: &[Pedestrian],
390 walls: &[WallSegment],
391 params: &Params,
392 stride: f64,
393) -> Vec2 {
394 let p = &peds[self_idx];
395 let mut best_pos = p.pos;
398 let mut best_score = utility(p.pos, self_idx, peds, walls, params);
399
400 let e_dest = p.desired_direction();
401 let cos_cutoff = params.cone_half_angle.cos();
402 let dest_present = e_dest != [0.0, 0.0];
403
404 for offset in candidate_offsets(stride, params.num_candidates) {
405 if dest_present {
407 let dir = normalize(offset);
408 if dir[0] * e_dest[0] + dir[1] * e_dest[1] < cos_cutoff {
409 continue;
410 }
411 }
412 let candidate = add(p.pos, offset);
413 let score = utility(candidate, self_idx, peds, walls, params);
414 if score < best_score {
415 best_score = score;
416 best_pos = candidate;
417 }
418 }
419
420 best_pos
421}
422
423#[cfg(test)]
424#[allow(deprecated)] mod tests {
426 use super::*;
427
428 #[test]
429 fn single_agent_advances_toward_goal() {
430 let mut peds = vec![Pedestrian {
431 pos: [0.0, 0.0],
432 vel: [0.0, 0.0],
433 radius: 0.2,
434 desired_speed: 1.34,
435 destination: [10.0, 0.0],
436 }];
437 let start = peds[0].pos[0];
438 for _ in 0..40 {
439 step(&mut peds, &[], &Params::default(), 0.4);
440 }
441 assert!(peds[0].pos[0] > start + 1.0);
442 assert!(peds[0].pos[1].abs() < 0.2);
443 }
444
445 #[test]
446 fn wall_repels_in_utility() {
447 let peds = vec![Pedestrian {
448 pos: [0.0, 0.0],
449 vel: [0.0, 0.0],
450 radius: 0.2,
451 desired_speed: 1.0,
452 destination: [5.0, 0.0],
453 }];
454 let walls = vec![WallSegment {
455 a: [1.0, -0.1],
456 b: [1.0, 0.1],
457 }];
458 let params = Params::default();
459 let near = utility([0.9, 0.0], 0, &peds, &walls, ¶ms);
460 let far = utility([0.0, 0.0], 0, &peds, &walls, ¶ms);
461 assert!(near > far, "wall penalty should make near-wall worse");
462 }
463
464 #[test]
465 fn stationary_agent_at_destination_does_not_drift() {
466 let mut peds = vec![Pedestrian {
467 pos: [1.0, 1.0],
468 vel: [0.0, 0.0],
469 radius: 0.2,
470 desired_speed: 1.0,
471 destination: [1.0, 1.0],
472 }];
473 for _ in 0..10 {
474 step(&mut peds, &[], &Params::default(), 0.4);
475 }
476 let d = norm(sub(peds[0].pos, [1.0, 1.0]));
477 assert!(d < 0.05);
478 }
479}