rustsim-crowd 0.0.1

Microscopic crowd and pedestrian locomotion for rustsim: 2-D and layered 3-D, with Social Force, Collision-Free Speed, Generalized Centrifugal Force, Optimal Steps, and Anticipation Velocity models
Documentation
//! CUDA vs CPU equivalence for `rustsim-crowd::cuda::optimal_steps`.
//!
//! Gated on the `cuda` feature. Skips cleanly on hosts without a
//! working CUDA driver, mirroring the SFM/GCF/CFS/AVM equivalence
//! tests.
//!
//! GPU arm runs `f32`; CPU reference runs `f64`. OSM is a discrete
//! kinematic model whose chosen step is the minimum of an
//! exponential-penalty utility over a polar candidate set. That
//! exponential plus per-candidate `cosf`/`sinf` calls puts OSM in
//! the same precision ballpark as AVM, so we use the same `1e-1 m`
//! positional and `1e-1 m/s` velocity tolerance after 5 ticks of a
//! 16-pedestrian counter-flow with two walls — well below the
//! 0.25 m agent radius and the OSM stride length (≤ 0.5 m).

#![cfg(feature = "cuda")]
#![allow(deprecated)]

use rustsim_crowd::common::{Pedestrian, WallSegment};
use rustsim_crowd::cuda::optimal_steps as osm_cuda;
use rustsim_crowd::optimal_steps;

fn seed() -> Vec<Pedestrian> {
    (0..16)
        .map(|i| {
            let lane = (i % 2) as f64;
            let col = (i / 2) as f64;
            let dir = if i % 2 == 0 { 1.0 } else { -1.0 };
            Pedestrian::new(
                [col * 1.5, lane * 1.5],
                [0.0, 0.0],
                0.25,
                1.34,
                [dir * 50.0, lane * 1.5],
            )
        })
        .collect()
}

#[test]
fn cuda_osm_matches_cpu_within_tolerance() {
    let mut peds_cpu = seed();
    let mut peds_gpu = seed();
    let walls = vec![
        WallSegment {
            a: [-20.0, -1.0],
            b: [20.0, -1.0],
        },
        WallSegment {
            a: [-20.0, 2.5],
            b: [20.0, 2.5],
        },
    ];
    let params = optimal_steps::Params::default();
    let dt = 0.4;

    let state = match osm_cuda::CudaState::new() {
        Ok(s) => s,
        Err(e) => {
            eprintln!("skipping cuda_osm_matches_cpu_within_tolerance: no CUDA device ({e})");
            return;
        }
    };

    for _ in 0..5 {
        optimal_steps::step(&mut peds_cpu, &walls, &params, dt);
        state
            .step(&mut peds_gpu, &walls, &params, dt)
            .expect("CUDA OSM step failed after successful init");
    }

    for (i, (a, b)) in peds_cpu.iter().zip(peds_gpu.iter()).enumerate() {
        let dx = (a.pos[0] - b.pos[0]).abs();
        let dy = (a.pos[1] - b.pos[1]).abs();
        let dvx = (a.vel[0] - b.vel[0]).abs();
        let dvy = (a.vel[1] - b.vel[1]).abs();
        assert!(
            dx < 1e-1 && dy < 1e-1,
            "agent {i} position diverged: cpu={:?} gpu={:?}",
            a.pos,
            b.pos
        );
        assert!(
            dvx < 1e-1 && dvy < 1e-1,
            "agent {i} velocity diverged: cpu={:?} gpu={:?}",
            a.vel,
            b.vel
        );
    }
}

#[test]
fn cuda_osm_resident_matches_stateless_within_tolerance() {
    let mut peds_stateless = seed();
    let peds_initial = seed();
    let walls = vec![
        WallSegment {
            a: [-20.0, -1.0],
            b: [20.0, -1.0],
        },
        WallSegment {
            a: [-20.0, 2.5],
            b: [20.0, 2.5],
        },
    ];
    let params = optimal_steps::Params::default();
    let dt = 0.4;

    let state = match osm_cuda::CudaState::new() {
        Ok(s) => s,
        Err(e) => {
            eprintln!("skipping cuda_osm_resident_matches_stateless_within_tolerance: no CUDA device ({e})");
            return;
        }
    };
    let mut resident = osm_cuda::CudaResident::upload(&peds_initial, &walls)
        .expect("OSM resident upload failed after successful CUDA init");

    for _ in 0..5 {
        state
            .step(&mut peds_stateless, &walls, &params, dt)
            .expect("CUDA OSM stateless step failed after successful init");
        resident
            .step(&params, dt)
            .expect("CUDA OSM resident step failed after successful upload");
    }

    let mut peds_resident = Vec::new();
    resident
        .download(&mut peds_resident)
        .expect("CUDA OSM resident download failed");

    assert_eq!(resident.len(), peds_stateless.len());
    assert!(!resident.is_empty());
    for (i, (a, b)) in peds_stateless.iter().zip(peds_resident.iter()).enumerate() {
        let dx = (a.pos[0] - b.pos[0]).abs();
        let dy = (a.pos[1] - b.pos[1]).abs();
        let dvx = (a.vel[0] - b.vel[0]).abs();
        let dvy = (a.vel[1] - b.vel[1]).abs();
        assert!(
            dx < 1e-5 && dy < 1e-5,
            "agent {i} resident position diverged: stateless={:?} resident={:?}",
            a.pos,
            b.pos
        );
        assert!(
            dvx < 1e-5 && dvy < 1e-5,
            "agent {i} resident velocity diverged: stateless={:?} resident={:?}",
            a.vel,
            b.vel
        );
    }
}

#[test]
fn cuda_osm_step_with_fallback_runs_on_cpu_when_no_device() {
    let mut peds = seed();
    let walls: Vec<WallSegment> = Vec::new();
    let params = optimal_steps::Params::default();
    let mut state: Option<osm_cuda::CudaState> = None;
    let _ok = osm_cuda::step_with_fallback(&mut state, &mut peds, &walls, &params, 0.4);
    for p in &peds {
        assert!(
            p.pos[0].is_finite() && p.pos[1].is_finite(),
            "non-finite post-step position: {:?}",
            p.pos
        );
        assert!(
            p.vel[0].is_finite() && p.vel[1].is_finite(),
            "non-finite post-step velocity: {:?}",
            p.vel
        );
    }
}