1use crate::broadphase::{NeighborGrid, Scratch};
23use crate::common::{
24 add, closest_point_on_segment, dot, norm, scale, sub, Pedestrian, PedestrianModel, Vec2,
25 WallSegment,
26};
27
28#[derive(Debug, Clone, Copy, PartialEq)]
30pub struct Params {
31 pub time_gap: f64,
33 pub anticipation_time: f64,
35 pub num_directions: usize,
37 pub cone_half_angle: f64,
39 pub deviation_weight: f64,
41 pub wall_range: f64,
53 pub arrival_radius: f64,
57}
58
59impl Default for Params {
60 fn default() -> Self {
61 Self {
62 time_gap: 1.06,
63 anticipation_time: 1.0,
64 num_directions: 13,
65 cone_half_angle: std::f64::consts::FRAC_PI_2,
66 deviation_weight: 0.5,
67 wall_range: 0.08,
68 arrival_radius: 0.3,
69 }
70 }
71}
72
73impl Params {
74 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 = "AnticipationVelocity";
83 require_dt(M, dt)?;
84 require_positive(M, "time_gap", self.time_gap)?;
85 require_positive(M, "anticipation_time", self.anticipation_time)?;
86 require_count(M, "num_directions", self.num_directions)?;
87 require_positive(M, "cone_half_angle", self.cone_half_angle)?;
88 require_nonneg(M, "deviation_weight", self.deviation_weight)?;
89 require_positive(M, "wall_range", self.wall_range)?;
90 require_nonneg(M, "arrival_radius", self.arrival_radius)?;
91 Ok(())
92 }
93}
94
95#[derive(Debug, Clone, Copy, Default)]
98pub struct AnticipationVelocity;
99
100impl PedestrianModel for AnticipationVelocity {
101 type Params = Params;
102
103 fn name(&self) -> &'static str {
104 "Anticipation Velocity"
105 }
106
107 fn step(&self, peds: &mut [Pedestrian], walls: &[WallSegment], params: &Params, dt: f64) {
108 #[allow(deprecated)]
109 step(peds, walls, params, dt);
110 }
111}
112
113#[deprecated(
119 since = "0.0.3",
120 note = "O(n²) reference path with per-tick heap allocation; use \
121 `step_scratch` (zero-alloc) or `step_with_grid` (broadphase) \
122 instead. See docs/rustsim-crowd.md P1-7."
123)]
124#[allow(clippy::needless_range_loop)]
125pub fn step(peds: &mut [Pedestrian], walls: &[WallSegment], params: &Params, dt: f64) {
126 let n = peds.len();
127 let mut new_vel = vec![[0.0f64; 2]; n];
128
129 for i in 0..n {
130 let (e, s) = best_direction_and_headroom(i, peds, walls, params);
131 let speed_cap = ((s - peds[i].radius) / params.time_gap).max(0.0);
132 let v = peds[i]
133 .effective_desired_speed(params.arrival_radius)
134 .min(speed_cap);
135 new_vel[i] = scale(e, v);
136 }
137
138 for (p, v) in peds.iter_mut().zip(new_vel.iter()) {
139 p.vel = *v;
140 p.pos = add(p.pos, scale(p.vel, dt));
141 }
142}
143
144#[inline]
153pub fn neighbor_cutoff(params: &Params) -> f64 {
154 params.anticipation_time * 2.5 + 1.5
155}
156
157#[allow(clippy::needless_range_loop)]
160pub fn step_with_grid(
161 peds: &mut [Pedestrian],
162 walls: &[WallSegment],
163 params: &Params,
164 dt: f64,
165 grid: &NeighborGrid,
166) {
167 let n = peds.len();
168 let cutoff = neighbor_cutoff(params);
169 let mut new_vel = vec![[0.0f64; 2]; n];
170
171 for i in 0..n {
172 let (e, s) = best_direction_and_headroom_grid(i, peds, walls, params, grid, cutoff);
173 let speed_cap = ((s - peds[i].radius) / params.time_gap).max(0.0);
174 let v = peds[i]
175 .effective_desired_speed(params.arrival_radius)
176 .min(speed_cap);
177 new_vel[i] = scale(e, v);
178 }
179
180 for (p, v) in peds.iter_mut().zip(new_vel.iter()) {
181 p.vel = *v;
182 p.pos = add(p.pos, scale(p.vel, dt));
183 }
184}
185
186#[allow(clippy::needless_range_loop)]
190pub fn step_scratch(
191 peds: &mut [Pedestrian],
192 walls: &[WallSegment],
193 params: &Params,
194 dt: f64,
195 scratch: &mut Scratch,
196) {
197 let n = peds.len();
198 let cutoff = neighbor_cutoff(params);
199 scratch.prepare(peds);
200 let (new_vel, grid) = scratch.split();
201
202 for i in 0..n {
203 let (e, s) = best_direction_and_headroom_grid(i, peds, walls, params, grid, cutoff);
204 let speed_cap = ((s - peds[i].radius) / params.time_gap).max(0.0);
205 let v = peds[i]
206 .effective_desired_speed(params.arrival_radius)
207 .min(speed_cap);
208 new_vel[i] = scale(e, v);
209 }
210
211 for (p, v) in peds.iter_mut().zip(new_vel.iter()) {
212 p.vel = *v;
213 p.pos = add(p.pos, scale(p.vel, dt));
214 }
215}
216
217#[cfg(feature = "simd")]
229#[allow(clippy::needless_range_loop)]
230pub fn step_scratch_simd(
231 peds: &mut [Pedestrian],
232 walls: &[WallSegment],
233 params: &Params,
234 dt: f64,
235 scratch: &mut Scratch,
236) {
237 let n = peds.len();
238 let cutoff = neighbor_cutoff(params);
239 scratch.prepare(peds);
240 let (new_vel, grid) = scratch.split();
241
242 for i in 0..n {
243 let (e, s) = best_direction_and_headroom_grid_simd(i, peds, walls, params, grid, cutoff);
244 let speed_cap = ((s - peds[i].radius) / params.time_gap).max(0.0);
245 let v = peds[i]
246 .effective_desired_speed(params.arrival_radius)
247 .min(speed_cap);
248 new_vel[i] = scale(e, v);
249 }
250
251 for (p, v) in peds.iter_mut().zip(new_vel.iter()) {
252 p.vel = *v;
253 p.pos = add(p.pos, scale(p.vel, dt));
254 }
255}
256
257#[cfg(feature = "rayon")]
263#[allow(clippy::needless_range_loop)]
264pub fn step_scratch_par(
265 peds: &mut [Pedestrian],
266 walls: &[WallSegment],
267 params: &Params,
268 dt: f64,
269 scratch: &mut Scratch,
270) {
271 use rayon::prelude::*;
272
273 let cutoff = neighbor_cutoff(params);
274 scratch.prepare(peds);
275 let (new_vel, grid) = scratch.split();
276 let peds_ro: &[Pedestrian] = peds;
277
278 new_vel.par_iter_mut().enumerate().for_each(|(i, v_slot)| {
279 let (e, s) = best_direction_and_headroom_grid(i, peds_ro, walls, params, grid, cutoff);
280 let speed_cap = ((s - peds_ro[i].radius) / params.time_gap).max(0.0);
281 let v = peds_ro[i]
282 .effective_desired_speed(params.arrival_radius)
283 .min(speed_cap);
284 *v_slot = scale(e, v);
285 });
286
287 for (p, v) in peds.iter_mut().zip(new_vel.iter()) {
288 p.vel = *v;
289 p.pos = add(p.pos, scale(p.vel, dt));
290 }
291}
292
293fn best_direction_and_headroom_grid(
295 i: usize,
296 peds: &[Pedestrian],
297 walls: &[WallSegment],
298 params: &Params,
299 grid: &NeighborGrid,
300 cutoff: f64,
301) -> (Vec2, f64) {
302 let p = &peds[i];
303 let e_dest = p.desired_direction();
304 if e_dest == [0.0, 0.0] {
305 return ([0.0, 0.0], 0.0);
306 }
307
308 let base_angle = e_dest[1].atan2(e_dest[0]);
309 let m = params.num_directions.max(1);
310 let half = params.cone_half_angle;
311
312 let mut best_dir = e_dest;
313 let mut best_headroom =
314 anticipated_headroom_grid(i, peds, walls, &e_dest, params, grid, cutoff);
315 let mut best_score = best_headroom;
316
317 for k in 0..m {
318 let t = if m == 1 {
319 0.0
320 } else {
321 -1.0 + 2.0 * (k as f64) / ((m - 1) as f64)
322 };
323 let theta = base_angle + t * half;
324 let dir = [theta.cos(), theta.sin()];
325 let headroom = anticipated_headroom_grid(i, peds, walls, &dir, params, grid, cutoff);
326 let deviation = params.deviation_weight * t.abs() * half;
327 let score = headroom - deviation;
328 if score > best_score {
329 best_score = score;
330 best_headroom = headroom;
331 best_dir = dir;
332 }
333 }
334
335 (best_dir, best_headroom)
336}
337
338#[cfg(feature = "simd")]
339fn best_direction_and_headroom_grid_simd(
340 i: usize,
341 peds: &[Pedestrian],
342 walls: &[WallSegment],
343 params: &Params,
344 grid: &NeighborGrid,
345 cutoff: f64,
346) -> (Vec2, f64) {
347 let p = &peds[i];
348 let e_dest = p.desired_direction();
349 if e_dest == [0.0, 0.0] {
350 return ([0.0, 0.0], 0.0);
351 }
352
353 let base_angle = e_dest[1].atan2(e_dest[0]);
354 let m = params.num_directions.max(1);
355 let half = params.cone_half_angle;
356
357 let mut best_dir = e_dest;
358 let mut best_headroom =
359 anticipated_headroom_grid_simd(i, peds, walls, &e_dest, params, grid, cutoff);
360 let mut best_score = best_headroom;
361
362 for k in 0..m {
363 let t = if m == 1 {
364 0.0
365 } else {
366 -1.0 + 2.0 * (k as f64) / ((m - 1) as f64)
367 };
368 let theta = base_angle + t * half;
369 let dir = [theta.cos(), theta.sin()];
370 let headroom = anticipated_headroom_grid_simd(i, peds, walls, &dir, params, grid, cutoff);
371 let deviation = params.deviation_weight * t.abs() * half;
372 let score = headroom - deviation;
373 if score > best_score {
374 best_score = score;
375 best_headroom = headroom;
376 best_dir = dir;
377 }
378 }
379
380 (best_dir, best_headroom)
381}
382
383fn anticipated_headroom_grid(
385 i: usize,
386 peds: &[Pedestrian],
387 walls: &[WallSegment],
388 e: &Vec2,
389 params: &Params,
390 grid: &NeighborGrid,
391 cutoff: f64,
392) -> f64 {
393 let p = &peds[i];
394 let mut s = f64::INFINITY;
395
396 grid.for_each_neighbor(i, cutoff, peds, |_j, q| {
397 let q_future = add(q.pos, scale(q.vel, params.anticipation_time));
398 let rel = sub(q_future, p.pos);
399 let forward = dot(rel, *e);
400 if forward <= 0.0 {
401 return;
402 }
403 let proj = scale(*e, forward);
404 let lat = norm(sub(rel, proj));
405 if lat > p.radius + q.radius {
406 return;
407 }
408 let d = norm(rel);
409 if d < s {
410 s = d;
411 }
412 });
413
414 for w in walls {
415 let probe = add(p.pos, scale(*e, p.desired_speed * params.anticipation_time));
418 let closest = closest_point_on_segment(probe, w.a, w.b);
419 let rel = sub(closest, p.pos);
420 let forward = dot(rel, *e);
421 if forward <= 0.0 {
422 continue;
423 }
424 let proj = scale(*e, forward);
425 let lat = norm(sub(rel, proj));
426 if lat > p.radius + params.wall_range {
427 continue;
428 }
429 let d = norm(rel);
430 if d < s {
431 s = d;
432 }
433 }
434
435 s
436}
437
438#[cfg(feature = "simd")]
439fn anticipated_headroom_grid_simd(
440 i: usize,
441 peds: &[Pedestrian],
442 walls: &[WallSegment],
443 e: &Vec2,
444 params: &Params,
445 grid: &NeighborGrid,
446 cutoff: f64,
447) -> f64 {
448 let p = &peds[i];
449 let mut s = f64::INFINITY;
450 let mut idxs: [Option<usize>; 4] = [None, None, None, None];
451 let mut filled: usize = 0;
452 grid.for_each_neighbor(i, cutoff, peds, |j, _q| {
453 idxs[filled] = Some(j);
454 filled += 1;
455 if filled == 4 {
456 let buf: [Option<&Pedestrian>; 4] = [
457 Some(&peds[idxs[0].unwrap()]),
458 Some(&peds[idxs[1].unwrap()]),
459 Some(&peds[idxs[2].unwrap()]),
460 Some(&peds[idxs[3].unwrap()]),
461 ];
462 s = s.min(crate::simd::avm_headroom_x4(p, *e, buf, params));
463 idxs = [None, None, None, None];
464 filled = 0;
465 }
466 });
467 if filled > 0 {
468 let buf: [Option<&Pedestrian>; 4] = [
469 idxs[0].map(|k| &peds[k]),
470 idxs[1].map(|k| &peds[k]),
471 idxs[2].map(|k| &peds[k]),
472 idxs[3].map(|k| &peds[k]),
473 ];
474 s = s.min(crate::simd::avm_headroom_x4(p, *e, buf, params));
475 }
476
477 for w in walls {
478 let probe = add(p.pos, scale(*e, p.desired_speed * params.anticipation_time));
479 let closest = closest_point_on_segment(probe, w.a, w.b);
480 let rel = sub(closest, p.pos);
481 let forward = dot(rel, *e);
482 if forward <= 0.0 {
483 continue;
484 }
485 let proj = scale(*e, forward);
486 let lat = norm(sub(rel, proj));
487 if lat > p.radius + params.wall_range {
488 continue;
489 }
490 let d = norm(rel);
491 if d < s {
492 s = d;
493 }
494 }
495
496 s
497}
498
499pub fn best_direction_and_headroom(
501 i: usize,
502 peds: &[Pedestrian],
503 walls: &[WallSegment],
504 params: &Params,
505) -> (Vec2, f64) {
506 let p = &peds[i];
507 let e_dest = p.desired_direction();
508 if e_dest == [0.0, 0.0] {
509 return ([0.0, 0.0], 0.0);
510 }
511
512 let base_angle = e_dest[1].atan2(e_dest[0]);
513 let m = params.num_directions.max(1);
514 let half = params.cone_half_angle;
515
516 let mut best_dir = e_dest;
517 let mut best_headroom = anticipated_headroom(i, peds, walls, &e_dest, params);
518 let mut best_score = best_headroom;
519
520 for k in 0..m {
521 let t = if m == 1 {
522 0.0
523 } else {
524 -1.0 + 2.0 * (k as f64) / ((m - 1) as f64)
525 };
526 let theta = base_angle + t * half;
527 let dir = [theta.cos(), theta.sin()];
528 let headroom = anticipated_headroom(i, peds, walls, &dir, params);
529 let deviation = params.deviation_weight * t.abs() * half;
530 let score = headroom - deviation;
531 if score > best_score {
532 best_score = score;
533 best_headroom = headroom;
534 best_dir = dir;
535 }
536 }
537
538 (best_dir, best_headroom)
539}
540
541pub fn anticipated_headroom(
545 i: usize,
546 peds: &[Pedestrian],
547 walls: &[WallSegment],
548 e: &Vec2,
549 params: &Params,
550) -> f64 {
551 let p = &peds[i];
552 let mut s = f64::INFINITY;
553
554 for (j, q) in peds.iter().enumerate() {
555 if i == j {
556 continue;
557 }
558 let q_future = add(q.pos, scale(q.vel, params.anticipation_time));
560 let rel = sub(q_future, p.pos);
561 let forward = dot(rel, *e);
562 if forward <= 0.0 {
563 continue;
564 }
565 let proj = scale(*e, forward);
566 let lat = norm(sub(rel, proj));
567 if lat > p.radius + q.radius {
568 continue;
569 }
570 let d = norm(rel);
571 if d < s {
572 s = d;
573 }
574 }
575
576 for w in walls {
577 let probe = add(p.pos, scale(*e, p.desired_speed * params.anticipation_time));
583 let closest = closest_point_on_segment(probe, w.a, w.b);
584 let rel = sub(closest, p.pos);
585 let forward = dot(rel, *e);
586 if forward <= 0.0 {
587 continue;
588 }
589 let proj = scale(*e, forward);
590 let lat = norm(sub(rel, proj));
591 if lat > p.radius + params.wall_range {
592 continue;
593 }
594 let d = norm(rel);
595 if d < s {
596 s = d;
597 }
598 }
599
600 s
601}
602
603#[cfg(test)]
604#[allow(deprecated)] mod tests {
606 use super::*;
607
608 #[test]
609 fn single_agent_advances_at_free_flow() {
610 let mut peds = vec![Pedestrian {
611 pos: [0.0, 0.0],
612 vel: [0.0, 0.0],
613 radius: 0.2,
614 desired_speed: 1.34,
615 destination: [20.0, 0.0],
616 }];
617 step(&mut peds, &[], &Params::default(), 0.1);
618 let v = norm(peds[0].vel);
619 assert!((v - 1.34).abs() < 1e-6);
620 }
621
622 #[test]
623 fn deviates_around_static_obstacle() {
624 let mut peds = vec![
626 Pedestrian {
627 pos: [0.0, 0.0],
628 vel: [0.0, 0.0],
629 radius: 0.2,
630 desired_speed: 1.34,
631 destination: [10.0, 0.0],
632 },
633 Pedestrian {
634 pos: [3.0, 0.0],
635 vel: [0.0, 0.0],
636 radius: 0.2,
637 desired_speed: 0.0,
638 destination: [3.0, 0.0],
639 },
640 ];
641 for _ in 0..200 {
642 step(&mut peds, &[], &Params::default(), 0.05);
643 }
644 assert!(peds[0].pos[0] > 3.5);
646 let d = norm(sub(peds[0].pos, peds[1].pos));
647 assert!(d >= peds[0].radius + peds[1].radius - 0.05);
648 }
649
650 #[test]
651 fn two_agents_head_on_do_not_overlap() {
652 let mut peds = vec![
653 Pedestrian {
654 pos: [-4.0, 0.05],
655 vel: [0.0, 0.0],
656 radius: 0.2,
657 desired_speed: 1.34,
658 destination: [4.0, 0.05],
659 },
660 Pedestrian {
661 pos: [4.0, -0.05],
662 vel: [0.0, 0.0],
663 radius: 0.2,
664 desired_speed: 1.34,
665 destination: [-4.0, -0.05],
666 },
667 ];
668 for _ in 0..400 {
669 step(&mut peds, &[], &Params::default(), 0.02);
670 }
671 let d = norm(sub(peds[0].pos, peds[1].pos));
672 assert!(d >= peds[0].radius + peds[1].radius - 0.05);
673 }
674}