use crate::math::Fix128;
use crate::molecular_dynamics::{checked_quotient, checked_sub};
use crate::physics2d::Vec2Fix;
use crate::world_participant::{
ObservationSink, Participant, ParticipantFault, ParticipantKind, StateError, SubstepCtx,
};
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InteractionParams {
pub strength_n: Fix128,
pub range_m: Fix128,
pub body_stiffness: Fix128,
pub sliding_friction: Fix128,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Pedestrian {
pub position: Vec2Fix,
pub velocity: Vec2Fix,
pub radius_m: Fix128,
pub mass_kg: Fix128,
pub desired_speed_m_s: Fix128,
pub desired_direction: Vec2Fix,
pub relaxation_time_s: Fix128,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WallSegment {
pub start: Vec2Fix,
pub end: Vec2Fix,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NeighborSearch {
Direct,
CellList,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CrowdForceError {
NonPositiveRange,
NegativeCoefficient,
AnisotropyOutOfRange,
NonPositiveCutoff,
NonPositiveRelaxationTime {
index: usize,
},
NonPositiveMass {
index: usize,
},
NegativeRadius {
index: usize,
},
NegativeDesiredSpeed {
index: usize,
},
NonPositiveTimeStep,
NonPositiveSpeedCap,
}
impl core::fmt::Display for CrowdForceError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::NonPositiveRange => f.write_str("interaction range B must be positive"),
Self::NegativeCoefficient => {
f.write_str("interaction strength, stiffness and friction must be non-negative")
}
Self::AnisotropyOutOfRange => f.write_str("view-angle weight must be in [0, 1]"),
Self::NonPositiveCutoff => f.write_str("pair cutoff must be positive"),
Self::NonPositiveRelaxationTime { index } => {
write!(f, "pedestrian {index}: relaxation time must be positive")
}
Self::NonPositiveMass { index } => {
write!(f, "pedestrian {index}: mass must be positive")
}
Self::NegativeRadius { index } => {
write!(f, "pedestrian {index}: radius must be non-negative")
}
Self::NegativeDesiredSpeed { index } => {
write!(f, "pedestrian {index}: desired speed must be non-negative")
}
Self::NonPositiveTimeStep => f.write_str("time step must be positive"),
Self::NonPositiveSpeedCap => f.write_str("speed-cap ratio must be positive"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for CrowdForceError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CrowdStepError {
Crowd(CrowdForceError),
Overflow,
}
impl From<CrowdForceError> for CrowdStepError {
fn from(e: CrowdForceError) -> Self {
Self::Crowd(e)
}
}
impl core::fmt::Display for CrowdStepError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Crowd(e) => core::fmt::Display::fmt(e, f),
Self::Overflow => f.write_str("a value of the step is outside the fixed-point range"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for CrowdStepError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SocialForce {
pedestrian: InteractionParams,
wall: InteractionParams,
anisotropy: Fix128,
cutoff_m: Fix128,
}
const FIX_MAX: Fix128 = Fix128::from_raw(i64::MAX, u64::MAX);
fn check_params(p: &InteractionParams) -> Result<(), CrowdForceError> {
if p.range_m <= Fix128::ZERO {
return Err(CrowdForceError::NonPositiveRange);
}
if p.strength_n.is_negative()
|| p.body_stiffness.is_negative()
|| p.sliding_friction.is_negative()
{
return Err(CrowdForceError::NegativeCoefficient);
}
Ok(())
}
fn check_pedestrian(p: &Pedestrian, index: usize) -> Result<(), CrowdForceError> {
if p.relaxation_time_s <= Fix128::ZERO {
return Err(CrowdForceError::NonPositiveRelaxationTime { index });
}
if p.mass_kg <= Fix128::ZERO {
return Err(CrowdForceError::NonPositiveMass { index });
}
if p.radius_m.is_negative() {
return Err(CrowdForceError::NegativeRadius { index });
}
if p.desired_speed_m_s.is_negative() {
return Err(CrowdForceError::NegativeDesiredSpeed { index });
}
Ok(())
}
fn social_magnitude(p: &InteractionParams, x: Fix128) -> Fix128 {
let e = (x / p.range_m).exp();
p.strength_n.checked_mul(e).unwrap_or(FIX_MAX)
}
impl SocialForce {
pub fn new(
pedestrian: InteractionParams,
wall: InteractionParams,
anisotropy: Fix128,
cutoff_m: Fix128,
) -> Result<Self, CrowdForceError> {
check_params(&pedestrian)?;
check_params(&wall)?;
if anisotropy.is_negative() || anisotropy > Fix128::ONE {
return Err(CrowdForceError::AnisotropyOutOfRange);
}
if cutoff_m <= Fix128::ZERO {
return Err(CrowdForceError::NonPositiveCutoff);
}
Ok(Self {
pedestrian,
wall,
anisotropy,
cutoff_m,
})
}
pub fn driving_force(&self, p: &Pedestrian) -> Result<Vec2Fix, CrowdForceError> {
check_pedestrian(p, 0)?;
Ok(driving(p, p.desired_direction.normalize()))
}
#[must_use]
pub fn anisotropy_weight(&self, p: &Pedestrian, n_ij: Vec2Fix) -> Fix128 {
self.weight(p.desired_direction.normalize(), n_ij)
}
fn weight(&self, e_hat: Vec2Fix, n_ij: Vec2Fix) -> Fix128 {
if e_hat == Vec2Fix::ZERO {
return Fix128::ONE;
}
let cos_phi = -n_ij.dot(e_hat);
self.anisotropy + (Fix128::ONE - self.anisotropy) * (Fix128::ONE + cos_phi).half()
}
#[must_use]
pub fn pair_forces(&self, a: &Pedestrian, b: &Pedestrian) -> (Vec2Fix, Vec2Fix) {
self.pair_kernel(
a,
b,
a.desired_direction.normalize(),
b.desired_direction.normalize(),
)
}
fn pair_kernel(
&self,
a: &Pedestrian,
b: &Pedestrian,
ea: Vec2Fix,
eb: Vec2Fix,
) -> (Vec2Fix, Vec2Fix) {
let diff = a.position - b.position;
let d2 = diff.length_squared();
if d2.is_zero() {
return (Vec2Fix::ZERO, Vec2Fix::ZERO);
}
let d = d2.sqrt();
let n = diff / d;
let overlap = a.radius_m + b.radius_m - d;
let social = social_magnitude(&self.pedestrian, overlap);
let mut fa = n * (social * self.weight(ea, n));
let mut fb = -(n * (social * self.weight(eb, -n)));
if overlap > Fix128::ZERO {
let t = n.perpendicular();
let dvt = (b.velocity - a.velocity).dot(t);
let contact = n * (self.pedestrian.body_stiffness * overlap)
+ t * (self.pedestrian.sliding_friction * overlap * dvt);
fa = fa + contact;
fb = fb - contact;
}
(fa, fb)
}
#[must_use]
pub fn wall_force(&self, p: &Pedestrian, w: &WallSegment) -> Vec2Fix {
let diff = p.position - nearest_point(p, w);
let d2 = diff.length_squared();
if d2.is_zero() {
return Vec2Fix::ZERO;
}
let d = d2.sqrt();
let n = diff / d;
let overlap = p.radius_m - d;
let mut f = n * social_magnitude(&self.wall, overlap);
if overlap > Fix128::ZERO {
let t = n.perpendicular();
f = f + n * (self.wall.body_stiffness * overlap)
- t * (self.wall.sliding_friction * overlap * p.velocity.dot(t));
}
f
}
pub fn total_forces(
&self,
peds: &[Pedestrian],
walls: &[WallSegment],
search: NeighborSearch,
out: &mut Vec<Vec2Fix>,
) -> Result<(), CrowdForceError> {
out.clear();
for (i, p) in peds.iter().enumerate() {
check_pedestrian(p, i)?;
}
let cutoff2 = self.cutoff_m * self.cutoff_m;
let headings: Vec<Vec2Fix> = peds
.iter()
.map(|p| p.desired_direction.normalize())
.collect();
for (p, &e) in peds.iter().zip(&headings) {
let mut f = driving(p, e);
for w in walls {
if wall_distance_squared(p, w) <= cutoff2 {
f = f + self.wall_force(p, w);
}
}
out.push(f);
}
let mut add_pair = |i: usize, j: usize| {
let (a, b) = (&peds[i], &peds[j]);
if (a.position - b.position).length_squared() > cutoff2 {
return;
}
let (fa, fb) = self.pair_kernel(a, b, headings[i], headings[j]);
out[i] = out[i] + fa;
out[j] = out[j] + fb;
};
match search {
NeighborSearch::Direct => {
for i in 0..peds.len() {
for j in i + 1..peds.len() {
add_pair(i, j);
}
}
}
NeighborSearch::CellList => {
for (i, j) in cell_list_pairs(peds, self.cutoff_m) {
add_pair(i, j);
}
}
}
Ok(())
}
pub fn step(
&self,
peds: &mut [Pedestrian],
walls: &[WallSegment],
h: Fix128,
search: NeighborSearch,
speed_cap_ratio: Option<Fix128>,
) -> Result<(), CrowdForceError> {
if h <= Fix128::ZERO {
return Err(CrowdForceError::NonPositiveTimeStep);
}
if let Some(c) = speed_cap_ratio {
if c <= Fix128::ZERO {
return Err(CrowdForceError::NonPositiveSpeedCap);
}
}
let mut forces = Vec::with_capacity(peds.len());
self.total_forces(peds, walls, search, &mut forces)?;
for (p, f) in peds.iter_mut().zip(forces) {
let mut v = p.velocity + f * (h / p.mass_kg);
if let Some(c) = speed_cap_ratio {
let v_max = c * p.desired_speed_m_s;
let speed = v.length();
if speed > v_max {
v = if speed.is_zero() {
Vec2Fix::ZERO
} else {
v * (v_max / speed)
};
}
}
p.velocity = v;
p.position = p.position + v * h;
}
Ok(())
}
pub fn try_step(
&self,
peds: &mut [Pedestrian],
walls: &[WallSegment],
h: Fix128,
search: NeighborSearch,
speed_cap_ratio: Option<Fix128>,
) -> Result<(), CrowdStepError> {
if h <= Fix128::ZERO {
return Err(CrowdForceError::NonPositiveTimeStep.into());
}
if let Some(c) = speed_cap_ratio {
if c <= Fix128::ZERO {
return Err(CrowdForceError::NonPositiveSpeedCap.into());
}
}
for (i, p) in peds.iter().enumerate() {
check_pedestrian(p, i)?;
}
let forces = self
.total_forces_checked(peds, walls, search)
.ok_or(CrowdStepError::Overflow)?;
let next = peds
.iter()
.zip(forces)
.map(|(p, f)| advance_checked(p, f, h, speed_cap_ratio))
.collect::<Option<Vec<Pedestrian>>>()
.ok_or(CrowdStepError::Overflow)?;
peds.copy_from_slice(&next);
Ok(())
}
fn total_forces_checked(
&self,
peds: &[Pedestrian],
walls: &[WallSegment],
search: NeighborSearch,
) -> Option<Vec<Vec2Fix>> {
let cutoff2 = self.cutoff_m.checked_mul(self.cutoff_m)?;
let headings = peds
.iter()
.map(|p| normalize_c(p.desired_direction))
.collect::<Option<Vec<Vec2Fix>>>()?;
let mut out = Vec::with_capacity(peds.len());
for (p, &e) in peds.iter().zip(&headings) {
let mut f = driving_c(p, e)?;
for w in walls {
let near = nearest_point_c(p, w)?;
if len_sq_c(sub_c(p.position, near)?)? <= cutoff2 {
f = add_c(f, self.wall_force_c(p, w)?)?;
}
}
out.push(f);
}
let mut add_pair = |i: usize, j: usize| -> Option<()> {
let (a, b) = (&peds[i], &peds[j]);
if len_sq_c(sub_c(a.position, b.position)?)? > cutoff2 {
return Some(());
}
let (fa, fb) = self.pair_kernel_c(a, b, headings[i], headings[j])?;
out[i] = add_c(out[i], fa)?;
out[j] = add_c(out[j], fb)?;
Some(())
};
match search {
NeighborSearch::Direct => {
for i in 0..peds.len() {
for j in i + 1..peds.len() {
add_pair(i, j)?;
}
}
}
NeighborSearch::CellList => {
for (i, j) in cell_list_pairs_checked(peds, self.cutoff_m)? {
add_pair(i, j)?;
}
}
}
Some(out)
}
fn weight_c(&self, e_hat: Vec2Fix, n_ij: Vec2Fix) -> Option<Fix128> {
if e_hat == Vec2Fix::ZERO {
return Some(Fix128::ONE);
}
let cos_phi = neg_c(dot_c(n_ij, e_hat)?)?;
let opening = Fix128::ONE.checked_add(cos_phi)?.half();
let spread = checked_sub(Fix128::ONE, self.anisotropy)?.checked_mul(opening)?;
self.anisotropy.checked_add(spread)
}
fn pair_kernel_c(
&self,
a: &Pedestrian,
b: &Pedestrian,
ea: Vec2Fix,
eb: Vec2Fix,
) -> Option<(Vec2Fix, Vec2Fix)> {
let diff = sub_c(a.position, b.position)?;
let d2 = len_sq_c(diff)?;
if d2.is_zero() {
return Some((Vec2Fix::ZERO, Vec2Fix::ZERO));
}
let d = d2.sqrt();
let n = div_c(diff, d)?;
let overlap = checked_sub(a.radius_m.checked_add(b.radius_m)?, d)?;
let social = social_magnitude_c(&self.pedestrian, overlap)?;
let mut fa = scale_c(n, social.checked_mul(self.weight_c(ea, n)?)?)?;
let mut fb = neg2_c(scale_c(
n,
social.checked_mul(self.weight_c(eb, neg2_c(n)?)?)?,
)?)?;
if overlap > Fix128::ZERO {
let t = perpendicular_c(n)?;
let dvt = dot_c(sub_c(b.velocity, a.velocity)?, t)?;
let normal = scale_c(n, self.pedestrian.body_stiffness.checked_mul(overlap)?)?;
let tangential = scale_c(
t,
self.pedestrian
.sliding_friction
.checked_mul(overlap)?
.checked_mul(dvt)?,
)?;
let contact = add_c(normal, tangential)?;
fa = add_c(fa, contact)?;
fb = sub_c(fb, contact)?;
}
Some((fa, fb))
}
fn wall_force_c(&self, p: &Pedestrian, w: &WallSegment) -> Option<Vec2Fix> {
let diff = sub_c(p.position, nearest_point_c(p, w)?)?;
let d2 = len_sq_c(diff)?;
if d2.is_zero() {
return Some(Vec2Fix::ZERO);
}
let d = d2.sqrt();
let n = div_c(diff, d)?;
let overlap = checked_sub(p.radius_m, d)?;
let mut f = scale_c(n, social_magnitude_c(&self.wall, overlap)?)?;
if overlap > Fix128::ZERO {
let t = perpendicular_c(n)?;
let normal = scale_c(n, self.wall.body_stiffness.checked_mul(overlap)?)?;
let tangential = scale_c(
t,
self.wall
.sliding_friction
.checked_mul(overlap)?
.checked_mul(dot_c(p.velocity, t)?)?,
)?;
f = sub_c(add_c(f, normal)?, tangential)?;
}
Some(f)
}
}
fn add_c(a: Vec2Fix, b: Vec2Fix) -> Option<Vec2Fix> {
Some(Vec2Fix::new(a.x.checked_add(b.x)?, a.y.checked_add(b.y)?))
}
fn sub_c(a: Vec2Fix, b: Vec2Fix) -> Option<Vec2Fix> {
Some(Vec2Fix::new(checked_sub(a.x, b.x)?, checked_sub(a.y, b.y)?))
}
fn scale_c(a: Vec2Fix, s: Fix128) -> Option<Vec2Fix> {
Some(Vec2Fix::new(a.x.checked_mul(s)?, a.y.checked_mul(s)?))
}
fn div_c(a: Vec2Fix, s: Fix128) -> Option<Vec2Fix> {
Some(Vec2Fix::new(
checked_quotient(a.x, s)?,
checked_quotient(a.y, s)?,
))
}
fn neg_c(x: Fix128) -> Option<Fix128> {
checked_sub(Fix128::ZERO, x)
}
fn neg2_c(a: Vec2Fix) -> Option<Vec2Fix> {
Some(Vec2Fix::new(neg_c(a.x)?, neg_c(a.y)?))
}
fn perpendicular_c(a: Vec2Fix) -> Option<Vec2Fix> {
Some(Vec2Fix::new(neg_c(a.y)?, a.x))
}
fn dot_c(a: Vec2Fix, b: Vec2Fix) -> Option<Fix128> {
a.x.checked_mul(b.x)?.checked_add(a.y.checked_mul(b.y)?)
}
fn len_sq_c(a: Vec2Fix) -> Option<Fix128> {
dot_c(a, a)
}
fn normalize_c(a: Vec2Fix) -> Option<Vec2Fix> {
let len = len_sq_c(a)?.sqrt();
if len.is_zero() {
Some(Vec2Fix::ZERO)
} else {
div_c(a, len)
}
}
fn social_magnitude_c(p: &InteractionParams, x: Fix128) -> Option<Fix128> {
let e = checked_quotient(x, p.range_m)?.exp();
Some(p.strength_n.checked_mul(e).unwrap_or(FIX_MAX))
}
fn driving_c(p: &Pedestrian, e_hat: Vec2Fix) -> Option<Vec2Fix> {
let rate = checked_quotient(p.mass_kg, p.relaxation_time_s)?;
scale_c(
sub_c(scale_c(e_hat, p.desired_speed_m_s)?, p.velocity)?,
rate,
)
}
fn nearest_point_c(p: &Pedestrian, w: &WallSegment) -> Option<Vec2Fix> {
let ab = sub_c(w.end, w.start)?;
let len2 = len_sq_c(ab)?;
let s = if len2.is_zero() {
Fix128::ZERO
} else {
checked_quotient(dot_c(sub_c(p.position, w.start)?, ab)?, len2)?
.clamp(Fix128::ZERO, Fix128::ONE)
};
add_c(w.start, scale_c(ab, s)?)
}
fn advance_checked(
p: &Pedestrian,
f: Vec2Fix,
h: Fix128,
speed_cap_ratio: Option<Fix128>,
) -> Option<Pedestrian> {
let mut v = add_c(p.velocity, scale_c(f, checked_quotient(h, p.mass_kg)?)?)?;
if let Some(c) = speed_cap_ratio {
let v_max = c.checked_mul(p.desired_speed_m_s)?;
let speed = len_sq_c(v)?.sqrt();
if speed > v_max {
v = if speed.is_zero() {
Vec2Fix::ZERO
} else {
scale_c(v, checked_quotient(v_max, speed)?)?
};
}
}
let mut next = *p;
next.velocity = v;
next.position = add_c(p.position, scale_c(v, h)?)?;
Some(next)
}
fn nearest_point(p: &Pedestrian, w: &WallSegment) -> Vec2Fix {
let ab = w.end - w.start;
let len2 = ab.length_squared();
let s = if len2.is_zero() {
Fix128::ZERO
} else {
((p.position - w.start).dot(ab) / len2).clamp(Fix128::ZERO, Fix128::ONE)
};
w.start + ab * s
}
fn wall_distance_squared(p: &Pedestrian, w: &WallSegment) -> Fix128 {
(p.position - nearest_point(p, w)).length_squared()
}
fn driving(p: &Pedestrian, e_hat: Vec2Fix) -> Vec2Fix {
(e_hat * p.desired_speed_m_s - p.velocity) * (p.mass_kg / p.relaxation_time_s)
}
fn cell_list_pairs(peds: &[Pedestrian], cutoff: Fix128) -> Vec<(usize, usize)> {
let cell = cutoff + cutoff.shr_bits(20);
let inv = Fix128::ONE / cell;
let keys: Vec<(i64, i64)> = peds
.iter()
.map(|p| ((p.position.y * inv).hi, (p.position.x * inv).hi))
.collect();
pairs_in_adjacent_cells(&keys)
}
fn cell_list_pairs_checked(peds: &[Pedestrian], cutoff: Fix128) -> Option<Vec<(usize, usize)>> {
let cell = cutoff.checked_add(cutoff.shr_bits(20))?;
let inv = checked_quotient(Fix128::ONE, cell)?;
let keys = peds
.iter()
.map(|p| {
Some((
p.position.y.checked_mul(inv)?.hi,
p.position.x.checked_mul(inv)?.hi,
))
})
.collect::<Option<Vec<(i64, i64)>>>()?;
Some(pairs_in_adjacent_cells(&keys))
}
fn pairs_in_adjacent_cells(keys: &[(i64, i64)]) -> Vec<(usize, usize)> {
let mut sorted: Vec<((i64, i64), usize)> =
keys.iter().enumerate().map(|(i, k)| (*k, i)).collect();
sorted.sort_unstable();
let mut pairs = Vec::new();
for (i, &(cy, cx)) in keys.iter().enumerate() {
for dy in -1i64..=1 {
let Some(ny) = cy.checked_add(dy) else {
continue;
};
for dx in -1i64..=1 {
let Some(nx) = cx.checked_add(dx) else {
continue;
};
let target = (ny, nx);
let start = sorted.partition_point(|&(k, _)| k < target);
for &(k, j) in &sorted[start..] {
if k != target {
break;
}
if j > i {
pairs.push((i, j));
}
}
}
}
}
pairs.sort_unstable();
pairs
}
pub const CROWD_PARTICIPANT_KIND: ParticipantKind =
ParticipantKind::new(u32::from_be_bytes(*b"CRWD"));
pub const CROWD_OBS_COUNT: u32 = 0;
pub const CROWD_OBS_MEAN_SPEED: u32 = 1;
const CROWD_STATE_VERSION: u32 = 1;
const CROWD_HEADER_LEN: usize = 4 + 8 + 8;
const CROWD_PEDESTRIAN_LEN: usize = 10 * 16;
#[derive(Debug, Clone)]
pub struct CrowdParticipant {
model: SocialForce,
pedestrians: Vec<Pedestrian>,
walls: Vec<WallSegment>,
search: NeighborSearch,
speed_cap_ratio: Option<Fix128>,
digest: u64,
}
impl CrowdParticipant {
pub fn new(
model: SocialForce,
pedestrians: Vec<Pedestrian>,
walls: Vec<WallSegment>,
search: NeighborSearch,
speed_cap_ratio: Option<Fix128>,
) -> Result<Self, CrowdForceError> {
if let Some(c) = speed_cap_ratio {
if c <= Fix128::ZERO {
return Err(CrowdForceError::NonPositiveSpeedCap);
}
}
for (i, p) in pedestrians.iter().enumerate() {
check_pedestrian(p, i)?;
}
let digest = crowd_digest(&model, &walls, search, speed_cap_ratio);
Ok(Self {
model,
pedestrians,
walls,
search,
speed_cap_ratio,
digest,
})
}
#[must_use]
pub fn pedestrians(&self) -> &[Pedestrian] {
&self.pedestrians
}
pub fn pedestrians_mut(&mut self) -> &mut [Pedestrian] {
&mut self.pedestrians
}
#[must_use]
pub fn walls(&self) -> &[WallSegment] {
&self.walls
}
#[must_use]
pub const fn model(&self) -> &SocialForce {
&self.model
}
}
impl Participant for CrowdParticipant {
fn kind(&self) -> ParticipantKind {
CROWD_PARTICIPANT_KIND
}
fn substep(&mut self, _ctx: &mut SubstepCtx<'_>, h: Fix128) -> Result<(), ParticipantFault> {
self.model
.try_step(
&mut self.pedestrians,
&self.walls,
h,
self.search,
self.speed_cap_ratio,
)
.map_err(|e| match e {
CrowdStepError::Overflow => ParticipantFault::OutOfRange,
_ => ParticipantFault::InvalidState,
})
}
fn observe(&self, out: &mut ObservationSink) {
let n = self.pedestrians.len();
out.push(CROWD_OBS_COUNT, Fix128::from_int(n as i64));
if n > 0 {
let total = self
.pedestrians
.iter()
.fold(Fix128::ZERO, |acc, p| acc + p.velocity.length());
out.push(CROWD_OBS_MEAN_SPEED, total / Fix128::from_int(n as i64));
}
}
fn write_state(&self, out: &mut Vec<u8>) {
out.extend_from_slice(&CROWD_STATE_VERSION.to_le_bytes());
out.extend_from_slice(&self.digest.to_le_bytes());
out.extend_from_slice(&(self.pedestrians.len() as u64).to_le_bytes());
for p in &self.pedestrians {
for f in pedestrian_fields(p) {
put_fix(out, f);
}
}
}
fn check_state(&self, bytes: &[u8]) -> Result<(), StateError> {
if bytes.len() < CROWD_HEADER_LEN {
return Err(StateError::Length {
expected: CROWD_HEADER_LEN,
found: bytes.len(),
});
}
if read_u32(bytes, 0) != CROWD_STATE_VERSION || read_u64(bytes, 4) != self.digest {
return Err(StateError::InvalidValue);
}
let count = read_u64(bytes, 12);
let expected = usize::try_from(count)
.ok()
.and_then(|n| n.checked_mul(CROWD_PEDESTRIAN_LEN))
.and_then(|b| b.checked_add(CROWD_HEADER_LEN))
.unwrap_or(usize::MAX);
if bytes.len() != expected {
return Err(StateError::Length {
expected,
found: bytes.len(),
});
}
Ok(())
}
fn read_state(&mut self, bytes: &[u8]) {
self.pedestrians = bytes[CROWD_HEADER_LEN..]
.chunks_exact(CROWD_PEDESTRIAN_LEN)
.map(|c| {
let f = |k: usize| get_fix(c, 16 * k);
Pedestrian {
position: Vec2Fix::new(f(0), f(1)),
velocity: Vec2Fix::new(f(2), f(3)),
radius_m: f(4),
mass_kg: f(5),
desired_speed_m_s: f(6),
desired_direction: Vec2Fix::new(f(7), f(8)),
relaxation_time_s: f(9),
}
})
.collect();
}
}
fn pedestrian_fields(p: &Pedestrian) -> [Fix128; 10] {
[
p.position.x,
p.position.y,
p.velocity.x,
p.velocity.y,
p.radius_m,
p.mass_kg,
p.desired_speed_m_s,
p.desired_direction.x,
p.desired_direction.y,
p.relaxation_time_s,
]
}
fn crowd_digest(
model: &SocialForce,
walls: &[WallSegment],
search: NeighborSearch,
speed_cap_ratio: Option<Fix128>,
) -> u64 {
let mut b = Vec::new();
for p in [&model.pedestrian, &model.wall] {
for f in [
p.strength_n,
p.range_m,
p.body_stiffness,
p.sliding_friction,
] {
put_fix(&mut b, f);
}
}
put_fix(&mut b, model.anisotropy);
put_fix(&mut b, model.cutoff_m);
b.extend_from_slice(&(walls.len() as u64).to_le_bytes());
for w in walls {
for f in [w.start.x, w.start.y, w.end.x, w.end.y] {
put_fix(&mut b, f);
}
}
b.push(match search {
NeighborSearch::Direct => 0,
NeighborSearch::CellList => 1,
});
match speed_cap_ratio {
None => b.push(0),
Some(c) => {
b.push(1);
put_fix(&mut b, c);
}
}
fnv1a64(&b)
}
fn fnv1a64(bytes: &[u8]) -> u64 {
bytes.iter().fold(0xcbf2_9ce4_8422_2325_u64, |h, &b| {
(h ^ u64::from(b)).wrapping_mul(0x0000_0100_0000_01b3)
})
}
fn put_fix(out: &mut Vec<u8>, f: Fix128) {
out.extend_from_slice(&f.hi.to_le_bytes());
out.extend_from_slice(&f.lo.to_le_bytes());
}
fn read_u32(b: &[u8], at: usize) -> u32 {
let mut a = [0u8; 4];
a.copy_from_slice(&b[at..at + 4]);
u32::from_le_bytes(a)
}
fn read_u64(b: &[u8], at: usize) -> u64 {
let mut a = [0u8; 8];
a.copy_from_slice(&b[at..at + 8]);
u64::from_le_bytes(a)
}
fn get_fix(b: &[u8], at: usize) -> Fix128 {
Fix128::from_raw(read_u64(b, at) as i64, read_u64(b, at + 8))
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(feature = "std"))]
use alloc::{string::ToString, vec};
#[test]
fn cell_list_pairs_cover_all_pairs_within_cutoff() {
let cutoff = Fix128::from_ratio(3, 2);
let mk = |x: i64, y: i64| Pedestrian {
position: Vec2Fix::new(Fix128::from_ratio(x, 4), Fix128::from_ratio(y, 4)),
velocity: Vec2Fix::ZERO,
radius_m: Fix128::ZERO,
mass_kg: Fix128::ONE,
desired_speed_m_s: Fix128::ZERO,
desired_direction: Vec2Fix::ZERO,
relaxation_time_s: Fix128::ONE,
};
let peds: Vec<Pedestrian> = (-6..6)
.flat_map(|x| (-6..6).map(move |y| mk(3 * x + y % 2, 2 * y - x % 3)))
.collect();
let pairs = cell_list_pairs(&peds, cutoff);
let c2 = cutoff * cutoff;
for i in 0..peds.len() {
for j in i + 1..peds.len() {
let near = (peds[i].position - peds[j].position).length_squared() <= c2;
if near {
assert!(pairs.binary_search(&(i, j)).is_ok(), "missing ({i}, {j})");
}
}
}
let mut dedup = pairs.clone();
dedup.dedup();
assert_eq!(dedup.len(), pairs.len(), "no duplicate pairs");
}
fn params(strength: i64, range: i64, k: i64, kappa: i64) -> InteractionParams {
InteractionParams {
strength_n: Fix128::from_int(strength),
range_m: Fix128::from_int(range),
body_stiffness: Fix128::from_int(k),
sliding_friction: Fix128::from_int(kappa),
}
}
fn v2(x: i64, y: i64) -> Vec2Fix {
Vec2Fix::new(Fix128::from_int(x), Fix128::from_int(y))
}
fn ped_at(position: Vec2Fix) -> Pedestrian {
Pedestrian {
position,
velocity: Vec2Fix::ZERO,
radius_m: Fix128::from_ratio(1, 2),
mass_kg: Fix128::ONE,
desired_speed_m_s: Fix128::ZERO,
desired_direction: Vec2Fix::ZERO,
relaxation_time_s: Fix128::ONE,
}
}
fn model(ped: InteractionParams, wall: InteractionParams, anisotropy: Fix128) -> SocialForce {
SocialForce::new(ped, wall, anisotropy, Fix128::from_int(5)).expect("valid model")
}
#[test]
fn construction_errors_and_messages() {
let ok = params(1, 1, 0, 0);
let new = |p, w, a: Fix128, c: Fix128| SocialForce::new(p, w, a, c).err();
assert_eq!(
new(params(1, 0, 0, 0), ok, Fix128::ZERO, Fix128::ONE),
Some(CrowdForceError::NonPositiveRange)
);
assert_eq!(
new(ok, params(1, 1, -1, 0), Fix128::ZERO, Fix128::ONE),
Some(CrowdForceError::NegativeCoefficient)
);
assert_eq!(
new(ok, ok, Fix128::from_int(2), Fix128::ONE),
Some(CrowdForceError::AnisotropyOutOfRange)
);
assert_eq!(
new(ok, ok, -Fix128::ONE, Fix128::ONE),
Some(CrowdForceError::AnisotropyOutOfRange)
);
assert_eq!(
new(ok, ok, Fix128::ZERO, Fix128::ZERO),
Some(CrowdForceError::NonPositiveCutoff)
);
let m = model(ok, ok, Fix128::ZERO);
let base = ped_at(Vec2Fix::ZERO);
let bad = [
(
Pedestrian {
relaxation_time_s: Fix128::ZERO,
..base
},
CrowdForceError::NonPositiveRelaxationTime { index: 0 },
),
(
Pedestrian {
mass_kg: Fix128::ZERO,
..base
},
CrowdForceError::NonPositiveMass { index: 0 },
),
(
Pedestrian {
radius_m: -Fix128::ONE,
..base
},
CrowdForceError::NegativeRadius { index: 0 },
),
(
Pedestrian {
desired_speed_m_s: -Fix128::ONE,
..base
},
CrowdForceError::NegativeDesiredSpeed { index: 0 },
),
];
for (p, e) in bad {
assert_eq!(m.driving_force(&p), Err(e));
}
let messages = [
(
CrowdForceError::NonPositiveRange,
"interaction range B must be positive",
),
(
CrowdForceError::NegativeCoefficient,
"interaction strength, stiffness and friction must be non-negative",
),
(
CrowdForceError::AnisotropyOutOfRange,
"view-angle weight must be in [0, 1]",
),
(
CrowdForceError::NonPositiveCutoff,
"pair cutoff must be positive",
),
(
CrowdForceError::NonPositiveRelaxationTime { index: 3 },
"pedestrian 3: relaxation time must be positive",
),
(
CrowdForceError::NonPositiveMass { index: 3 },
"pedestrian 3: mass must be positive",
),
(
CrowdForceError::NegativeRadius { index: 3 },
"pedestrian 3: radius must be non-negative",
),
(
CrowdForceError::NegativeDesiredSpeed { index: 3 },
"pedestrian 3: desired speed must be non-negative",
),
(
CrowdForceError::NonPositiveTimeStep,
"time step must be positive",
),
(
CrowdForceError::NonPositiveSpeedCap,
"speed-cap ratio must be positive",
),
];
for (e, text) in messages {
assert_eq!(e.to_string(), text);
assert_eq!(CrowdStepError::from(e).to_string(), text);
}
assert_eq!(
CrowdStepError::Overflow.to_string(),
"a value of the step is outside the fixed-point range"
);
}
#[test]
fn driving_force_and_view_weight_closed_form() {
let m = model(
params(1, 1, 0, 0),
params(1, 1, 0, 0),
Fix128::from_ratio(1, 4),
);
let p = Pedestrian {
velocity: v2(1, 0),
mass_kg: Fix128::from_int(2),
desired_speed_m_s: Fix128::from_int(3),
desired_direction: v2(0, 1),
relaxation_time_s: Fix128::from_ratio(1, 2),
..ped_at(Vec2Fix::ZERO)
};
assert_eq!(m.driving_force(&p), Ok(v2(-4, 12)));
let walker = Pedestrian {
desired_direction: v2(1, 0),
..ped_at(Vec2Fix::ZERO)
};
assert_eq!(m.anisotropy_weight(&walker, v2(-1, 0)), Fix128::ONE);
assert_eq!(
m.anisotropy_weight(&walker, v2(1, 0)),
Fix128::from_ratio(1, 4)
);
assert_eq!(
m.anisotropy_weight(&walker, v2(0, 1)),
Fix128::from_ratio(5, 8)
);
assert_eq!(
m.anisotropy_weight(&ped_at(Vec2Fix::ZERO), v2(1, 0)),
Fix128::ONE
);
}
#[test]
fn pair_forces_closed_form() {
let social = model(params(2, 1, 0, 0), params(1, 1, 0, 0), Fix128::ONE);
let a = ped_at(Vec2Fix::ZERO);
let b = ped_at(v2(2, 0));
let (fa, fb) = social.pair_forces(&a, &b);
let want = 2.0 / core::f64::consts::E;
assert!((fa.x.to_f64() + want).abs() < 1e-7, "{fa:?}");
assert!(fa.y.is_zero());
assert_eq!(fb, -fa);
let contact = model(params(0, 1, 4, 3), params(1, 1, 0, 0), Fix128::ONE);
let b = Pedestrian {
velocity: v2(0, 2),
..ped_at(Vec2Fix::new(Fix128::from_ratio(1, 2), Fix128::ZERO))
};
let (fa, fb) = contact.pair_forces(&a, &b);
assert_eq!(fa, v2(-2, 3));
assert_eq!(fb, v2(2, -3));
assert_eq!(contact.pair_forces(&a, &a), (Vec2Fix::ZERO, Vec2Fix::ZERO));
}
#[test]
fn wall_force_closed_form() {
let wall = WallSegment {
start: v2(-1, 0),
end: v2(1, 0),
};
let social = model(params(1, 1, 0, 0), params(2, 1, 0, 0), Fix128::ONE);
let f = social.wall_force(&ped_at(v2(0, 1)), &wall);
assert!(f.x.is_zero());
assert!(
(f.y.to_f64() - 1.213_061_319_425_266_8).abs() < 1e-7,
"{f:?}"
);
let contact = model(params(1, 1, 0, 0), params(0, 1, 4, 3), Fix128::ONE);
let p = Pedestrian {
velocity: v2(2, 0),
..ped_at(Vec2Fix::new(Fix128::ZERO, Fix128::from_ratio(1, 4)))
};
assert_eq!(
contact.wall_force(&p, &wall),
Vec2Fix::new(Fix128::from_ratio(-3, 2), Fix128::ONE)
);
let point = WallSegment {
start: v2(0, -1),
end: v2(0, -1),
};
let f = social.wall_force(&ped_at(v2(0, 1)), &point);
assert!(f.x.is_zero() && f.y > Fix128::ZERO);
assert_eq!(social.wall_force(&ped_at(v2(0, 0)), &wall), Vec2Fix::ZERO);
}
#[test]
fn total_forces_cancel_in_pairs() {
let m = model(params(2, 1, 0, 0), params(1, 1, 0, 0), Fix128::ONE);
let peds = [
ped_at(v2(0, 0)),
ped_at(v2(1, 0)),
ped_at(v2(0, 2)),
ped_at(v2(40, 0)),
];
let mut direct = Vec::new();
m.total_forces(&peds, &[], NeighborSearch::Direct, &mut direct)
.expect("valid crowd");
let mut cells = Vec::new();
m.total_forces(&peds, &[], NeighborSearch::CellList, &mut cells)
.expect("valid crowd");
assert_eq!(direct, cells);
let sum = direct.iter().fold(Vec2Fix::ZERO, |s, f| s + *f);
assert_eq!(sum, Vec2Fix::ZERO);
assert_ne!(direct[0], Vec2Fix::ZERO);
assert_eq!(direct[3], Vec2Fix::ZERO);
let mut bad = peds;
bad[2].mass_kg = Fix128::ZERO;
assert_eq!(
m.total_forces(&bad, &[], NeighborSearch::Direct, &mut direct),
Err(CrowdForceError::NonPositiveMass { index: 2 })
);
}
fn walker() -> Pedestrian {
Pedestrian {
desired_speed_m_s: Fix128::ONE,
desired_direction: v2(1, 0),
relaxation_time_s: Fix128::from_ratio(1, 2),
..ped_at(Vec2Fix::ZERO)
}
}
#[test]
fn step_and_try_step_closed_form() {
let m = model(params(2, 1, 0, 0), params(2, 1, 0, 0), Fix128::ONE);
let far_wall = [WallSegment {
start: v2(-1, 30),
end: v2(1, 30),
}];
let h = Fix128::from_ratio(1, 4);
for cap in [None, Some(Fix128::from_ratio(1, 4))] {
let (v, x) = if cap.is_some() {
(Fix128::from_ratio(1, 4), Fix128::from_ratio(1, 16))
} else {
(Fix128::from_ratio(1, 2), Fix128::from_ratio(1, 8))
};
let want_v = Vec2Fix::new(v, Fix128::ZERO);
let want_x = Vec2Fix::new(x, Fix128::ZERO);
for search in [NeighborSearch::Direct, NeighborSearch::CellList] {
let mut a = [walker()];
m.step(&mut a, &far_wall, h, search, cap).expect("step");
assert_eq!((a[0].velocity, a[0].position), (want_v, want_x), "{cap:?}");
let mut b = [walker()];
m.try_step(&mut b, &far_wall, h, search, cap)
.expect("try_step");
assert_eq!((b[0].velocity, b[0].position), (want_v, want_x), "{cap:?}");
}
}
}
#[test]
fn try_step_refusals_leave_the_crowd_unchanged() {
let m = model(params(2, 1, 0, 0), params(2, 1, 0, 0), Fix128::ONE);
let mut peds = [walker()];
let h = Fix128::from_ratio(1, 4);
assert_eq!(
m.try_step(&mut peds, &[], Fix128::ZERO, NeighborSearch::Direct, None),
Err(CrowdStepError::Crowd(CrowdForceError::NonPositiveTimeStep))
);
assert_eq!(
m.try_step(
&mut peds,
&[],
h,
NeighborSearch::Direct,
Some(Fix128::ZERO)
),
Err(CrowdStepError::Crowd(CrowdForceError::NonPositiveSpeedCap))
);
assert_eq!(
m.step(&mut peds, &[], Fix128::ZERO, NeighborSearch::Direct, None),
Err(CrowdForceError::NonPositiveTimeStep)
);
assert_eq!(
m.step(
&mut peds,
&[],
h,
NeighborSearch::Direct,
Some(Fix128::ZERO)
),
Err(CrowdForceError::NonPositiveSpeedCap)
);
let mut bad = [Pedestrian {
mass_kg: Fix128::ZERO,
..walker()
}];
assert_eq!(
m.try_step(&mut bad, &[], h, NeighborSearch::Direct, None),
Err(CrowdStepError::Crowd(CrowdForceError::NonPositiveMass {
index: 0
}))
);
let fast = Pedestrian {
velocity: Vec2Fix::new(Fix128::from_int(1 << 62), Fix128::ZERO),
..walker()
};
let mut runaway = [fast];
assert_eq!(
m.try_step(
&mut runaway,
&[],
Fix128::from_int(4),
NeighborSearch::Direct,
None
),
Err(CrowdStepError::Overflow)
);
assert_eq!(runaway, [fast]);
assert_eq!(peds, [walker()]);
}
fn crowd() -> CrowdParticipant {
let m = model(params(2, 1, 0, 0), params(2, 1, 0, 0), Fix128::ONE);
CrowdParticipant::new(
m,
vec![walker()],
vec![WallSegment {
start: v2(-1, 30),
end: v2(1, 30),
}],
NeighborSearch::Direct,
None,
)
.expect("valid crowd")
}
#[test]
fn crowd_participant_state_and_observations() {
let m = model(params(2, 1, 0, 0), params(2, 1, 0, 0), Fix128::ONE);
assert_eq!(
CrowdParticipant::new(
m,
vec![],
vec![],
NeighborSearch::Direct,
Some(Fix128::ZERO)
)
.err(),
Some(CrowdForceError::NonPositiveSpeedCap)
);
let bad = Pedestrian {
radius_m: -Fix128::ONE,
..walker()
};
assert_eq!(
CrowdParticipant::new(
m,
vec![walker(), bad],
vec![],
NeighborSearch::CellList,
None
)
.err(),
Some(CrowdForceError::NegativeRadius { index: 1 })
);
let mut c = crowd();
assert_eq!(c.pedestrians(), &[walker()]);
assert_eq!(c.walls().len(), 1);
assert_eq!(*c.model(), m);
assert_eq!(Participant::kind(&c), ParticipantKind::new(0x4352_5744));
c.pedestrians_mut()[0].velocity = v2(3, 4);
let mut bytes = Vec::new();
c.write_state(&mut bytes);
assert_eq!(bytes.len(), 20 + 160);
assert_eq!(bytes[..4], 1_u32.to_le_bytes());
assert_eq!(bytes[12..20], 1_u64.to_le_bytes());
assert_eq!(c.check_state(&bytes), Ok(()));
let mut d = crowd();
d.read_state(&bytes);
assert_eq!(d.pedestrians(), c.pedestrians());
assert_eq!(
c.check_state(&bytes[..10]),
Err(StateError::Length {
expected: 20,
found: 10
})
);
let mut version = bytes.clone();
version[0] = 2;
assert_eq!(c.check_state(&version), Err(StateError::InvalidValue));
let mut digest = bytes.clone();
digest[4] ^= 1;
assert_eq!(c.check_state(&digest), Err(StateError::InvalidValue));
assert_eq!(
c.check_state(&bytes[..100]),
Err(StateError::Length {
expected: 180,
found: 100
})
);
let mut huge = bytes.clone();
huge[12..20].copy_from_slice(&u64::MAX.to_le_bytes());
assert!(matches!(
c.check_state(&huge),
Err(StateError::Length { .. })
));
let capped = CrowdParticipant::new(
m,
vec![walker()],
c.walls().to_vec(),
NeighborSearch::Direct,
Some(Fix128::ONE),
)
.expect("valid crowd");
assert_eq!(capped.check_state(&bytes), Err(StateError::InvalidValue));
c.pedestrians_mut()[0].velocity = v2(3, 4);
let mut two = crowd();
two.pedestrians = vec![c.pedestrians()[0], walker()];
let mut sink = ObservationSink::new();
two.observe(&mut sink);
assert_eq!(
sink.values(),
&[
(CROWD_OBS_COUNT, Fix128::from_int(2)),
(CROWD_OBS_MEAN_SPEED, Fix128::from_ratio(5, 2))
]
);
two.pedestrians.clear();
let mut sink = ObservationSink::new();
two.observe(&mut sink);
assert_eq!(sink.values(), &[(CROWD_OBS_COUNT, Fix128::ZERO)]);
}
#[cfg(feature = "std")]
#[test]
fn crowd_participant_in_a_world() {
let world_of = |c: CrowdParticipant| {
let mut world = crate::solver::PhysicsWorld::new(crate::solver::SolverConfig {
substeps: 1,
..Default::default()
});
world.add_participant(Box::new(c)).expect("register");
world.step(Fix128::from_ratio(1, 4));
world
};
let world = world_of(crowd());
let Some(crate::world_participant::Observed::Exact(sink)) = world.observe_participant(0)
else {
panic!("observation");
};
assert_eq!(
sink.values(),
&[
(CROWD_OBS_COUNT, Fix128::ONE),
(CROWD_OBS_MEAN_SPEED, Fix128::from_ratio(1, 2))
]
);
let mut runaway = crowd();
runaway.pedestrians_mut()[0].position =
Vec2Fix::new(Fix128::from_int(i64::MAX - 1), Fix128::ZERO);
runaway.pedestrians_mut()[0].velocity =
Vec2Fix::new(Fix128::from_int(1 << 62), Fix128::ZERO);
let world = world_of(runaway);
assert!(
matches!(
world.fault(),
Some(crate::world_participant::WorldFault::Participant {
fault: ParticipantFault::OutOfRange,
..
})
),
"{:?}",
world.fault()
);
}
}