rustsim 0.0.1

High-performance agent-based modelling engine - top-level orchestration crate
Documentation
//! Layered 2.5-D crowd example: a two-floor station with a stair.
//!
//! Demonstrates the `rustsim_crowd::threed` module end-to-end:
//!
//! - A [`LayeredSpace`] with two floors (ground = 0 m, upper = 4 m) and
//!   one stair connector between them (10 s travel time, 1.0 m boarding
//!   radius).
//! - Two groups of pedestrians:
//!   * **Commuters** start on the ground floor with `target_floor =
//!     Some(1)` and head for the stair's boarding zone. They board,
//!     ride for 10 s, and alight on the upper floor.
//!   * **Loiterers** start on the ground floor with `target_floor =
//!     None` and walk toward a point *inside the stair's boarding
//!     zone* as their 2-D destination. Thanks to the planner-owned
//!     `target_floor` gate added for blocker #8, they do **not**
//!     re-board the stair every tick, they just sit at the goal.
//! - A per-tick histogram of `(floor, state)` so the demo output
//!   directly shows the board → ride → alight → settle lifecycle.
//!
//! Run with:
//!
//! ```bash
//! cargo run -p rustsim --example crowd_station --release
//! ```
//!
//! The example prints a summary header, a few state samples, and a
//! final "every commuter made it upstairs, every loiterer stayed
//! grounded on floor 0" assertion line.

use rustsim::rustsim_crowd::prelude::*;
use rustsim::rustsim_crowd::social_force;
use rustsim::rustsim_crowd::threed::{step_layered, ConnectorKind, FloorTransition, LayeredSpace};

// ---- Scenario -----------------------------------------------------------

const NUM_COMMUTERS: usize = 20;
const NUM_LOITERERS: usize = 5;
const DT: f64 = 0.1;
const NUM_TICKS: usize = 500; // 50 s simulated time.
const REPORT_EVERY: usize = 50;

/// Build a minimal two-floor station with one stair.
fn build_space() -> LayeredSpace {
    let mut space = LayeredSpace::new();
    space.set_floor(0, 0.0); // ground
    space.set_floor(1, 4.0); // upper floor at z = 4 m
    space.connectors.push(FloorTransition {
        id: 1,
        kind: ConnectorKind::Stair,
        from_floor: 0,
        from_pos: [10.0, 0.0],
        to_floor: 1,
        to_pos: [10.0, 0.0],
        boarding_radius: 1.0,
        travel_time: 10.0,
    });
    space
}

/// A commuter: starts on floor 0 away from the stair, walks toward the
/// stair's boarding zone, explicitly intends to reach floor 1.
fn commuter(i: usize) -> Pedestrian3D {
    let base = Pedestrian::new(
        [0.0, (i as f64 - NUM_COMMUTERS as f64 / 2.0) * 0.6],
        [0.0, 0.0],
        0.25,
        1.34,
        [10.0, 0.0],
    );
    Pedestrian3D::heading_to_floor(base, 0, 1)
}

/// A loiterer: starts on floor 0, walks toward a point that happens to
/// sit inside the stair's boarding zone, but has no intent to change
/// floor. Without the `target_floor` gate, these agents would re-board
/// the stair on every tick after completing their walk.
fn loiterer(i: usize) -> Pedestrian3D {
    let base = Pedestrian::new(
        [20.0 + i as f64 * 0.4, 0.0],
        [0.0, 0.0],
        0.25,
        1.34,
        // Goal *inside* the stair's boarding zone on floor 0.
        [10.0, 0.0],
    );
    Pedestrian3D::grounded(base, 0)
}

/// Categorise a pedestrian for the per-tick histogram.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum State {
    GroundedFloor0,
    GroundedFloor1,
    Riding,
}

fn state_of(p: &Pedestrian3D) -> State {
    if p.transition.is_some() {
        State::Riding
    } else if p.floor == 1 {
        State::GroundedFloor1
    } else {
        State::GroundedFloor0
    }
}

fn histogram(peds: &[Pedestrian3D]) -> (usize, usize, usize) {
    let mut g0 = 0;
    let mut g1 = 0;
    let mut riding = 0;
    for p in peds {
        match state_of(p) {
            State::GroundedFloor0 => g0 += 1,
            State::GroundedFloor1 => g1 += 1,
            State::Riding => riding += 1,
        }
    }
    (g0, g1, riding)
}

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

fn main() {
    // Validate parameters up front (same pattern as `crowd_corridor`).
    let params = social_force::Params::default();
    params
        .validate(DT)
        .expect("SFM defaults should validate at DT = 0.1 s");

    let space = build_space();

    let mut peds: Vec<Pedestrian3D> = (0..NUM_COMMUTERS)
        .map(commuter)
        .chain((0..NUM_LOITERERS).map(loiterer))
        .collect();

    println!("# rustsim-crowd two-floor station example");
    println!("# commuters = {NUM_COMMUTERS}, loiterers = {NUM_LOITERERS}");
    println!("# stair: floor 0 <-> floor 1, boarding_radius = 1.0 m, travel = 10 s");
    println!("tick,t_s,grounded_f0,grounded_f1,riding");

    for tick in 0..NUM_TICKS {
        // Example uses the reference O(n²) `step` for clarity; production
        // callers wrap a `Scratch` and pass `step_scratch` instead.
        #[allow(deprecated)]
        step_layered(&mut peds, &space, social_force::step, &params, DT);
        if tick % REPORT_EVERY == 0 {
            let (g0, g1, r) = histogram(&peds);
            println!("{tick},{:.1},{g0},{g1},{r}", tick as f64 * DT);
        }
    }

    // Final outcome.
    let (g0, g1, r) = histogram(&peds);
    println!("# final: grounded_f0={g0}, grounded_f1={g1}, riding={r}");

    // Sanity assertions — these are what blocker #8 actually fixes.
    assert_eq!(
        r,
        0,
        "all connector transits should complete within {} s",
        NUM_TICKS as f64 * DT
    );
    assert_eq!(
        g1, NUM_COMMUTERS,
        "every commuter (with target_floor = Some(1)) should end up on floor 1"
    );
    assert_eq!(
        g0, NUM_LOITERERS,
        "every loiterer (with target_floor = None) should stay grounded on floor 0 \
         — if this fails, the boarding gate from blocker #8 has regressed and \
         loiterers are re-boarding the stair on spatial overlap alone"
    );
    println!(
        "# OK: {NUM_COMMUTERS}/{NUM_COMMUTERS} commuters reached floor 1, \
         {NUM_LOITERERS}/{NUM_LOITERERS} loiterers stayed on floor 0"
    );
}