Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Traffic Generation

Real simulations need rider arrivals. The traffic module (enabled by default via the traffic feature flag) provides tools for generating realistic passenger traffic — from uniform random spawns to time-varying daily patterns.

Traffic generation is external to the simulation loop. A TrafficSource produces SpawnRequests each tick; your code feeds them into the simulation. This keeps the core loop untouched and gives you full control over when and how riders spawn.

Quick start

use elevator_core::prelude::*;
use elevator_core::config::ElevatorConfig;
use elevator_core::traffic::{PoissonSource, TrafficPattern, TrafficSchedule};

fn main() -> Result<(), SimError> {
let mut sim = SimulationBuilder::new()
    .stop(StopId(0), "Ground", 0.0)
    .stop(StopId(1), "Top", 10.0)
    .elevator(ElevatorConfig::default())
    .build()?;
let stops: Vec<StopId> = sim.stop_lookup_iter().map(|(id, _)| *id).collect();

// Poisson arrivals with an office-day schedule.
let mut source = PoissonSource::new(
    stops,
    TrafficSchedule::office_day(3600), // 3600 ticks per hour
    120,                                // mean inter-arrival: 120 ticks
    (60.0, 90.0),                       // weight range: 60-90kg
);

for _ in 0..10_000 {
    let tick = sim.current_tick();
    for req in source.generate(tick) {
        let _ = sim.spawn_rider_by_stop_id(req.origin, req.destination, req.weight);
    }
    sim.step();
}
Ok(())
}

Patterns

TrafficPattern selects origin/destination distributions. Five presets cover common building scenarios:

PatternDistribution
UniformEqual probability for all origin/destination pairs
UpPeak80% from lobby, 20% inter-floor (morning rush)
DownPeak80% to lobby, 20% inter-floor (evening rush)
Lunchtime40% upper→mid, 40% mid→upper, 20% random
Mixed30% up-peak, 30% down-peak, 40% inter-floor

The first stop in the slice is treated as the “lobby” — make sure stops are sorted by position.

#![allow(unused)]
fn main() {
use elevator_core::traffic::TrafficPattern;

fn run(stops: &[elevator_core::stop::StopId]) {
let mut rng = rand::rng();
if let Some((origin, destination)) = TrafficPattern::UpPeak.sample_stop_ids(stops, &mut rng) {
    println!("{origin} → {destination}");
}
}
}

Schedules

A TrafficSchedule maps tick ranges to patterns, enabling realistic daily cycles:

#![allow(unused)]
fn main() {
use elevator_core::traffic::{TrafficPattern, TrafficSchedule};

let schedule = TrafficSchedule::new(vec![
    (0..3600, TrafficPattern::UpPeak),        // First hour: morning rush
    (3600..7200, TrafficPattern::Uniform),    // Second hour: normal
    (7200..10800, TrafficPattern::Lunchtime), // Third hour: lunch
    (10800..14400, TrafficPattern::DownPeak), // Fourth hour: evening rush
]);
}

When the current tick falls outside all segments, the schedule uses a fallback pattern (default: Uniform):

#![allow(unused)]
fn main() {
use elevator_core::traffic::{TrafficPattern, TrafficSchedule};
let schedule = TrafficSchedule::new(vec![(0..1000, TrafficPattern::UpPeak)])
    .with_fallback(TrafficPattern::Mixed);
}

Built-in schedule presets

  • TrafficSchedule::office_day(ticks_per_hour) — typical 9-hour office day with morning rush, lunch, and evening rush
  • TrafficSchedule::constant(pattern) — a single pattern for all ticks

Poisson arrivals

PoissonSource is the default traffic generator. It uses exponential inter-arrival times — a standard Poisson process — driven by a mean interval parameter:

#![allow(unused)]
fn main() {
use elevator_core::traffic::{PoissonSource, TrafficSchedule, TrafficPattern};
use elevator_core::stop::StopId;

let stops = vec![StopId(0), StopId(1), StopId(2)];
let source = PoissonSource::new(
    stops,
    TrafficSchedule::constant(TrafficPattern::Uniform),
    60,              // mean arrival every 60 ticks
    (50.0, 100.0),   // weight range (min, max) in kg
);
}

Each call to source.generate(tick) returns a Vec<SpawnRequest> — zero, one, or multiple requests depending on how many arrivals are due since the last call.

From config

If your SimConfig already has passenger_spawning populated, use from_config:

#![allow(unused)]
fn main() {
use elevator_core::prelude::*;
use elevator_core::traffic::PoissonSource;
fn run(config: &SimConfig) {
let source = PoissonSource::from_config(config);
}
}

This uses the config’s mean_interval_ticks and weight_range, and defaults to a Uniform schedule. Override with .with_schedule(...) for time-varying traffic.

Fluent configuration

#![allow(unused)]
fn main() {
use elevator_core::traffic::{PoissonSource, TrafficSchedule, TrafficPattern};
use elevator_core::stop::StopId;
fn run(stops: Vec<StopId>) {
let source = PoissonSource::new(stops, TrafficSchedule::constant(TrafficPattern::Uniform), 100, (50.0, 100.0))
    .with_schedule(TrafficSchedule::office_day(3600))
    .with_mean_interval(50)
    .with_weight_range((65.0, 85.0));
}
}

Determinism and seeding

PoissonSource uses a thread-local RNG (rand::rng()) internally, so two runs of the same config will produce different traffic. This is convenient for exploration but unsuitable for replay, regression testing, or research comparisons.

For reproducible traffic, write a custom TrafficSource that owns a seeded RNG:

#![allow(unused)]
fn main() {
use elevator_core::traffic::{TrafficSource, SpawnRequest, TrafficPattern};
use elevator_core::stop::StopId;
use rand::{Rng, SeedableRng, rngs::StdRng};

struct SeededPoisson {
    stops: Vec<StopId>,
    rng: StdRng,
    mean_interval: u32,
    next: u64,
}

impl SeededPoisson {
    fn new(stops: Vec<StopId>, seed: u64, mean_interval: u32) -> Self {
        let mut rng = StdRng::seed_from_u64(seed);
        let next = rng.random_range(1..=(mean_interval * 2) as u64);
        Self { stops, rng, mean_interval, next }
    }
}

impl TrafficSource for SeededPoisson {
    fn generate(&mut self, tick: u64) -> Vec<SpawnRequest> {
        let mut out = Vec::new();
        while tick >= self.next {
            if let Some((origin, destination)) =
                TrafficPattern::Uniform.sample_stop_ids(&self.stops, &mut self.rng)
            {
                out.push(SpawnRequest { origin, destination, weight: 75.0 });
            }
            self.next += self.rng.random_range(1..=(self.mean_interval * 2) as u64);
        }
        out
    }
}
}

With a fixed seed, identical config, and a deterministic dispatch strategy, sim.snapshot() outputs byte-for-byte match across runs.

Custom traffic sources

The TrafficSource trait is trivial to implement for game-specific logic:

#![allow(unused)]
fn main() {
use elevator_core::traffic::{TrafficSource, SpawnRequest};
use elevator_core::stop::StopId;

/// Spawns a single VIP rider at a fixed tick.
struct VipSpawn {
    tick: u64,
    origin: StopId,
    destination: StopId,
    fired: bool,
}

impl TrafficSource for VipSpawn {
    fn generate(&mut self, tick: u64) -> Vec<SpawnRequest> {
        if !self.fired && tick >= self.tick {
            self.fired = true;
            vec![SpawnRequest {
                origin: self.origin,
                destination: self.destination,
                weight: 85.0,
            }]
        } else {
            Vec::new()
        }
    }
}
}

You can layer multiple sources, wrap them in a composite, or mix Poisson arrivals with scripted events. The simulation doesn’t care how requests are generated — only that you feed them in.

SpawnRequest

A SpawnRequest is the minimal description of a rider to spawn:

pub struct SpawnRequest {
    pub origin: StopId,
    pub destination: StopId,
    pub weight: f64,
}

For riders that need patience, preferences, or access control, spawn through the simulation’s build_rider_by_stop_id fluent API instead of using spawn_rider_by_stop_id directly:

#![allow(unused)]
fn main() {
use elevator_core::prelude::*;
use elevator_core::traffic::SpawnRequest;
fn run(sim: &mut Simulation, req: SpawnRequest) -> Result<(), SimError> {
sim.build_rider_by_stop_id(req.origin, req.destination)?
    .weight(req.weight)
    .patience(600)  // abandon after 10 seconds at 60 tps
    .spawn()?;
Ok(())
}
}

RON configuration

TrafficPattern and TrafficSchedule derive Serialize/Deserialize, so you can include them in RON config files:

// traffic_config.ron
TrafficSchedule(
    segments: [
        (0..3600, UpPeak),
        (3600..7200, Uniform),
        (7200..10800, Lunchtime),
    ],
    fallback: Uniform,
)

Next steps

Head to Metrics and Events to see how generated traffic produces events and summary statistics.