1use crate::broadphase::{NeighborGrid, Scratch};
21use crate::common::{
22 add, closest_point_on_segment, dot, norm, normalize, scale, sub, Pedestrian, PedestrianModel,
23 Vec2, WallSegment,
24};
25
26#[derive(Debug, Clone, Copy, PartialEq)]
28pub struct Params {
29 pub time_gap: f64,
31 pub alpha: f64,
33 pub interaction_range: f64,
35 pub interaction_strength: f64,
37 pub wall_range: f64,
39 pub wall_strength: f64,
41 pub arrival_radius: f64,
45}
46
47impl Default for Params {
48 fn default() -> Self {
49 Self {
50 time_gap: 1.06,
51 alpha: 3.0,
52 interaction_range: 0.1,
53 interaction_strength: 5.0,
54 wall_range: 0.08,
55 wall_strength: 5.0,
56 arrival_radius: 0.3,
57 }
58 }
59}
60
61impl Params {
62 pub fn validate(&self, dt: f64) -> Result<(), crate::error::CrowdError> {
69 use crate::error::{require_dt, require_nonneg, require_positive};
70 const M: &str = "CollisionFreeSpeed";
71 require_dt(M, dt)?;
72 require_positive(M, "time_gap", self.time_gap)?;
73 require_positive(M, "alpha", self.alpha)?;
74 require_positive(M, "interaction_range", self.interaction_range)?;
75 require_nonneg(M, "interaction_strength", self.interaction_strength)?;
76 require_positive(M, "wall_range", self.wall_range)?;
77 require_nonneg(M, "wall_strength", self.wall_strength)?;
78 require_nonneg(M, "arrival_radius", self.arrival_radius)?;
79 Ok(())
80 }
81}
82
83#[derive(Debug, Clone, Copy, Default)]
86pub struct CollisionFreeSpeed;
87
88impl PedestrianModel for CollisionFreeSpeed {
89 type Params = Params;
90
91 fn name(&self) -> &'static str {
92 "Collision-Free Speed"
93 }
94
95 fn step(&self, peds: &mut [Pedestrian], walls: &[WallSegment], params: &Params, dt: f64) {
96 #[allow(deprecated)]
97 step(peds, walls, params, dt);
98 }
99}
100
101#[deprecated(
107 since = "0.0.3",
108 note = "O(n²) reference path with per-tick heap allocation; use \
109 `step_scratch` (zero-alloc) or `step_with_grid` (broadphase) \
110 instead. See docs/rustsim-crowd.md P1-7."
111)]
112#[allow(clippy::needless_range_loop)]
113pub fn step(peds: &mut [Pedestrian], walls: &[WallSegment], params: &Params, dt: f64) {
114 let n = peds.len();
115 let mut new_vel = vec![[0.0f64; 2]; n];
116
117 for i in 0..n {
118 let p = &peds[i];
119 let e = chosen_direction(i, peds, walls, params);
120 let s = free_headroom(i, peds, &e);
121 let effective_radius = p.radius;
122 let speed_cap = ((s - effective_radius) / params.time_gap).max(0.0);
123 let v = p
124 .effective_desired_speed(params.arrival_radius)
125 .min(speed_cap);
126 new_vel[i] = scale(e, v);
127 }
128
129 for (p, v) in peds.iter_mut().zip(new_vel.iter()) {
130 p.vel = *v;
131 p.pos = add(p.pos, scale(p.vel, dt));
132 }
133}
134
135#[inline]
143pub fn neighbor_cutoff(params: &Params) -> f64 {
144 8.0 * params.interaction_range + 2.0
145}
146
147#[allow(clippy::needless_range_loop)]
152pub fn step_with_grid(
153 peds: &mut [Pedestrian],
154 walls: &[WallSegment],
155 params: &Params,
156 dt: f64,
157 grid: &NeighborGrid,
158) {
159 let n = peds.len();
160 let cutoff = neighbor_cutoff(params);
161 let mut new_vel = vec![[0.0f64; 2]; n];
162
163 for i in 0..n {
164 let p = &peds[i];
165 let e = chosen_direction_grid(i, peds, walls, params, grid, cutoff);
166 let s = free_headroom_grid(i, peds, &e, grid, cutoff);
167 let effective_radius = p.radius;
168 let speed_cap = ((s - effective_radius) / params.time_gap).max(0.0);
169 let v = p
170 .effective_desired_speed(params.arrival_radius)
171 .min(speed_cap);
172 new_vel[i] = scale(e, v);
173 }
174
175 for (p, v) in peds.iter_mut().zip(new_vel.iter()) {
176 p.vel = *v;
177 p.pos = add(p.pos, scale(p.vel, dt));
178 }
179}
180
181#[allow(clippy::needless_range_loop)]
185pub fn step_scratch(
186 peds: &mut [Pedestrian],
187 walls: &[WallSegment],
188 params: &Params,
189 dt: f64,
190 scratch: &mut Scratch,
191) {
192 let n = peds.len();
193 let cutoff = neighbor_cutoff(params);
194 scratch.prepare(peds);
195 let (new_vel, grid) = scratch.split();
196
197 for i in 0..n {
198 let p = &peds[i];
199 let e = chosen_direction_grid(i, peds, walls, params, grid, cutoff);
200 let s = free_headroom_grid(i, peds, &e, grid, cutoff);
201 let speed_cap = ((s - p.radius) / params.time_gap).max(0.0);
202 let v = p
203 .effective_desired_speed(params.arrival_radius)
204 .min(speed_cap);
205 new_vel[i] = scale(e, v);
206 }
207
208 for (p, v) in peds.iter_mut().zip(new_vel.iter()) {
209 p.vel = *v;
210 p.pos = add(p.pos, scale(p.vel, dt));
211 }
212}
213
214#[cfg(feature = "simd")]
226#[allow(clippy::needless_range_loop)]
227pub fn step_scratch_simd(
228 peds: &mut [Pedestrian],
229 walls: &[WallSegment],
230 params: &Params,
231 dt: f64,
232 scratch: &mut Scratch,
233) {
234 let n = peds.len();
235 let cutoff = neighbor_cutoff(params);
236 scratch.prepare(peds);
237 let (new_vel, grid) = scratch.split();
238
239 for i in 0..n {
240 let p = &peds[i];
241 let e = chosen_direction_grid_simd(i, peds, walls, params, grid, cutoff);
242 let s = free_headroom_grid_simd(i, peds, &e, grid, cutoff);
243 let speed_cap = ((s - p.radius) / params.time_gap).max(0.0);
244 let v = p
245 .effective_desired_speed(params.arrival_radius)
246 .min(speed_cap);
247 new_vel[i] = scale(e, v);
248 }
249
250 for (p, v) in peds.iter_mut().zip(new_vel.iter()) {
251 p.vel = *v;
252 p.pos = add(p.pos, scale(p.vel, dt));
253 }
254}
255
256#[cfg(feature = "rayon")]
262#[allow(clippy::needless_range_loop)]
263pub fn step_scratch_par(
264 peds: &mut [Pedestrian],
265 walls: &[WallSegment],
266 params: &Params,
267 dt: f64,
268 scratch: &mut Scratch,
269) {
270 use rayon::prelude::*;
271
272 let cutoff = neighbor_cutoff(params);
273 scratch.prepare(peds);
274 let (new_vel, grid) = scratch.split();
275 let peds_ro: &[Pedestrian] = peds;
276
277 new_vel.par_iter_mut().enumerate().for_each(|(i, v_slot)| {
278 let p = &peds_ro[i];
279 let e = chosen_direction_grid(i, peds_ro, walls, params, grid, cutoff);
280 let s = free_headroom_grid(i, peds_ro, &e, grid, cutoff);
281 let speed_cap = ((s - p.radius) / params.time_gap).max(0.0);
282 let v = p
283 .effective_desired_speed(params.arrival_radius)
284 .min(speed_cap);
285 *v_slot = scale(e, v);
286 });
287
288 for (p, v) in peds.iter_mut().zip(new_vel.iter()) {
289 p.vel = *v;
290 p.pos = add(p.pos, scale(p.vel, dt));
291 }
292}
293
294fn chosen_direction_grid(
296 i: usize,
297 peds: &[Pedestrian],
298 walls: &[WallSegment],
299 params: &Params,
300 grid: &NeighborGrid,
301 cutoff: f64,
302) -> Vec2 {
303 let p = &peds[i];
304 let e_dest = p.desired_direction();
305 let mut acc = scale(e_dest, params.alpha);
306
307 grid.for_each_neighbor(i, cutoff, peds, |_j, q| {
308 let diff = sub(p.pos, q.pos);
309 let d = norm(diff);
310 if d < 1e-9 {
311 return;
312 }
313 let e_ij = scale(diff, 1.0 / d);
314 let clearance = d - (p.radius + q.radius);
315 let weight =
316 params.interaction_strength * (-clearance.max(0.0) / params.interaction_range).exp();
317 acc = add(acc, scale(e_ij, weight));
318 });
319
320 for w in walls {
321 let closest = closest_point_on_segment(p.pos, w.a, w.b);
322 let diff = sub(p.pos, closest);
323 let d = norm(diff);
324 if d < 1e-9 {
325 continue;
326 }
327 let e_iw = scale(diff, 1.0 / d);
328 let clearance = d - p.radius;
329 let weight = params.wall_strength * (-clearance.max(0.0) / params.wall_range).exp();
330 acc = add(acc, scale(e_iw, weight));
331 }
332
333 normalize(acc)
334}
335
336#[cfg(feature = "simd")]
337fn chosen_direction_grid_simd(
338 i: usize,
339 peds: &[Pedestrian],
340 walls: &[WallSegment],
341 params: &Params,
342 grid: &NeighborGrid,
343 cutoff: f64,
344) -> Vec2 {
345 let p = &peds[i];
346 let e_dest = p.desired_direction();
347 let mut acc = scale(e_dest, params.alpha);
348
349 let mut idxs: [Option<usize>; 4] = [None, None, None, None];
350 let mut filled: usize = 0;
351 grid.for_each_neighbor(i, cutoff, peds, |j, _q| {
352 idxs[filled] = Some(j);
353 filled += 1;
354 if filled == 4 {
355 let buf: [Option<&Pedestrian>; 4] = [
356 Some(&peds[idxs[0].unwrap()]),
357 Some(&peds[idxs[1].unwrap()]),
358 Some(&peds[idxs[2].unwrap()]),
359 Some(&peds[idxs[3].unwrap()]),
360 ];
361 acc = add(acc, crate::simd::cfs_direction_x4(p, buf, params));
362 idxs = [None, None, None, None];
363 filled = 0;
364 }
365 });
366 if filled > 0 {
367 let buf: [Option<&Pedestrian>; 4] = [
368 idxs[0].map(|k| &peds[k]),
369 idxs[1].map(|k| &peds[k]),
370 idxs[2].map(|k| &peds[k]),
371 idxs[3].map(|k| &peds[k]),
372 ];
373 acc = add(acc, crate::simd::cfs_direction_x4(p, buf, params));
374 }
375
376 for w in walls {
377 let closest = closest_point_on_segment(p.pos, w.a, w.b);
378 let diff = sub(p.pos, closest);
379 let d = norm(diff);
380 if d < 1e-9 {
381 continue;
382 }
383 let e_iw = scale(diff, 1.0 / d);
384 let clearance = d - p.radius;
385 let weight = params.wall_strength * (-clearance.max(0.0) / params.wall_range).exp();
386 acc = add(acc, scale(e_iw, weight));
387 }
388
389 normalize(acc)
390}
391
392fn free_headroom_grid(
394 i: usize,
395 peds: &[Pedestrian],
396 e: &Vec2,
397 grid: &NeighborGrid,
398 cutoff: f64,
399) -> f64 {
400 let p = &peds[i];
401 let mut s = f64::INFINITY;
402 grid.for_each_neighbor(i, cutoff, peds, |_j, q| {
403 let rel = sub(q.pos, p.pos);
404 let forward = dot(rel, *e);
405 if forward <= 0.0 {
406 return;
407 }
408 let proj = scale(*e, forward);
409 let lat = norm(sub(rel, proj));
410 if lat > p.radius + q.radius {
411 return;
412 }
413 let d = norm(rel);
414 if d < s {
415 s = d;
416 }
417 });
418 s
419}
420
421#[cfg(feature = "simd")]
422fn free_headroom_grid_simd(
423 i: usize,
424 peds: &[Pedestrian],
425 e: &Vec2,
426 grid: &NeighborGrid,
427 cutoff: f64,
428) -> f64 {
429 let p = &peds[i];
430 let mut s = f64::INFINITY;
431 let mut idxs: [Option<usize>; 4] = [None, None, None, None];
432 let mut filled: usize = 0;
433 grid.for_each_neighbor(i, cutoff, peds, |j, _q| {
434 idxs[filled] = Some(j);
435 filled += 1;
436 if filled == 4 {
437 let buf: [Option<&Pedestrian>; 4] = [
438 Some(&peds[idxs[0].unwrap()]),
439 Some(&peds[idxs[1].unwrap()]),
440 Some(&peds[idxs[2].unwrap()]),
441 Some(&peds[idxs[3].unwrap()]),
442 ];
443 s = s.min(crate::simd::cfs_headroom_x4(p, *e, buf));
444 idxs = [None, None, None, None];
445 filled = 0;
446 }
447 });
448 if filled > 0 {
449 let buf: [Option<&Pedestrian>; 4] = [
450 idxs[0].map(|k| &peds[k]),
451 idxs[1].map(|k| &peds[k]),
452 idxs[2].map(|k| &peds[k]),
453 idxs[3].map(|k| &peds[k]),
454 ];
455 s = s.min(crate::simd::cfs_headroom_x4(p, *e, buf));
456 }
457 s
458}
459
460#[inline]
462pub fn chosen_direction(
463 i: usize,
464 peds: &[Pedestrian],
465 walls: &[WallSegment],
466 params: &Params,
467) -> Vec2 {
468 let p = &peds[i];
469 let e_dest = p.desired_direction();
470 let mut acc = scale(e_dest, params.alpha);
471
472 for (j, q) in peds.iter().enumerate() {
473 if i == j {
474 continue;
475 }
476 let diff = sub(p.pos, q.pos);
477 let d = norm(diff);
478 if d < 1e-9 {
479 continue;
480 }
481 let e_ij = scale(diff, 1.0 / d);
482 let clearance = d - (p.radius + q.radius);
483 let weight =
484 params.interaction_strength * (-clearance.max(0.0) / params.interaction_range).exp();
485 acc = add(acc, scale(e_ij, weight));
486 }
487
488 for w in walls {
489 let closest = closest_point_on_segment(p.pos, w.a, w.b);
490 let diff = sub(p.pos, closest);
491 let d = norm(diff);
492 if d < 1e-9 {
493 continue;
494 }
495 let e_iw = scale(diff, 1.0 / d);
496 let clearance = d - p.radius;
497 let weight = params.wall_strength * (-clearance.max(0.0) / params.wall_range).exp();
498 acc = add(acc, scale(e_iw, weight));
499 }
500
501 normalize(acc)
502}
503
504#[inline]
507pub fn free_headroom(i: usize, peds: &[Pedestrian], e: &Vec2) -> f64 {
508 let p = &peds[i];
509 let mut s = f64::INFINITY;
510 for (j, q) in peds.iter().enumerate() {
511 if i == j {
512 continue;
513 }
514 let rel = sub(q.pos, p.pos);
515 let forward = dot(rel, *e);
516 if forward <= 0.0 {
517 continue;
518 }
519 let proj = scale(*e, forward);
521 let lat = norm(sub(rel, proj));
522 if lat > p.radius + q.radius {
523 continue;
524 }
525 let d = norm(rel);
526 if d < s {
527 s = d;
528 }
529 }
530 s
531}
532
533#[cfg(test)]
534#[allow(deprecated)] mod tests {
536 use super::*;
537
538 #[test]
539 fn single_agent_reaches_free_flow() {
540 let mut peds = vec![Pedestrian {
541 pos: [0.0, 0.0],
542 vel: [0.0, 0.0],
543 radius: 0.2,
544 desired_speed: 1.34,
545 destination: [20.0, 0.0],
546 }];
547 step(&mut peds, &[], &Params::default(), 0.1);
548 assert!((peds[0].vel[0] - 1.34).abs() < 1e-6);
549 }
550
551 #[test]
552 fn follower_slows_behind_leader() {
553 let mut peds = vec![
554 Pedestrian {
555 pos: [0.0, 0.0],
556 vel: [0.0, 0.0],
557 radius: 0.2,
558 desired_speed: 1.34,
559 destination: [100.0, 0.0],
560 },
561 Pedestrian {
562 pos: [-0.5, 0.0],
563 vel: [0.0, 0.0],
564 radius: 0.2,
565 desired_speed: 1.34,
566 destination: [100.0, 0.0],
567 },
568 ];
569 step(&mut peds, &[], &Params::default(), 0.1);
570 assert!(
571 peds[1].vel[0] < peds[0].vel[0],
572 "follower must be slower than free-flowing leader"
573 );
574 }
575
576 #[test]
577 fn head_on_pair_does_not_overlap() {
578 let mut peds = vec![
579 Pedestrian {
580 pos: [-4.0, 0.1],
581 vel: [0.0, 0.0],
582 radius: 0.2,
583 desired_speed: 1.34,
584 destination: [4.0, 0.1],
585 },
586 Pedestrian {
587 pos: [4.0, -0.1],
588 vel: [0.0, 0.0],
589 radius: 0.2,
590 desired_speed: 1.34,
591 destination: [-4.0, -0.1],
592 },
593 ];
594 for _ in 0..300 {
595 step(&mut peds, &[], &Params::default(), 0.05);
596 }
597 let d = norm(sub(peds[0].pos, peds[1].pos));
598 assert!(d >= peds[0].radius + peds[1].radius - 1e-3);
599 }
600}