rustsim 0.0.1

High-performance agent-based modelling engine - top-level orchestration crate
Documentation
//! Periodic-corridor crowd example driven through the `rustsim-core`
//! agent store.
//!
//! This example is the canonical end-to-end usage pattern for the
//! `rustsim-crowd` + `rustsim-core` bridge:
//!
//! 1. A [`VecStore<CrowdAgent>`] owns identified pedestrian state.
//! 2. One [`Scratch`] and one `Vec<Pedestrian>` are allocated once and
//!    reused every tick (zero-alloc hot path).
//! 3. [`step_scratch_store`] drives any of the five microscopic models
//!    (here: Social Force) over the store.
//! 4. Walls wrap a bidirectional corridor; agents are manually wrapped
//!    across the periodic x-boundary every tick and re-targeted so they
//!    keep their original direction.
//!
//! Run with:
//!
//! ```bash
//! cargo run -p rustsim --example crowd_corridor --release
//! ```
//!
//! The example prints a small header and then a CSV-ish density /
//! mean-speed sample every `REPORT_EVERY` ticks, plus a final
//! fundamental-diagram row that external tools can grep for.

use rustsim::rustsim_core::prelude::{AgentStore, VecStore};
use rustsim::rustsim_crowd::prelude::*;
use rustsim::rustsim_crowd::social_force;

// ---- Configuration ----------------------------------------------------

/// Corridor length (periodic in x).
const CORRIDOR_L: f64 = 20.0;
/// Corridor width.
const CORRIDOR_W: f64 = 3.0;
/// Target density (ped / m²). Picked above Weidmann's inflection so
/// the counter-flow noticeably slows down.
const DENSITY: f64 = 2.0;
/// Integration step (s).
const DT: f64 = 0.05;
/// Number of ticks to run (20 s simulated time at DT = 0.05 s).
const NUM_TICKS: usize = 400;
/// Pedestrian body radius (m).
const RADIUS: f64 = 0.25;
/// Free-flow desired speed (m/s). Weidmann mean.
const DESIRED_SPEED: f64 = 1.34;
/// How often to print a sample.
const REPORT_EVERY: usize = 40;

// ---- Setup helpers ----------------------------------------------------

/// Horizontal walls forming a corridor of width `W` centred on `y = 0`.
fn corridor_walls() -> Vec<WallSegment> {
    let half = CORRIDOR_W / 2.0;
    vec![
        WallSegment {
            a: [0.0, half],
            b: [CORRIDOR_L, half],
        },
        WallSegment {
            a: [0.0, -half],
            b: [CORRIDOR_L, -half],
        },
    ]
}

/// Deterministic jittered counter-flow seeding. Half the agents face
/// `+x`, half `-x`. A small sub-cell jitter breaks lattice symmetry so
/// pair repulsions do not cancel in aggregate.
fn seed_counterflow(store: &mut VecStore<CrowdAgent>) -> usize {
    let usable_w = (CORRIDOR_W - 2.0 * RADIUS).max(0.01);
    let n = (DENSITY * CORRIDOR_L * CORRIDOR_W).round() as usize;
    let cols = ((CORRIDOR_L / 1.5).ceil() as usize).max(1);
    let rows = n.div_ceil(cols);
    let dx = CORRIDOR_L / cols as f64;
    let dy = usable_w / rows.max(1) as f64;
    for k in 0..n {
        let c = k % cols;
        let r = k / cols;
        // Deterministic Knuth-style hash for jitter in [-0.4, 0.4] cell.
        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.8 * jx) * dx;
        let y = -usable_w / 2.0 + (r as f64 + 0.5 + 0.8 * jy) * dy;
        let dir: f64 = if k % 2 == 0 { 1.0 } else { -1.0 };
        store.insert(CrowdAgent {
            id: k as u64,
            ped: Pedestrian::new(
                [x, y],
                [0.5 * DESIRED_SPEED * dir, 0.0],
                RADIUS,
                DESIRED_SPEED,
                [x + 100.0 * dir, 0.0],
            ),
        });
    }
    n
}

/// Wrap every agent's `pos.x` into `[0, L)` and shift its destination
/// by the same amount so heading stays stable across the periodic seam.
fn wrap_periodic(store: &mut VecStore<CrowdAgent>) {
    let ids = store.iter_ids();
    for id in ids {
        if let Some(mut a) = store.get_mut(id) {
            let x = a.ped.pos[0];
            let wrapped = x.rem_euclid(CORRIDOR_L);
            let shift = x - wrapped;
            a.ped.pos[0] = wrapped;
            a.ped.destination[0] -= shift;
        }
    }
}

/// Arithmetic-mean speed across the store.
fn mean_speed(store: &VecStore<CrowdAgent>) -> f64 {
    let ids = store.iter_ids();
    if ids.is_empty() {
        return 0.0;
    }
    let mut s = 0.0;
    for id in &ids {
        if let Some(a) = store.get(*id) {
            s += (a.ped.vel[0].powi(2) + a.ped.vel[1].powi(2)).sqrt();
        }
    }
    s / ids.len() as f64
}

// ---- Main -------------------------------------------------------------

fn main() {
    // Validate the parameters once up front. This is the recommended
    // start-of-simulation gate: it catches CFL violations and bad
    // physical parameters before a single tick runs.
    let params = social_force::Params::default();
    params
        .validate(DT)
        .expect("social_force::Params::default() should validate at DT = 0.05 s");

    // Build the agent store and walls.
    let mut store: VecStore<CrowdAgent> = VecStore::new();
    let n = seed_counterflow(&mut store);
    let walls = corridor_walls();

    // One Scratch + one Pedestrian buffer, reused every tick.
    let cell = recommended_cell_size(social_force::neighbor_cutoff(&params));
    let mut scratch = Scratch::with_capacity(n, cell);
    let mut peds_buf: Vec<Pedestrian> = Vec::with_capacity(n);

    // Header.
    println!("# rustsim-crowd corridor example");
    println!("# n = {n}, density = {DENSITY} ped/m^2, corridor = {CORRIDOR_L} x {CORRIDOR_W} m");
    println!("# dt = {DT} s, model = Social Force (Helbing & Molnár 1995)");
    println!("tick,t_s,mean_speed_m_s,v_ratio");

    // Accumulators over the last third of the run for a steady-state
    // fundamental-diagram row.
    let warmup = (NUM_TICKS * 2) / 3;
    let mut ss_accum = 0.0;
    let mut ss_samples = 0usize;

    for tick in 0..NUM_TICKS {
        wrap_periodic(&mut store);
        step_scratch_store(
            &SocialForceModel,
            &mut store,
            &walls,
            &params,
            DT,
            &mut scratch,
            &mut peds_buf,
        );

        if tick % REPORT_EVERY == 0 {
            let v = mean_speed(&store);
            println!(
                "{tick},{:.2},{v:.3},{:.3}",
                tick as f64 * DT,
                v / DESIRED_SPEED
            );
        }

        if tick >= warmup {
            ss_accum += mean_speed(&store);
            ss_samples += 1;
        }
    }

    let v_ss = ss_accum / ss_samples.max(1) as f64;
    println!(
        "# steady_state: density = {DENSITY:.2} ped/m^2, mean_speed = {v_ss:.3} m/s, \
         v/v0 = {:.3}",
        v_ss / DESIRED_SPEED
    );
}