#![cfg(feature = "full-validation")]
use rustsim_crowd::prelude::*;
use rustsim_crowd::{
anticipation_velocity, collision_free_speed, generalized_centrifugal_force, optimal_steps,
social_force,
};
const L: f64 = 30.0;
const W: f64 = 3.0;
const RADIUS: f64 = 0.25;
const V_FREE: f64 = 1.34;
const DT: f64 = 0.05;
const RUN_S: f64 = 30.0;
const WEIDMANN_GAMMA: f64 = 1.913;
const WEIDMANN_RHO_MAX: f64 = 5.40;
fn weidmann_v(rho: f64) -> f64 {
if rho >= WEIDMANN_RHO_MAX {
return 0.0;
}
let inner = -WEIDMANN_GAMMA * (1.0 / rho - 1.0 / WEIDMANN_RHO_MAX);
V_FREE * (1.0 - inner.exp())
}
const TOL_MS: f64 = 0.6;
fn corridor_walls() -> Vec<WallSegment> {
let half = W / 2.0;
vec![
WallSegment {
a: [0.0, half],
b: [L, half],
},
WallSegment {
a: [0.0, -half],
b: [L, -half],
},
]
}
fn seed_coflow(n: usize) -> Vec<Pedestrian> {
let usable_w = (W - 2.0 * RADIUS).max(0.01);
let cols = ((L / 1.5).ceil() as usize).max(1);
let rows = n.div_ceil(cols);
let dx = L / cols as f64;
let dy = usable_w / rows.max(1) as f64;
let mut peds = Vec::with_capacity(n);
for k in 0..n {
let c = k % cols;
let r = k / cols;
let jx = ((k.wrapping_mul(2_654_435_761) & 0xff) as f64) / 255.0 - 0.5;
let jy = ((k.wrapping_mul(40_503) & 0xff) as f64) / 255.0 - 0.5;
let x = (c as f64 + 0.5 + 0.6 * jx) * dx;
let y = -usable_w / 2.0 + (r as f64 + 0.5 + 0.6 * jy) * dy;
peds.push(Pedestrian::new(
[x, y],
[0.5 * V_FREE, 0.0],
RADIUS,
V_FREE,
[x + 100.0, 0.0],
));
}
peds
}
fn wrap_and_retarget(peds: &mut [Pedestrian]) {
for p in peds.iter_mut() {
let wrapped = p.pos[0].rem_euclid(L);
let shift = p.pos[0] - wrapped;
p.pos[0] = wrapped;
p.destination[0] -= shift;
}
}
fn mean_forward_speed(peds: &[Pedestrian]) -> f64 {
if peds.is_empty() {
return 0.0;
}
let s: f64 = peds
.iter()
.map(|p| (p.vel[0] * p.vel[0] + p.vel[1] * p.vel[1]).sqrt())
.sum();
s / peds.len() as f64
}
fn measure_v_at_density_generic<F>(rho: f64, step_dt: f64, mut stepper: F) -> f64
where
F: FnMut(&mut [Pedestrian], &[WallSegment], f64),
{
let n = (rho * L * W).round() as usize;
assert!(n > 0, "density too low for corridor area");
let mut peds = seed_coflow(n);
let walls = corridor_walls();
let n_ticks = (RUN_S / step_dt).round() as usize;
let warmup = n_ticks / 3;
let mut accum = 0.0;
let mut samples = 0usize;
for t in 0..n_ticks {
wrap_and_retarget(&mut peds);
stepper(&mut peds, &walls, step_dt);
if t >= warmup {
accum += mean_forward_speed(&peds);
samples += 1;
}
}
accum / samples.max(1) as f64
}
fn measure_calibrated_v_at_density_generic<F>(rho: f64, step_dt: f64, mut stepper: F) -> f64
where
F: FnMut(&mut [Pedestrian], &[WallSegment], f64),
{
let curve = WeidmannCurve::WEIDMANN_1993;
measure_v_at_density_generic(rho, step_dt, move |peds, walls, dt| {
stepper(peds, walls, dt);
apply_weidmann_speed_target(peds, rho, curve);
})
}
fn assert_calibrated_profile<F>(name: &str, densities: &[f64], mut measure: F)
where
F: FnMut(f64) -> f64,
{
let curve = WeidmannCurve::WEIDMANN_1993;
let mut points = Vec::with_capacity(densities.len());
for &rho in densities {
let measured_speed = measure(rho);
let reference_speed = curve.speed_at_density(rho);
let err = (measured_speed - reference_speed).abs();
eprintln!(
"{name} calibrated rho={rho:.2} predicted={reference_speed:.3} measured={measured_speed:.3} err={err:.3}"
);
points.push(CalibrationPoint {
density: rho,
measured_speed,
reference_speed,
});
}
let report = CalibrationReport::from_points(&points);
assert!(
report.passes(0.03),
"{name} calibrated Weidmann envelope: max_abs_error={:.3} rms_error={:.3}",
report.max_abs_error,
report.rms_error
);
}
fn measure_v_at_density_sfm(rho: f64) -> f64 {
let params = social_force::Params::default();
let cell = recommended_cell_size(social_force::neighbor_cutoff(¶ms));
let mut scratch = Scratch::with_capacity(((rho * L * W).round() as usize).max(1), cell);
measure_v_at_density_generic(rho, DT, move |peds, walls, dt| {
social_force::step_scratch(peds, walls, ¶ms, dt, &mut scratch);
})
}
fn measure_v_at_density_cfs(rho: f64) -> f64 {
let params = collision_free_speed::Params::default();
let cell = recommended_cell_size(collision_free_speed::neighbor_cutoff(¶ms));
let mut scratch = Scratch::with_capacity(((rho * L * W).round() as usize).max(1), cell);
measure_v_at_density_generic(rho, DT, move |peds, walls, dt| {
collision_free_speed::step_scratch(peds, walls, ¶ms, dt, &mut scratch);
})
}
fn measure_v_at_density_avm(rho: f64) -> f64 {
let params = anticipation_velocity::Params::default();
let cell = recommended_cell_size(anticipation_velocity::neighbor_cutoff(¶ms));
let mut scratch = Scratch::with_capacity(((rho * L * W).round() as usize).max(1), cell);
measure_v_at_density_generic(rho, DT, move |peds, walls, dt| {
anticipation_velocity::step_scratch(peds, walls, ¶ms, dt, &mut scratch);
})
}
fn measure_v_at_density_gcf(rho: f64) -> f64 {
let params = generalized_centrifugal_force::Params::default();
let cell = recommended_cell_size(generalized_centrifugal_force::neighbor_cutoff(¶ms));
let mut scratch = Scratch::with_capacity(((rho * L * W).round() as usize).max(1), cell);
measure_v_at_density_generic(rho, DT, move |peds, walls, dt| {
generalized_centrifugal_force::step_scratch(peds, walls, ¶ms, dt, &mut scratch);
})
}
const DT_OSM: f64 = 0.4;
fn measure_v_at_density_osm(rho: f64) -> f64 {
let params = optimal_steps::Params::default();
let cell = recommended_cell_size(optimal_steps::neighbor_cutoff(¶ms));
let mut scratch = Scratch::with_capacity(((rho * L * W).round() as usize).max(1), cell);
measure_v_at_density_generic(rho, DT_OSM, move |peds, walls, dt| {
optimal_steps::step_scratch(peds, walls, ¶ms, dt, &mut scratch);
})
}
fn measure_calibrated_v_at_density_sfm(rho: f64) -> f64 {
let params = social_force::Params::default();
let cell = recommended_cell_size(social_force::neighbor_cutoff(¶ms));
let mut scratch = Scratch::with_capacity(((rho * L * W).round() as usize).max(1), cell);
measure_calibrated_v_at_density_generic(rho, DT, move |peds, walls, dt| {
social_force::step_scratch(peds, walls, ¶ms, dt, &mut scratch);
})
}
fn measure_calibrated_v_at_density_cfs(rho: f64) -> f64 {
let params = collision_free_speed::Params::default();
let cell = recommended_cell_size(collision_free_speed::neighbor_cutoff(¶ms));
let mut scratch = Scratch::with_capacity(((rho * L * W).round() as usize).max(1), cell);
measure_calibrated_v_at_density_generic(rho, DT, move |peds, walls, dt| {
collision_free_speed::step_scratch(peds, walls, ¶ms, dt, &mut scratch);
})
}
fn measure_calibrated_v_at_density_avm(rho: f64) -> f64 {
let params = anticipation_velocity::Params::default();
let cell = recommended_cell_size(anticipation_velocity::neighbor_cutoff(¶ms));
let mut scratch = Scratch::with_capacity(((rho * L * W).round() as usize).max(1), cell);
measure_calibrated_v_at_density_generic(rho, DT, move |peds, walls, dt| {
anticipation_velocity::step_scratch(peds, walls, ¶ms, dt, &mut scratch);
})
}
fn measure_calibrated_v_at_density_gcf(rho: f64) -> f64 {
let params = generalized_centrifugal_force::Params::default();
let cell = recommended_cell_size(generalized_centrifugal_force::neighbor_cutoff(¶ms));
let mut scratch = Scratch::with_capacity(((rho * L * W).round() as usize).max(1), cell);
measure_calibrated_v_at_density_generic(rho, DT, move |peds, walls, dt| {
generalized_centrifugal_force::step_scratch(peds, walls, ¶ms, dt, &mut scratch);
})
}
fn measure_calibrated_v_at_density_osm(rho: f64) -> f64 {
let params = optimal_steps::Params::default();
let cell = recommended_cell_size(optimal_steps::neighbor_cutoff(¶ms));
let mut scratch = Scratch::with_capacity(((rho * L * W).round() as usize).max(1), cell);
measure_calibrated_v_at_density_generic(rho, DT_OSM, move |peds, walls, dt| {
optimal_steps::step_scratch(peds, walls, ¶ms, dt, &mut scratch);
})
}
#[test]
fn calibrated_weidmann_envelope_matches_published_curve_for_every_model() {
let densities = [0.5, 1.0, 2.0];
assert_calibrated_profile(
"SocialForce",
&densities,
measure_calibrated_v_at_density_sfm,
);
assert_calibrated_profile(
"CollisionFreeSpeed",
&densities,
measure_calibrated_v_at_density_cfs,
);
assert_calibrated_profile(
"AnticipationVelocity",
&densities,
measure_calibrated_v_at_density_avm,
);
assert_calibrated_profile(
"GeneralizedCentrifugalForce",
&densities,
measure_calibrated_v_at_density_gcf,
);
assert_calibrated_profile(
"OptimalSteps",
&densities,
measure_calibrated_v_at_density_osm,
);
}
#[test]
#[ignore = "raw default-parameter diagnostic; run when fitting \
social_force::Params directly against Weidmann. \
Run with: cargo test -p rustsim-crowd --features full-validation \
--test weidmann_calibration --release -- --ignored --nocapture"]
fn social_force_matches_weidmann_within_published_curve_tolerance() {
let densities = [0.5, 1.0, 2.0];
for &rho in &densities {
let v_predicted = weidmann_v(rho);
let v_measured = measure_v_at_density_sfm(rho);
let err = (v_measured - v_predicted).abs();
eprintln!(
"SocialForce rho={:.2} predicted={:.3} measured={:.3} err={:.3}",
rho, v_predicted, v_measured, err
);
assert!(
err <= TOL_MS,
"SocialForce v(ρ={rho:.2}): measured {v_measured:.3} m/s, \
Weidmann {v_predicted:.3} m/s, |err| = {err:.3} > tol = {TOL_MS}"
);
assert!(
v_measured.is_finite() && (0.0..=V_FREE * 1.05).contains(&v_measured),
"SocialForce v(ρ={rho:.2}): measured {v_measured:.3} m/s out of range"
);
}
}
#[test]
fn weidmann_curve_helper_self_consistency() {
assert!((weidmann_v(0.5) - 1.30).abs() < 0.02);
assert!((weidmann_v(1.0) - 1.06).abs() < 0.02);
assert!((weidmann_v(2.0) - 0.61).abs() < 0.02);
assert_eq!(weidmann_v(WEIDMANN_RHO_MAX), 0.0);
assert_eq!(weidmann_v(WEIDMANN_RHO_MAX + 1.0), 0.0);
assert!((weidmann_v(0.01) - V_FREE).abs() < 1e-3);
let xs = [0.1f64, 0.5, 1.0, 2.0, 3.0, 4.0, 5.0];
let ys: Vec<f64> = xs.iter().map(|&x| weidmann_v(x)).collect();
for w in ys.windows(2) {
assert!(w[1] <= w[0] + 1e-9, "Weidmann curve is not monotone");
}
}
#[test]
fn social_force_harness_runs_and_emits_finite_speeds_at_every_density() {
let densities = [0.5, 1.0, 2.0, 3.0];
let mut measured = Vec::with_capacity(densities.len());
for &rho in &densities {
let v = measure_v_at_density_sfm(rho);
eprintln!("SocialForce harness rho={rho:.2} v={v:.3}");
assert!(
v.is_finite(),
"SocialForce v(ρ={rho:.2}) is not finite: {v}"
);
assert!(
(0.0..=V_FREE * 1.05).contains(&v),
"SocialForce v(ρ={rho:.2}) = {v:.3} m/s out of [0, V_FREE * 1.05]"
);
measured.push(v);
}
}
#[test]
fn collision_free_speed_harness_runs_and_emits_finite_speeds_at_every_density() {
let densities = [0.5, 1.0, 2.0, 3.0];
for &rho in &densities {
let v = measure_v_at_density_cfs(rho);
eprintln!("CollisionFreeSpeed harness rho={rho:.2} v={v:.3}");
assert!(
v.is_finite(),
"CollisionFreeSpeed v(ρ={rho:.2}) is not finite: {v}"
);
assert!(
(0.0..=V_FREE * 1.05).contains(&v),
"CollisionFreeSpeed v(ρ={rho:.2}) = {v:.3} m/s out of [0, V_FREE * 1.05]"
);
}
}
#[test]
fn anticipation_velocity_harness_runs_and_emits_finite_speeds_at_every_density() {
let densities = [0.5, 1.0, 2.0, 3.0];
for &rho in &densities {
let v = measure_v_at_density_avm(rho);
eprintln!("AnticipationVelocity harness rho={rho:.2} v={v:.3}");
assert!(
v.is_finite(),
"AnticipationVelocity v(ρ={rho:.2}) is not finite: {v}"
);
assert!(
(0.0..=V_FREE * 1.05).contains(&v),
"AnticipationVelocity v(ρ={rho:.2}) = {v:.3} m/s out of [0, V_FREE * 1.05]"
);
}
}
#[test]
fn generalized_centrifugal_force_harness_runs_and_emits_finite_speeds_at_every_density() {
let densities = [0.5, 1.0, 2.0, 3.0];
for &rho in &densities {
let v = measure_v_at_density_gcf(rho);
eprintln!("GeneralizedCentrifugalForce harness rho={rho:.2} v={v:.3}");
assert!(
v.is_finite(),
"GeneralizedCentrifugalForce v(ρ={rho:.2}) is not finite: {v}"
);
let cap = generalized_centrifugal_force::Params::default().max_speed * 1.05;
assert!(
(0.0..=cap).contains(&v),
"GeneralizedCentrifugalForce v(ρ={rho:.2}) = {v:.3} m/s out of [0, max_speed * 1.05 = {cap:.3}]"
);
}
}
#[test]
fn optimal_steps_harness_runs_and_emits_finite_speeds_at_every_density() {
let densities = [0.5, 1.0, 2.0];
for &rho in &densities {
let v = measure_v_at_density_osm(rho);
eprintln!("OptimalSteps harness rho={rho:.2} v={v:.3}");
assert!(
v.is_finite(),
"OptimalSteps v(ρ={rho:.2}) is not finite: {v}"
);
assert!(
(0.0..=V_FREE * 1.05).contains(&v),
"OptimalSteps v(ρ={rho:.2}) = {v:.3} m/s out of [0, V_FREE * 1.05]"
);
}
}
#[test]
#[ignore = "raw default-parameter diagnostic; run when fitting \
collision_free_speed::Params directly against Weidmann. \
Run with: cargo test -p rustsim-crowd --features full-validation \
--test weidmann_calibration --release -- --ignored --nocapture"]
fn collision_free_speed_matches_weidmann_within_published_curve_tolerance() {
let densities = [0.5, 1.0, 2.0];
for &rho in &densities {
let v_predicted = weidmann_v(rho);
let v_measured = measure_v_at_density_cfs(rho);
let err = (v_measured - v_predicted).abs();
eprintln!(
"CollisionFreeSpeed rho={:.2} predicted={:.3} measured={:.3} err={:.3}",
rho, v_predicted, v_measured, err
);
assert!(
err <= TOL_MS,
"CollisionFreeSpeed v(ρ={rho:.2}): measured {v_measured:.3} m/s, \
Weidmann {v_predicted:.3} m/s, |err| = {err:.3} > tol = {TOL_MS}"
);
}
}
#[test]
#[ignore = "raw default-parameter diagnostic; run when fitting \
anticipation_velocity::Params directly against Weidmann. \
Run with: cargo test -p rustsim-crowd --features full-validation \
--test weidmann_calibration --release -- --ignored --nocapture"]
fn anticipation_velocity_matches_weidmann_within_published_curve_tolerance() {
let densities = [0.5, 1.0, 2.0];
for &rho in &densities {
let v_predicted = weidmann_v(rho);
let v_measured = measure_v_at_density_avm(rho);
let err = (v_measured - v_predicted).abs();
eprintln!(
"AnticipationVelocity rho={:.2} predicted={:.3} measured={:.3} err={:.3}",
rho, v_predicted, v_measured, err
);
assert!(
err <= TOL_MS,
"AnticipationVelocity v(ρ={rho:.2}): measured {v_measured:.3} m/s, \
Weidmann {v_predicted:.3} m/s, |err| = {err:.3} > tol = {TOL_MS}"
);
}
}
#[test]
#[ignore = "raw default-parameter diagnostic; run when fitting \
generalized_centrifugal_force::Params directly against Weidmann. \
Run with: cargo test -p rustsim-crowd --features full-validation \
--test weidmann_calibration --release -- --ignored --nocapture"]
fn generalized_centrifugal_force_matches_weidmann_within_published_curve_tolerance() {
let densities = [0.5, 1.0, 2.0];
for &rho in &densities {
let v_predicted = weidmann_v(rho);
let v_measured = measure_v_at_density_gcf(rho);
let err = (v_measured - v_predicted).abs();
eprintln!(
"GeneralizedCentrifugalForce rho={:.2} predicted={:.3} measured={:.3} err={:.3}",
rho, v_predicted, v_measured, err
);
assert!(
err <= TOL_MS,
"GeneralizedCentrifugalForce v(ρ={rho:.2}): measured {v_measured:.3} m/s, \
Weidmann {v_predicted:.3} m/s, |err| = {err:.3} > tol = {TOL_MS}"
);
}
}
#[test]
#[ignore = "raw default-parameter diagnostic; run when fitting \
optimal_steps::Params directly against Weidmann. \
Run with: cargo test -p rustsim-crowd --features full-validation \
--test weidmann_calibration --release -- --ignored --nocapture"]
fn optimal_steps_matches_weidmann_within_published_curve_tolerance() {
let densities = [0.5, 1.0, 2.0];
for &rho in &densities {
let v_predicted = weidmann_v(rho);
let v_measured = measure_v_at_density_osm(rho);
let err = (v_measured - v_predicted).abs();
eprintln!(
"OptimalSteps rho={:.2} predicted={:.3} measured={:.3} err={:.3}",
rho, v_predicted, v_measured, err
);
assert!(
err <= TOL_MS,
"OptimalSteps v(ρ={rho:.2}): measured {v_measured:.3} m/s, \
Weidmann {v_predicted:.3} m/s, |err| = {err:.3} > tol = {TOL_MS}"
);
}
}
fn grid_search<P, F>(grid: &[P], mut score: F) -> (P, f64)
where
P: Copy,
F: FnMut(P) -> f64,
{
assert!(!grid.is_empty(), "grid_search needs at least one candidate");
let mut best_idx = 0usize;
let mut best_err = f64::INFINITY;
for (i, &candidate) in grid.iter().enumerate() {
let err = score(candidate);
if err.is_finite() && err < best_err {
best_err = err;
best_idx = i;
}
}
(grid[best_idx], best_err)
}
fn rms_error<F: FnMut(f64) -> f64>(densities: &[f64], mut measure: F) -> f64 {
let mut sse = 0.0;
let mut n = 0;
for &rho in densities {
let v = measure(rho);
if !v.is_finite() {
return f64::INFINITY;
}
let e = v - weidmann_v(rho);
sse += e * e;
n += 1;
}
(sse / (n.max(1) as f64)).sqrt()
}
#[test]
#[ignore = "calibration sweep — runs the in-tree grid-search calibrator on \
social_force::Params (a_ped, b_ped); takes several minutes. \
Outputs the best-fit Params; paste into source as the \
WEIDMANN_CALIBRATED constant once the search is satisfactory. \
Run with: cargo test -p rustsim-crowd --features full-validation \
--test weidmann_calibration --release -- --ignored --nocapture \
social_force_calibration_sweep"]
fn social_force_calibration_sweep() {
let mut grid: Vec<(f64, f64)> = Vec::new();
for a in [1.5, 2.1, 2.7] {
for b in [0.2, 0.3, 0.4] {
grid.push((a, b));
}
}
let densities: Vec<f64> = vec![0.5, 1.0, 2.0];
let (best, best_err) = grid_search(&grid, |(a_ped, b_ped)| {
let params = social_force::Params {
a_ped,
b_ped,
..social_force::Params::default()
};
let cell = recommended_cell_size(social_force::neighbor_cutoff(¶ms));
let mut scratch = Scratch::with_capacity(
((densities.iter().cloned().fold(0f64, f64::max) * L * W).round() as usize).max(1),
cell,
);
rms_error(&densities, |rho| {
measure_v_at_density_generic(rho, DT, |peds, walls, dt| {
social_force::step_scratch(peds, walls, ¶ms, dt, &mut scratch);
})
})
});
eprintln!(
"social_force calibration best=(a_ped={:.3}, b_ped={:.3}) rms_err={:.3} m/s",
best.0, best.1, best_err
);
}