use crate::broadphase::{NeighborGrid, Scratch};
use crate::common::{add, clamp_speed, closest_point_on_segment, norm, scale, sub};
use crate::common::{Pedestrian, PedestrianModel, Vec2, WallSegment};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Params {
pub tau: f64,
pub a_ped: f64,
pub b_ped: f64,
pub a_wall: f64,
pub b_wall: f64,
pub mass: f64,
pub max_speed: f64,
pub arrival_radius: f64,
pub max_accel: f64,
}
impl Default for Params {
fn default() -> Self {
Self {
tau: 0.5,
a_ped: 2000.0,
b_ped: 0.08,
a_wall: 2000.0,
b_wall: 0.08,
mass: 80.0,
max_speed: 2.5,
arrival_radius: 0.3,
max_accel: 20.0,
}
}
}
impl Params {
pub fn validate(&self, dt: f64) -> Result<(), crate::error::CrowdError> {
use crate::error::{require_dt, require_nonneg, require_positive, CrowdError};
const M: &str = "SocialForce";
require_dt(M, dt)?;
require_positive(M, "tau", self.tau)?;
require_positive(M, "b_ped", self.b_ped)?;
require_positive(M, "b_wall", self.b_wall)?;
require_positive(M, "mass", self.mass)?;
require_positive(M, "max_speed", self.max_speed)?;
require_positive(M, "max_accel", self.max_accel)?;
require_nonneg(M, "a_ped", self.a_ped)?;
require_nonneg(M, "a_wall", self.a_wall)?;
require_nonneg(M, "arrival_radius", self.arrival_radius)?;
let product = dt * self.max_accel;
if product > self.max_speed {
return Err(CrowdError::CflViolation {
model: M,
product,
max_speed: self.max_speed,
max_dt: self.max_speed / self.max_accel,
});
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct SocialForce;
impl PedestrianModel for SocialForce {
type Params = Params;
fn name(&self) -> &'static str {
"Social Force"
}
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 accels = vec![[0.0f64; 2]; n];
for i in 0..n {
let p = &peds[i];
let mut f = driving_force(p, params);
for j in 0..n {
if i == j {
continue;
}
let q = &peds[j];
let f_ij = ped_repulsion(p, q, params);
f = add(f, f_ij);
}
for w in walls {
let f_iw = wall_repulsion(p, w, params);
f = add(f, f_iw);
}
accels[i] = cap_accel(scale(f, 1.0 / params.mass), params.max_accel);
}
for (p, a) in peds.iter_mut().zip(accels.iter()) {
p.vel = add(p.vel, scale(*a, dt));
p.vel = clamp_speed(p.vel, params.max_speed);
p.pos = add(p.pos, scale(p.vel, dt));
}
}
#[inline]
pub fn neighbor_cutoff(params: &Params) -> f64 {
8.0 * params.b_ped + 1.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 accels = vec![[0.0f64; 2]; n];
for i in 0..n {
let p = &peds[i];
let mut f = driving_force(p, params);
grid.for_each_neighbor(i, cutoff, peds, |_j, q| {
f = add(f, ped_repulsion(p, q, params));
});
for w in walls {
f = add(f, wall_repulsion(p, w, params));
}
accels[i] = cap_accel(scale(f, 1.0 / params.mass), params.max_accel);
}
for (p, a) in peds.iter_mut().zip(accels.iter()) {
p.vel = add(p.vel, scale(*a, dt));
p.vel = clamp_speed(p.vel, params.max_speed);
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 (accels, grid) = scratch.split();
for i in 0..n {
let p = &peds[i];
let mut f = driving_force(p, params);
grid.for_each_neighbor(i, cutoff, peds, |_j, q| {
f = add(f, ped_repulsion(p, q, params));
});
for w in walls {
f = add(f, wall_repulsion(p, w, params));
}
accels[i] = cap_accel(scale(f, 1.0 / params.mass), params.max_accel);
}
for (p, a) in peds.iter_mut().zip(accels.iter()) {
p.vel = add(p.vel, scale(*a, dt));
p.vel = clamp_speed(p.vel, params.max_speed);
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 (accels, grid) = scratch.split();
let peds_ro: &[Pedestrian] = peds;
accels.par_iter_mut().enumerate().for_each(|(i, a_slot)| {
let p = &peds_ro[i];
let mut f = driving_force(p, params);
grid.for_each_neighbor(i, cutoff, peds_ro, |_j, q| {
f = add(f, ped_repulsion(p, q, params));
});
for w in walls {
f = add(f, wall_repulsion(p, w, params));
}
*a_slot = cap_accel(scale(f, 1.0 / params.mass), params.max_accel);
});
for (p, a) in peds.iter_mut().zip(accels.iter()) {
p.vel = add(p.vel, scale(*a, dt));
p.vel = clamp_speed(p.vel, params.max_speed);
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 (accels, grid) = scratch.split();
for i in 0..n {
let p = &peds[i];
let mut f = driving_force(p, params);
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()]),
];
let pf = crate::simd::pair_force_x4(p, buf, params);
f = add(f, pf);
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]),
];
let pf = crate::simd::pair_force_x4(p, buf, params);
f = add(f, pf);
}
for w in walls {
f = add(f, wall_repulsion(p, w, params));
}
accels[i] = cap_accel(scale(f, 1.0 / params.mass), params.max_accel);
}
for (p, a) in peds.iter_mut().zip(accels.iter()) {
p.vel = add(p.vel, scale(*a, dt));
p.vel = clamp_speed(p.vel, params.max_speed);
p.pos = add(p.pos, scale(p.vel, dt));
}
}
#[inline]
pub fn cap_accel(a: Vec2, cap: f64) -> Vec2 {
let m = (a[0] * a[0] + a[1] * a[1]).sqrt();
if m > cap && m > 0.0 {
scale(a, cap / m)
} else {
a
}
}
#[inline]
pub fn driving_force(p: &Pedestrian, params: &Params) -> Vec2 {
let e = p.desired_direction();
let target = scale(e, p.effective_desired_speed(params.arrival_radius));
let delta = sub(target, p.vel);
scale(delta, params.mass / params.tau)
}
#[inline]
pub fn ped_repulsion(p: &Pedestrian, q: &Pedestrian, params: &Params) -> Vec2 {
let diff = sub(p.pos, q.pos);
let d = norm(diff);
if d < 1e-9 {
return [0.0, 0.0];
}
let r_sum = p.radius + q.radius;
let e = scale(diff, 1.0 / d);
let magnitude = params.a_ped * ((r_sum - d) / params.b_ped).exp();
scale(e, magnitude)
}
#[inline]
pub fn wall_repulsion(p: &Pedestrian, wall: &WallSegment, params: &Params) -> Vec2 {
let closest = closest_point_on_segment(p.pos, wall.a, wall.b);
let diff = sub(p.pos, closest);
let d = norm(diff);
if d < 1e-9 {
return [0.0, 0.0];
}
let e = scale(diff, 1.0 / d);
let magnitude = params.a_wall * ((p.radius - d) / params.b_wall).exp();
scale(e, magnitude)
}
#[cfg(test)]
#[allow(deprecated)] mod tests {
use super::*;
fn single_agent_toward(dest: Vec2) -> Pedestrian {
Pedestrian {
pos: [0.0, 0.0],
vel: [0.0, 0.0],
radius: 0.25,
desired_speed: 1.34,
destination: dest,
}
}
#[test]
fn integrator_is_semi_implicit_euler() {
let mut peds = vec![Pedestrian {
pos: [0.0, 0.0],
vel: [0.0, 0.0],
radius: 0.25,
desired_speed: 1.34,
destination: [100.0, 0.0],
}];
let params = Params::default();
let dt = 0.05;
step(&mut peds, &[], ¶ms, dt);
let expected_a_x = peds[0].desired_speed / params.tau;
let expected_v_x = expected_a_x * dt;
let expected_p_x = expected_v_x * dt; let explicit_p_x = 0.0;
assert!(
peds[0].pos[0] > explicit_p_x + 0.5 * expected_a_x * dt * dt,
"position advance must use post-update velocity (symplectic Euler), got p_x = {}",
peds[0].pos[0]
);
assert!(
(peds[0].pos[0] - expected_p_x).abs() < 1e-12,
"symplectic Euler position should equal a_x * dt² = {}, got {}",
expected_p_x,
peds[0].pos[0]
);
assert!(
(peds[0].vel[0] - expected_v_x).abs() < 1e-12,
"velocity should equal a_x * dt = {}, got {}",
expected_v_x,
peds[0].vel[0]
);
}
#[test]
fn single_agent_moves_toward_destination() {
let mut peds = vec![single_agent_toward([10.0, 0.0])];
for _ in 0..100 {
step(&mut peds, &[], &Params::default(), 0.05);
}
assert!(peds[0].pos[0] > 1.0, "agent should have advanced in +x");
assert!(peds[0].pos[1].abs() < 0.05, "no lateral drift");
}
#[test]
fn two_agents_head_on_do_not_overlap() {
let mut peds = vec![
Pedestrian {
pos: [-5.0, 0.05],
vel: [0.0, 0.0],
radius: 0.25,
desired_speed: 1.34,
destination: [5.0, 0.05],
},
Pedestrian {
pos: [5.0, -0.05],
vel: [0.0, 0.0],
radius: 0.25,
desired_speed: 1.34,
destination: [-5.0, -0.05],
},
];
for _ in 0..400 {
step(&mut peds, &[], &Params::default(), 0.02);
}
let dx = peds[0].pos[0] - peds[1].pos[0];
let dy = peds[0].pos[1] - peds[1].pos[1];
let d = (dx * dx + dy * dy).sqrt();
assert!(
d >= peds[0].radius + peds[1].radius,
"agents overlapped: d={d}"
);
}
#[test]
fn trait_impl_reports_name() {
let m = SocialForce;
assert_eq!(m.name(), "Social Force");
}
#[test]
fn cap_accel_clamps_magnitude() {
let a = cap_accel([30.0, 40.0], 10.0); let m = (a[0] * a[0] + a[1] * a[1]).sqrt();
assert!((m - 10.0).abs() < 1e-12);
assert!((a[0] / a[1] - 30.0 / 40.0).abs() < 1e-12);
let b = cap_accel([3.0, 4.0], 10.0);
assert_eq!(b, [3.0, 4.0]);
}
#[test]
fn agent_settles_inside_arrival_radius() {
let mut peds = vec![Pedestrian {
pos: [0.0, 0.0],
vel: [0.0, 0.0],
radius: 0.25,
desired_speed: 1.34,
destination: [5.0, 0.0],
}];
let params = Params::default();
let mut max_overshoot: f64 = 0.0;
for _ in 0..1000 {
step(&mut peds, &[], ¶ms, 0.05);
let overshoot = peds[0].pos[0] - peds[0].destination[0];
if overshoot > max_overshoot {
max_overshoot = overshoot;
}
}
assert!(peds[0].has_arrived(params.arrival_radius + 1e-6));
assert!(
max_overshoot <= params.arrival_radius + 1e-3,
"agent overshot destination by {max_overshoot:.3} m (arrival_radius={})",
params.arrival_radius
);
}
#[test]
fn stiff_pair_does_not_blow_up_with_accel_cap() {
let mut peds = vec![
Pedestrian {
pos: [0.0, 0.0],
vel: [0.0, 0.0],
radius: 0.25,
desired_speed: 0.0,
destination: [0.0, 0.0],
},
Pedestrian {
pos: [0.1, 0.0], vel: [0.0, 0.0],
radius: 0.25,
desired_speed: 0.0,
destination: [0.0, 0.0],
},
];
let params = Params::default();
step(&mut peds, &[], ¶ms, 0.05);
for p in &peds {
let v = (p.vel[0] * p.vel[0] + p.vel[1] * p.vel[1]).sqrt();
assert!(
v <= params.max_accel * 0.05 + 1e-9,
"velocity {v} exceeded max_accel*dt={} — accel cap not applied",
params.max_accel * 0.05
);
}
}
#[test]
fn step_with_grid_matches_step_within_tolerance() {
let mut a: Vec<Pedestrian> = Vec::new();
for k in 0..32 {
let x = ((k * 2654435761u64) % 6_000_000) as f64 / 1_000_000.0;
let y = ((k * 40503u64) % 6_000_000) as f64 / 1_000_000.0;
a.push(Pedestrian {
pos: [x, y],
vel: [0.0, 0.0],
radius: 0.25,
desired_speed: 1.2,
destination: [x + 5.0, y],
});
}
let mut b = a.clone();
let params = Params::default();
let mut grid = crate::broadphase::NeighborGrid::new(neighbor_cutoff(¶ms));
for _ in 0..50 {
step(&mut a, &[], ¶ms, 0.05);
grid.rebuild(&b);
step_with_grid(&mut b, &[], ¶ms, 0.05, &grid);
}
for i in 0..a.len() {
let dx = a[i].pos[0] - b[i].pos[0];
let dy = a[i].pos[1] - b[i].pos[1];
let d = (dx * dx + dy * dy).sqrt();
assert!(
d < 1e-3,
"agent {i}: grid path diverged from O(n^2) by {d:.3e} m"
);
}
}
#[test]
fn step_scratch_matches_step_with_grid_bit_exact() {
let mut a: Vec<Pedestrian> = Vec::new();
for k in 0..24 {
let x = ((k * 2654435761u64) % 6_000_000) as f64 / 1_000_000.0;
let y = ((k * 40503u64) % 6_000_000) as f64 / 1_000_000.0;
a.push(Pedestrian {
pos: [x, y],
vel: [0.0, 0.0],
radius: 0.25,
desired_speed: 1.2,
destination: [x + 5.0, y],
});
}
let mut b = a.clone();
let params = Params::default();
let cutoff = neighbor_cutoff(¶ms);
let mut grid = crate::broadphase::NeighborGrid::new(cutoff);
let mut scratch = crate::broadphase::Scratch::with_capacity(a.len(), cutoff);
for _ in 0..40 {
grid.rebuild(&a);
step_with_grid(&mut a, &[], ¶ms, 0.05, &grid);
step_scratch(&mut b, &[], ¶ms, 0.05, &mut scratch);
}
for i in 0..a.len() {
assert_eq!(a[i].pos, b[i].pos, "scratch path diverged at {i}");
assert_eq!(a[i].vel, b[i].vel);
}
}
#[cfg(feature = "rayon")]
#[test]
fn step_scratch_par_matches_step_scratch_bit_exact() {
let mut a: Vec<Pedestrian> = Vec::new();
for k in 0..64 {
let x = ((k * 2654435761u64) % 6_000_000) as f64 / 1_000_000.0;
let y = ((k * 40503u64) % 6_000_000) as f64 / 1_000_000.0;
a.push(Pedestrian {
pos: [x, y],
vel: [0.0, 0.0],
radius: 0.25,
desired_speed: 1.2,
destination: [x + 5.0, y],
});
}
let mut b = a.clone();
let walls = vec![WallSegment {
a: [-1.0, -1.0],
b: [20.0, -1.0],
}];
let params = Params::default();
let cutoff = neighbor_cutoff(¶ms);
let mut scratch_a = crate::broadphase::Scratch::with_capacity(a.len(), cutoff);
let mut scratch_b = crate::broadphase::Scratch::with_capacity(b.len(), cutoff);
for _ in 0..40 {
step_scratch(&mut a, &walls, ¶ms, 0.05, &mut scratch_a);
step_scratch_par(&mut b, &walls, ¶ms, 0.05, &mut scratch_b);
}
for i in 0..a.len() {
assert_eq!(
a[i].pos, b[i].pos,
"parallel path diverged in position at {i}"
);
assert_eq!(
a[i].vel, b[i].vel,
"parallel path diverged in velocity at {i}"
);
}
}
}