use crate::broadphase::{NeighborGrid, Scratch};
use crate::common::{
add, closest_point_on_segment, dot, norm, normalize, scale, sub, Pedestrian, PedestrianModel,
Vec2, WallSegment,
};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Params {
pub time_gap: f64,
pub alpha: f64,
pub interaction_range: f64,
pub interaction_strength: f64,
pub wall_range: f64,
pub wall_strength: f64,
pub arrival_radius: f64,
}
impl Default for Params {
fn default() -> Self {
Self {
time_gap: 1.06,
alpha: 3.0,
interaction_range: 0.1,
interaction_strength: 5.0,
wall_range: 0.08,
wall_strength: 5.0,
arrival_radius: 0.3,
}
}
}
impl Params {
pub fn validate(&self, dt: f64) -> Result<(), crate::error::CrowdError> {
use crate::error::{require_dt, require_nonneg, require_positive};
const M: &str = "CollisionFreeSpeed";
require_dt(M, dt)?;
require_positive(M, "time_gap", self.time_gap)?;
require_positive(M, "alpha", self.alpha)?;
require_positive(M, "interaction_range", self.interaction_range)?;
require_nonneg(M, "interaction_strength", self.interaction_strength)?;
require_positive(M, "wall_range", self.wall_range)?;
require_nonneg(M, "wall_strength", self.wall_strength)?;
require_nonneg(M, "arrival_radius", self.arrival_radius)?;
Ok(())
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CollisionFreeSpeed;
impl PedestrianModel for CollisionFreeSpeed {
type Params = Params;
fn name(&self) -> &'static str {
"Collision-Free Speed"
}
fn step(&self, peds: &mut [Pedestrian], walls: &[WallSegment], params: &Params, dt: f64) {
#[allow(deprecated)]
step(peds, walls, params, dt);
}
}
#[deprecated(
since = "0.0.3",
note = "O(n²) reference path with per-tick heap allocation; use \
`step_scratch` (zero-alloc) or `step_with_grid` (broadphase) \
instead. See docs/rustsim-crowd.md P1-7."
)]
#[allow(clippy::needless_range_loop)]
pub fn step(peds: &mut [Pedestrian], walls: &[WallSegment], params: &Params, dt: f64) {
let n = peds.len();
let mut new_vel = vec![[0.0f64; 2]; n];
for i in 0..n {
let p = &peds[i];
let e = chosen_direction(i, peds, walls, params);
let s = free_headroom(i, peds, &e);
let effective_radius = p.radius;
let speed_cap = ((s - effective_radius) / params.time_gap).max(0.0);
let v = p
.effective_desired_speed(params.arrival_radius)
.min(speed_cap);
new_vel[i] = scale(e, v);
}
for (p, v) in peds.iter_mut().zip(new_vel.iter()) {
p.vel = *v;
p.pos = add(p.pos, scale(p.vel, dt));
}
}
#[inline]
pub fn neighbor_cutoff(params: &Params) -> f64 {
8.0 * params.interaction_range + 2.0
}
#[allow(clippy::needless_range_loop)]
pub fn step_with_grid(
peds: &mut [Pedestrian],
walls: &[WallSegment],
params: &Params,
dt: f64,
grid: &NeighborGrid,
) {
let n = peds.len();
let cutoff = neighbor_cutoff(params);
let mut new_vel = vec![[0.0f64; 2]; n];
for i in 0..n {
let p = &peds[i];
let e = chosen_direction_grid(i, peds, walls, params, grid, cutoff);
let s = free_headroom_grid(i, peds, &e, grid, cutoff);
let effective_radius = p.radius;
let speed_cap = ((s - effective_radius) / params.time_gap).max(0.0);
let v = p
.effective_desired_speed(params.arrival_radius)
.min(speed_cap);
new_vel[i] = scale(e, v);
}
for (p, v) in peds.iter_mut().zip(new_vel.iter()) {
p.vel = *v;
p.pos = add(p.pos, scale(p.vel, dt));
}
}
#[allow(clippy::needless_range_loop)]
pub fn step_scratch(
peds: &mut [Pedestrian],
walls: &[WallSegment],
params: &Params,
dt: f64,
scratch: &mut Scratch,
) {
let n = peds.len();
let cutoff = neighbor_cutoff(params);
scratch.prepare(peds);
let (new_vel, grid) = scratch.split();
for i in 0..n {
let p = &peds[i];
let e = chosen_direction_grid(i, peds, walls, params, grid, cutoff);
let s = free_headroom_grid(i, peds, &e, grid, cutoff);
let speed_cap = ((s - p.radius) / params.time_gap).max(0.0);
let v = p
.effective_desired_speed(params.arrival_radius)
.min(speed_cap);
new_vel[i] = scale(e, v);
}
for (p, v) in peds.iter_mut().zip(new_vel.iter()) {
p.vel = *v;
p.pos = add(p.pos, scale(p.vel, dt));
}
}
#[cfg(feature = "simd")]
#[allow(clippy::needless_range_loop)]
pub fn step_scratch_simd(
peds: &mut [Pedestrian],
walls: &[WallSegment],
params: &Params,
dt: f64,
scratch: &mut Scratch,
) {
let n = peds.len();
let cutoff = neighbor_cutoff(params);
scratch.prepare(peds);
let (new_vel, grid) = scratch.split();
for i in 0..n {
let p = &peds[i];
let e = chosen_direction_grid_simd(i, peds, walls, params, grid, cutoff);
let s = free_headroom_grid_simd(i, peds, &e, grid, cutoff);
let speed_cap = ((s - p.radius) / params.time_gap).max(0.0);
let v = p
.effective_desired_speed(params.arrival_radius)
.min(speed_cap);
new_vel[i] = scale(e, v);
}
for (p, v) in peds.iter_mut().zip(new_vel.iter()) {
p.vel = *v;
p.pos = add(p.pos, scale(p.vel, dt));
}
}
#[cfg(feature = "rayon")]
#[allow(clippy::needless_range_loop)]
pub fn step_scratch_par(
peds: &mut [Pedestrian],
walls: &[WallSegment],
params: &Params,
dt: f64,
scratch: &mut Scratch,
) {
use rayon::prelude::*;
let cutoff = neighbor_cutoff(params);
scratch.prepare(peds);
let (new_vel, grid) = scratch.split();
let peds_ro: &[Pedestrian] = peds;
new_vel.par_iter_mut().enumerate().for_each(|(i, v_slot)| {
let p = &peds_ro[i];
let e = chosen_direction_grid(i, peds_ro, walls, params, grid, cutoff);
let s = free_headroom_grid(i, peds_ro, &e, grid, cutoff);
let speed_cap = ((s - p.radius) / params.time_gap).max(0.0);
let v = p
.effective_desired_speed(params.arrival_radius)
.min(speed_cap);
*v_slot = scale(e, v);
});
for (p, v) in peds.iter_mut().zip(new_vel.iter()) {
p.vel = *v;
p.pos = add(p.pos, scale(p.vel, dt));
}
}
fn chosen_direction_grid(
i: usize,
peds: &[Pedestrian],
walls: &[WallSegment],
params: &Params,
grid: &NeighborGrid,
cutoff: f64,
) -> Vec2 {
let p = &peds[i];
let e_dest = p.desired_direction();
let mut acc = scale(e_dest, params.alpha);
grid.for_each_neighbor(i, cutoff, peds, |_j, q| {
let diff = sub(p.pos, q.pos);
let d = norm(diff);
if d < 1e-9 {
return;
}
let e_ij = scale(diff, 1.0 / d);
let clearance = d - (p.radius + q.radius);
let weight =
params.interaction_strength * (-clearance.max(0.0) / params.interaction_range).exp();
acc = add(acc, scale(e_ij, weight));
});
for w in walls {
let closest = closest_point_on_segment(p.pos, w.a, w.b);
let diff = sub(p.pos, closest);
let d = norm(diff);
if d < 1e-9 {
continue;
}
let e_iw = scale(diff, 1.0 / d);
let clearance = d - p.radius;
let weight = params.wall_strength * (-clearance.max(0.0) / params.wall_range).exp();
acc = add(acc, scale(e_iw, weight));
}
normalize(acc)
}
#[cfg(feature = "simd")]
fn chosen_direction_grid_simd(
i: usize,
peds: &[Pedestrian],
walls: &[WallSegment],
params: &Params,
grid: &NeighborGrid,
cutoff: f64,
) -> Vec2 {
let p = &peds[i];
let e_dest = p.desired_direction();
let mut acc = scale(e_dest, params.alpha);
let mut idxs: [Option<usize>; 4] = [None, None, None, None];
let mut filled: usize = 0;
grid.for_each_neighbor(i, cutoff, peds, |j, _q| {
idxs[filled] = Some(j);
filled += 1;
if filled == 4 {
let buf: [Option<&Pedestrian>; 4] = [
Some(&peds[idxs[0].unwrap()]),
Some(&peds[idxs[1].unwrap()]),
Some(&peds[idxs[2].unwrap()]),
Some(&peds[idxs[3].unwrap()]),
];
acc = add(acc, crate::simd::cfs_direction_x4(p, buf, params));
idxs = [None, None, None, None];
filled = 0;
}
});
if filled > 0 {
let buf: [Option<&Pedestrian>; 4] = [
idxs[0].map(|k| &peds[k]),
idxs[1].map(|k| &peds[k]),
idxs[2].map(|k| &peds[k]),
idxs[3].map(|k| &peds[k]),
];
acc = add(acc, crate::simd::cfs_direction_x4(p, buf, params));
}
for w in walls {
let closest = closest_point_on_segment(p.pos, w.a, w.b);
let diff = sub(p.pos, closest);
let d = norm(diff);
if d < 1e-9 {
continue;
}
let e_iw = scale(diff, 1.0 / d);
let clearance = d - p.radius;
let weight = params.wall_strength * (-clearance.max(0.0) / params.wall_range).exp();
acc = add(acc, scale(e_iw, weight));
}
normalize(acc)
}
fn free_headroom_grid(
i: usize,
peds: &[Pedestrian],
e: &Vec2,
grid: &NeighborGrid,
cutoff: f64,
) -> f64 {
let p = &peds[i];
let mut s = f64::INFINITY;
grid.for_each_neighbor(i, cutoff, peds, |_j, q| {
let rel = sub(q.pos, p.pos);
let forward = dot(rel, *e);
if forward <= 0.0 {
return;
}
let proj = scale(*e, forward);
let lat = norm(sub(rel, proj));
if lat > p.radius + q.radius {
return;
}
let d = norm(rel);
if d < s {
s = d;
}
});
s
}
#[cfg(feature = "simd")]
fn free_headroom_grid_simd(
i: usize,
peds: &[Pedestrian],
e: &Vec2,
grid: &NeighborGrid,
cutoff: f64,
) -> f64 {
let p = &peds[i];
let mut s = f64::INFINITY;
let mut idxs: [Option<usize>; 4] = [None, None, None, None];
let mut filled: usize = 0;
grid.for_each_neighbor(i, cutoff, peds, |j, _q| {
idxs[filled] = Some(j);
filled += 1;
if filled == 4 {
let buf: [Option<&Pedestrian>; 4] = [
Some(&peds[idxs[0].unwrap()]),
Some(&peds[idxs[1].unwrap()]),
Some(&peds[idxs[2].unwrap()]),
Some(&peds[idxs[3].unwrap()]),
];
s = s.min(crate::simd::cfs_headroom_x4(p, *e, buf));
idxs = [None, None, None, None];
filled = 0;
}
});
if filled > 0 {
let buf: [Option<&Pedestrian>; 4] = [
idxs[0].map(|k| &peds[k]),
idxs[1].map(|k| &peds[k]),
idxs[2].map(|k| &peds[k]),
idxs[3].map(|k| &peds[k]),
];
s = s.min(crate::simd::cfs_headroom_x4(p, *e, buf));
}
s
}
#[inline]
pub fn chosen_direction(
i: usize,
peds: &[Pedestrian],
walls: &[WallSegment],
params: &Params,
) -> Vec2 {
let p = &peds[i];
let e_dest = p.desired_direction();
let mut acc = scale(e_dest, params.alpha);
for (j, q) in peds.iter().enumerate() {
if i == j {
continue;
}
let diff = sub(p.pos, q.pos);
let d = norm(diff);
if d < 1e-9 {
continue;
}
let e_ij = scale(diff, 1.0 / d);
let clearance = d - (p.radius + q.radius);
let weight =
params.interaction_strength * (-clearance.max(0.0) / params.interaction_range).exp();
acc = add(acc, scale(e_ij, weight));
}
for w in walls {
let closest = closest_point_on_segment(p.pos, w.a, w.b);
let diff = sub(p.pos, closest);
let d = norm(diff);
if d < 1e-9 {
continue;
}
let e_iw = scale(diff, 1.0 / d);
let clearance = d - p.radius;
let weight = params.wall_strength * (-clearance.max(0.0) / params.wall_range).exp();
acc = add(acc, scale(e_iw, weight));
}
normalize(acc)
}
#[inline]
pub fn free_headroom(i: usize, peds: &[Pedestrian], e: &Vec2) -> f64 {
let p = &peds[i];
let mut s = f64::INFINITY;
for (j, q) in peds.iter().enumerate() {
if i == j {
continue;
}
let rel = sub(q.pos, p.pos);
let forward = dot(rel, *e);
if forward <= 0.0 {
continue;
}
let proj = scale(*e, forward);
let lat = norm(sub(rel, proj));
if lat > p.radius + q.radius {
continue;
}
let d = norm(rel);
if d < s {
s = d;
}
}
s
}
#[cfg(test)]
#[allow(deprecated)] mod tests {
use super::*;
#[test]
fn single_agent_reaches_free_flow() {
let mut peds = vec![Pedestrian {
pos: [0.0, 0.0],
vel: [0.0, 0.0],
radius: 0.2,
desired_speed: 1.34,
destination: [20.0, 0.0],
}];
step(&mut peds, &[], &Params::default(), 0.1);
assert!((peds[0].vel[0] - 1.34).abs() < 1e-6);
}
#[test]
fn follower_slows_behind_leader() {
let mut peds = vec![
Pedestrian {
pos: [0.0, 0.0],
vel: [0.0, 0.0],
radius: 0.2,
desired_speed: 1.34,
destination: [100.0, 0.0],
},
Pedestrian {
pos: [-0.5, 0.0],
vel: [0.0, 0.0],
radius: 0.2,
desired_speed: 1.34,
destination: [100.0, 0.0],
},
];
step(&mut peds, &[], &Params::default(), 0.1);
assert!(
peds[1].vel[0] < peds[0].vel[0],
"follower must be slower than free-flowing leader"
);
}
#[test]
fn head_on_pair_does_not_overlap() {
let mut peds = vec![
Pedestrian {
pos: [-4.0, 0.1],
vel: [0.0, 0.0],
radius: 0.2,
desired_speed: 1.34,
destination: [4.0, 0.1],
},
Pedestrian {
pos: [4.0, -0.1],
vel: [0.0, 0.0],
radius: 0.2,
desired_speed: 1.34,
destination: [-4.0, -0.1],
},
];
for _ in 0..300 {
step(&mut peds, &[], &Params::default(), 0.05);
}
let d = norm(sub(peds[0].pos, peds[1].pos));
assert!(d >= peds[0].radius + peds[1].radius - 1e-3);
}
}