#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SocialForceParams {
pub tau: f64,
pub a_social: f64,
pub b_social: f64,
pub k_body: f64,
pub kappa_friction: f64,
pub a_wall: f64,
pub b_wall: f64,
pub k_wall: f64,
pub kappa_wall: f64,
pub max_speed: f64,
pub mass: f64,
}
impl Default for SocialForceParams {
fn default() -> Self {
Self {
tau: 0.5,
a_social: 2000.0,
b_social: 0.08,
k_body: 1.2e5,
kappa_friction: 2.4e5,
a_wall: 2000.0,
b_wall: 0.08,
k_wall: 1.2e5,
kappa_wall: 2.4e5,
max_speed: 2.5,
mass: 80.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct WallSegment {
pub x1: f64,
pub y1: f64,
pub x2: f64,
pub y2: f64,
}
impl WallSegment {
pub fn new(x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
Self { x1, y1, x2, y2 }
}
pub fn closest_point(&self, px: f64, py: f64) -> (f64, f64) {
let dx = self.x2 - self.x1;
let dy = self.y2 - self.y1;
let len_sq = dx * dx + dy * dy;
if len_sq < 1e-12 {
return (self.x1, self.y1);
}
let t = ((px - self.x1) * dx + (py - self.y1) * dy) / len_sq;
let t = t.clamp(0.0, 1.0);
(self.x1 + t * dx, self.y1 + t * dy)
}
}
#[allow(clippy::too_many_arguments)]
pub fn desired_force_2d(
x: f64,
y: f64,
vx: f64,
vy: f64,
dest_x: f64,
dest_y: f64,
desired_speed: f64,
tau: f64,
) -> (f64, f64) {
let dx = dest_x - x;
let dy = dest_y - y;
let dist = (dx * dx + dy * dy).sqrt();
if dist < 1e-12 {
return (0.0, 0.0);
}
let ex = dx / dist;
let ey = dy / dist;
let fx = (desired_speed * ex - vx) / tau;
let fy = (desired_speed * ey - vy) / tau;
(fx, fy)
}
#[allow(clippy::too_many_arguments)]
pub fn social_repulsion_2d(
x: f64,
y: f64,
vx: f64,
vy: f64,
radius: f64,
nx: f64,
ny: f64,
nvx: f64,
nvy: f64,
n_radius: f64,
params: &SocialForceParams,
) -> (f64, f64) {
let dx = x - nx;
let dy = y - ny;
let dist = (dx * dx + dy * dy).sqrt();
if dist < 1e-12 {
return (0.0, 0.0);
}
let r_ij = radius + n_radius;
let overlap = r_ij - dist;
let ux = dx / dist;
let uy = dy / dist;
let tx = -uy;
let ty = ux;
let inv_m = 1.0 / params.mass;
let f_social = params.a_social * (overlap / params.b_social).exp() * inv_m;
let mut fx = f_social * ux;
let mut fy = f_social * uy;
if overlap > 0.0 {
fx += params.k_body * inv_m * overlap * ux;
fy += params.k_body * inv_m * overlap * uy;
let dvx = nvx - vx;
let dvy = nvy - vy;
let dv_tangential = dvx * tx + dvy * ty;
fx += params.kappa_friction * inv_m * overlap * dv_tangential * tx;
fy += params.kappa_friction * inv_m * overlap * dv_tangential * ty;
}
(fx, fy)
}
pub fn wall_repulsion_2d(
x: f64,
y: f64,
vx: f64,
vy: f64,
radius: f64,
wall: &WallSegment,
params: &SocialForceParams,
) -> (f64, f64) {
let (cx, cy) = wall.closest_point(x, y);
let dx = x - cx;
let dy = y - cy;
let dist = (dx * dx + dy * dy).sqrt();
if dist < 1e-12 {
return (0.0, 0.0);
}
let overlap = radius - dist;
let ux = dx / dist;
let uy = dy / dist;
let tx = -uy;
let ty = ux;
let inv_m = 1.0 / params.mass;
let f_rep = params.a_wall * (overlap / params.b_wall).exp() * inv_m;
let mut fx = f_rep * ux;
let mut fy = f_rep * uy;
if overlap > 0.0 {
fx += params.k_wall * inv_m * overlap * ux;
fy += params.k_wall * inv_m * overlap * uy;
let v_tangential = vx * tx + vy * ty;
fx -= params.kappa_wall * inv_m * overlap * v_tangential * tx;
fy -= params.kappa_wall * inv_m * overlap * v_tangential * ty;
}
(fx, fy)
}
#[allow(clippy::too_many_arguments)]
pub fn integrate_euler_2d(
x: f64,
y: f64,
vx: f64,
vy: f64,
fx: f64,
fy: f64,
dt: f64,
params: &SocialForceParams,
) -> (f64, f64, f64, f64) {
let mut new_vx = vx + fx * dt;
let mut new_vy = vy + fy * dt;
let speed = (new_vx * new_vx + new_vy * new_vy).sqrt();
if speed > params.max_speed {
let scale = params.max_speed / speed;
new_vx *= scale;
new_vy *= scale;
}
let new_x = x + new_vx * dt;
let new_y = y + new_vy * dt;
(new_x, new_y, new_vx, new_vy)
}
#[allow(clippy::too_many_arguments)]
pub fn desired_force_3d(
x: f64,
y: f64,
z: f64,
vx: f64,
vy: f64,
vz: f64,
dest_x: f64,
dest_y: f64,
dest_z: f64,
desired_speed: f64,
tau: f64,
) -> (f64, f64, f64) {
let dx = dest_x - x;
let dy = dest_y - y;
let dz = dest_z - z;
let dist = (dx * dx + dy * dy + dz * dz).sqrt();
if dist < 1e-12 {
return (0.0, 0.0, 0.0);
}
let ex = dx / dist;
let ey = dy / dist;
let ez = dz / dist;
let fx = (desired_speed * ex - vx) / tau;
let fy = (desired_speed * ey - vy) / tau;
let fz = (desired_speed * ez - vz) / tau;
(fx, fy, fz)
}
#[allow(clippy::too_many_arguments)]
pub fn social_repulsion_3d(
x: f64,
y: f64,
z: f64,
vx: f64,
vy: f64,
vz: f64,
radius: f64,
nx: f64,
ny: f64,
nz: f64,
nvx: f64,
nvy: f64,
nvz: f64,
n_radius: f64,
params: &SocialForceParams,
) -> (f64, f64, f64) {
let dx = x - nx;
let dy = y - ny;
let dz = z - nz;
let dist = (dx * dx + dy * dy + dz * dz).sqrt();
if dist < 1e-12 {
return (0.0, 0.0, 0.0);
}
let r_ij = radius + n_radius;
let overlap = r_ij - dist;
let ux = dx / dist;
let uy = dy / dist;
let uz = dz / dist;
let inv_m = 1.0 / params.mass;
let f_social = params.a_social * (overlap / params.b_social).exp() * inv_m;
let mut fx = f_social * ux;
let mut fy = f_social * uy;
let mut fz = f_social * uz;
if overlap > 0.0 {
fx += params.k_body * inv_m * overlap * ux;
fy += params.k_body * inv_m * overlap * uy;
fz += params.k_body * inv_m * overlap * uz;
let dvx = nvx - vx;
let dvy = nvy - vy;
let dvz = nvz - vz;
let dv_normal = dvx * ux + dvy * uy + dvz * uz;
let tvx = dvx - dv_normal * ux;
let tvy = dvy - dv_normal * uy;
let tvz = dvz - dv_normal * uz;
fx += params.kappa_friction * inv_m * overlap * tvx;
fy += params.kappa_friction * inv_m * overlap * tvy;
fz += params.kappa_friction * inv_m * overlap * tvz;
}
(fx, fy, fz)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct WallSegment3D {
pub x1: f64,
pub y1: f64,
pub z1: f64,
pub x2: f64,
pub y2: f64,
pub z2: f64,
}
impl WallSegment3D {
pub fn new(x1: f64, y1: f64, z1: f64, x2: f64, y2: f64, z2: f64) -> Self {
Self {
x1,
y1,
z1,
x2,
y2,
z2,
}
}
pub fn closest_point(&self, px: f64, py: f64, pz: f64) -> (f64, f64, f64) {
let dx = self.x2 - self.x1;
let dy = self.y2 - self.y1;
let dz = self.z2 - self.z1;
let len_sq = dx * dx + dy * dy + dz * dz;
if len_sq < 1e-12 {
return (self.x1, self.y1, self.z1);
}
let t = ((px - self.x1) * dx + (py - self.y1) * dy + (pz - self.z1) * dz) / len_sq;
let t = t.clamp(0.0, 1.0);
(self.x1 + t * dx, self.y1 + t * dy, self.z1 + t * dz)
}
}
#[allow(clippy::too_many_arguments)]
pub fn wall_repulsion_3d(
x: f64,
y: f64,
z: f64,
vx: f64,
vy: f64,
vz: f64,
radius: f64,
wall: &WallSegment3D,
params: &SocialForceParams,
) -> (f64, f64, f64) {
let (cx, cy, cz) = wall.closest_point(x, y, z);
let dx = x - cx;
let dy = y - cy;
let dz = z - cz;
let dist = (dx * dx + dy * dy + dz * dz).sqrt();
if dist < 1e-12 {
return (0.0, 0.0, 0.0);
}
let overlap = radius - dist;
let ux = dx / dist;
let uy = dy / dist;
let uz = dz / dist;
let inv_m = 1.0 / params.mass;
let f_rep = params.a_wall * (overlap / params.b_wall).exp() * inv_m;
let mut fx = f_rep * ux;
let mut fy = f_rep * uy;
let mut fz = f_rep * uz;
if overlap > 0.0 {
fx += params.k_wall * inv_m * overlap * ux;
fy += params.k_wall * inv_m * overlap * uy;
fz += params.k_wall * inv_m * overlap * uz;
let v_normal = vx * ux + vy * uy + vz * uz;
let tvx = vx - v_normal * ux;
let tvy = vy - v_normal * uy;
let tvz = vz - v_normal * uz;
fx -= params.kappa_wall * inv_m * overlap * tvx;
fy -= params.kappa_wall * inv_m * overlap * tvy;
fz -= params.kappa_wall * inv_m * overlap * tvz;
}
(fx, fy, fz)
}
#[allow(clippy::too_many_arguments)]
pub fn integrate_euler_3d(
x: f64,
y: f64,
z: f64,
vx: f64,
vy: f64,
vz: f64,
fx: f64,
fy: f64,
fz: f64,
dt: f64,
params: &SocialForceParams,
) -> (f64, f64, f64, f64, f64, f64) {
let mut new_vx = vx + fx * dt;
let mut new_vy = vy + fy * dt;
let mut new_vz = vz + fz * dt;
let speed = (new_vx * new_vx + new_vy * new_vy + new_vz * new_vz).sqrt();
if speed > params.max_speed {
let scale = params.max_speed / speed;
new_vx *= scale;
new_vy *= scale;
new_vz *= scale;
}
let new_x = x + new_vx * dt;
let new_y = y + new_vy * dt;
let new_z = z + new_vz * dt;
(new_x, new_y, new_z, new_vx, new_vy, new_vz)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn desired_force_points_toward_destination() {
let (fx, fy) = desired_force_2d(0.0, 0.0, 0.0, 0.0, 10.0, 0.0, 1.3, 0.5);
assert!(fx > 0.0, "force should point toward destination (+x)");
assert!(
fy.abs() < 1e-12,
"no y component when destination is on x axis"
);
}
#[test]
fn desired_force_zero_at_destination() {
let (fx, fy) = desired_force_2d(5.0, 5.0, 1.0, 1.0, 5.0, 5.0, 1.3, 0.5);
assert!(fx.abs() < 1e-10);
assert!(fy.abs() < 1e-10);
}
#[test]
fn social_repulsion_pushes_agents_apart() {
let params = SocialForceParams::default();
let (fx, _fy) =
social_repulsion_2d(1.0, 0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 0.0, 0.0, 0.25, ¶ms);
assert!(fx > 0.0, "repulsion should push agent in +x direction");
}
#[test]
fn physical_contact_activates_on_overlap() {
let params = SocialForceParams::default();
let (fx_overlap, _) =
social_repulsion_2d(0.3, 0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 0.0, 0.0, 0.25, ¶ms);
let (fx_far, _) =
social_repulsion_2d(5.0, 0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 0.0, 0.0, 0.25, ¶ms);
assert!(
fx_overlap > fx_far,
"overlapping agents should have stronger force"
);
}
#[test]
fn wall_repulsion_pushes_away() {
let params = SocialForceParams::default();
let wall = WallSegment::new(0.0, 0.0, 10.0, 0.0);
let (fx, fy) = wall_repulsion_2d(5.0, 0.1, 0.0, 0.0, 0.25, &wall, ¶ms);
assert!(
fx.abs() < fy.abs(),
"force should be mostly in +y (away from wall)"
);
assert!(fy > 0.0, "should push agent away from wall in +y direction");
}
#[test]
fn integration_clamps_speed() {
let params = SocialForceParams {
max_speed: 2.0,
..Default::default()
};
let (_, _, new_vx, new_vy) = integrate_euler_2d(0.0, 0.0, 0.0, 0.0, 1e6, 0.0, 0.1, ¶ms);
let speed = (new_vx * new_vx + new_vy * new_vy).sqrt();
assert!(
(speed - 2.0).abs() < 1e-10,
"speed should be clamped to max_speed"
);
}
#[test]
fn wall_segment_closest_point() {
let wall = WallSegment::new(0.0, 0.0, 10.0, 0.0);
let (cx, cy) = wall.closest_point(5.0, 3.0);
assert!((cx - 5.0).abs() < 1e-12);
assert!(cy.abs() < 1e-12);
let (cx, cy) = wall.closest_point(15.0, 3.0);
assert!((cx - 10.0).abs() < 1e-12);
assert!(cy.abs() < 1e-12);
let (cx, cy) = wall.closest_point(-5.0, 3.0);
assert!(cx.abs() < 1e-12);
assert!(cy.abs() < 1e-12);
}
#[test]
fn wall_segment_3d_closest_point() {
let wall = WallSegment3D::new(0.0, 0.0, 0.0, 10.0, 0.0, 0.0);
let (cx, cy, cz) = wall.closest_point(5.0, 3.0, 4.0);
assert!((cx - 5.0).abs() < 1e-12);
assert!(cy.abs() < 1e-12);
assert!(cz.abs() < 1e-12);
}
#[test]
fn desired_force_3d_points_toward_destination() {
let (fx, fy, fz) = desired_force_3d(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 10.0, 0.0, 0.0, 1.3, 0.5);
assert!(fx > 0.0);
assert!(fy.abs() < 1e-12);
assert!(fz.abs() < 1e-12);
}
#[test]
fn social_repulsion_3d_pushes_apart() {
let params = SocialForceParams::default();
let (fx, _, _) = social_repulsion_3d(
1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.25, ¶ms,
);
assert!(fx > 0.0);
}
#[test]
fn integration_3d_clamps_speed() {
let params = SocialForceParams {
max_speed: 2.0,
..Default::default()
};
let (_, _, _, nvx, nvy, nvz) =
integrate_euler_3d(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1e6, 1e6, 1e6, 0.1, ¶ms);
let speed = (nvx * nvx + nvy * nvy + nvz * nvz).sqrt();
assert!((speed - 2.0).abs() < 1e-10);
}
}