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

Getting Started

In this chapter we will build a minimal elevator simulation from scratch: a 3-stop building with one elevator, a single rider, and a loop that runs until the rider arrives at their destination.

Add the dependency

cargo add elevator-core

Import the prelude

The prelude re-exports everything you need for typical usage:

#![allow(unused)]
fn main() {
use elevator_core::prelude::*;
}

This brings in, at a glance:

GroupItems
Builder & simSimulationBuilder, Simulation, RiderBuilder
ComponentsRider, RiderPhase, Elevator, ElevatorPhase, Stop, Line, Position, Velocity, FloorPosition, Route, Patience, Preferences, AccessControl, Orientation, ServiceMode
ConfigSimConfig, GroupConfig, LineConfig
Dispatch traitsDispatchStrategy, RepositionStrategy
Reposition strategiesNearestIdle, ReturnToLobby, SpreadEvenly, DemandWeighted
IdentityEntityId, StopId, GroupId
Errors & eventsSimError, RejectionReason, RejectionContext, Event, EventBus
MiscMetrics, TimeAdapter

Not in the prelude (import explicitly): the concrete built-in dispatch types (ScanDispatch, LookDispatch, NearestCarDispatch, EtdDispatch — see Dispatch Strategies), ElevatorConfig and StopConfig from elevator_core::config, the traffic module (feature-gated), the snapshot module, and the World type (needed as a parameter when implementing custom dispatch).

Feature flags

FlagDefault?Enables
trafficyestraffic module: PoissonSource, TrafficPattern, TrafficSchedule. Pulls in rand.
energynoPer-elevator EnergyProfile/EnergyMetrics components and snapshot fields.

Turn off defaults with default-features = false if you want a leaner build and intend to write your own rider spawning.

Build a simulation

We will use SimulationBuilder to set up a 3-stop building with one elevator, SCAN dispatch (the builder’s default), and 60 ticks per second. ElevatorConfig implements Default with sensible physics (max speed 2.0, acceleration 1.5, deceleration 2.0, 800 kg capacity) so the elevator is one line; stops we spell out because positions are the whole point.

use elevator_core::prelude::*;
use elevator_core::config::ElevatorConfig;
use elevator_core::stop::StopId;

fn main() -> Result<(), SimError> {
    let mut sim = SimulationBuilder::new()
        .stop(StopId(0), "Lobby", 0.0)
        .stop(StopId(1), "Floor 2", 4.0)
        .stop(StopId(2), "Floor 3", 8.0)
        .elevator(ElevatorConfig { starting_stop: StopId(0), ..Default::default() })
        .building_name("Tutorial Tower")
        .build()?;

    Ok(())
}

Override any ElevatorConfig field with struct-update syntax (ElevatorConfig { max_speed: 4.0, ..Default::default() }) — the Configuration chapter covers every field.

Spawn a rider

A rider is anything that rides an elevator. To spawn one, you provide an origin stop, a destination stop, and a weight:

use elevator_core::prelude::*;
use elevator_core::config::ElevatorConfig;
use elevator_core::stop::StopId;
fn main() -> Result<(), SimError> {
let mut sim = SimulationBuilder::new()
    .stop(StopId(0), "Lobby", 0.0)
    .stop(StopId(1), "Floor 2", 4.0)
    .stop(StopId(2), "Floor 3", 8.0)
    .elevator(ElevatorConfig { starting_stop: StopId(0), ..Default::default() })
    .build()?;
let rider_id = sim.spawn_rider_by_stop_id(
    StopId(0),  // origin: Lobby
    StopId(2),  // destination: Floor 3
    75.0,       // weight in kg
)?;
println!("Spawned rider: {:?}", rider_id);
Ok(())
}

spawn_rider_by_stop_id maps config-level StopId values to runtime EntityId values internally. It returns Result<EntityId, SimError> – it will fail if you pass a StopId that does not exist in your building.

Run the simulation loop

Each call to sim.step() advances the simulation by one tick, running all eight phases of the tick loop (advance transient, dispatch, reposition, advance queue, movement, doors, loading, metrics). After stepping, you can drain events to see what happened:

use elevator_core::prelude::*;
use elevator_core::config::ElevatorConfig;
use elevator_core::stop::StopId;
fn main() -> Result<(), SimError> {
let mut sim = SimulationBuilder::new()
    .stop(StopId(0), "Lobby", 0.0)
    .stop(StopId(1), "Floor 2", 4.0)
    .stop(StopId(2), "Floor 3", 8.0)
    .elevator(ElevatorConfig { starting_stop: StopId(0), ..Default::default() })
    .build()?;
let rider_id = sim.spawn_rider_by_stop_id(StopId(0), StopId(2), 75.0)?;
let mut arrived = false;

while !arrived {
    sim.step();

    for event in sim.drain_events() {
        match event {
            Event::RiderBoarded { rider, elevator, tick } => {
                println!("Tick {}: rider {:?} boarded elevator {:?}", tick, rider, elevator);
            }
            Event::ElevatorArrived { elevator, at_stop, tick } => {
                println!("Tick {}: elevator {:?} arrived at stop {:?}", tick, elevator, at_stop);
            }
            Event::RiderExited { rider, stop, tick, .. } => {
                println!("Tick {}: rider {:?} arrived at stop {:?}", tick, rider, stop);
                if rider == rider_id {
                    arrived = true;
                }
            }
            _ => {}
        }
    }
}

println!("Rider delivered!");
println!("Total ticks: {}", sim.current_tick());
println!("Avg wait time: {:.1} ticks", sim.metrics().avg_wait_time());
Ok(())
}

The complete program

Here is everything together as a single runnable file:

use elevator_core::prelude::*;
use elevator_core::config::ElevatorConfig;
use elevator_core::stop::StopId;

fn main() -> Result<(), SimError> {
    // 1. Build a 3-stop building.
    let mut sim = SimulationBuilder::new()
        .stop(StopId(0), "Lobby", 0.0)
        .stop(StopId(1), "Floor 2", 4.0)
        .stop(StopId(2), "Floor 3", 8.0)
        .elevator(ElevatorConfig { starting_stop: StopId(0), ..Default::default() })
        .building_name("Tutorial Tower")
        .build()?;

    // 2. Spawn a rider going from the Lobby to Floor 3.
    let rider_id = sim.spawn_rider_by_stop_id(StopId(0), StopId(2), 75.0)?;

    // 3. Run until the rider arrives.
    let mut arrived = false;
    while !arrived {
        sim.step();

        for event in sim.drain_events() {
            match event {
                Event::RiderBoarded { rider, elevator, tick } => {
                    println!("Tick {tick}: rider {rider:?} boarded elevator {elevator:?}");
                }
                Event::ElevatorArrived { elevator, at_stop, tick } => {
                    println!("Tick {tick}: elevator {elevator:?} arrived at {at_stop:?}");
                }
                Event::RiderExited { rider, stop, tick, .. } => {
                    println!("Tick {tick}: rider {rider:?} exited at {stop:?}");
                    if rider == rider_id {
                        arrived = true;
                    }
                }
                _ => {}
            }
        }
    }

    // 4. Print summary metrics.
    println!("\n--- Summary ---");
    println!("Total ticks: {}", sim.current_tick());
    // Metrics implements Display for a compact one-liner.
    println!("{}", sim.metrics());

    Ok(())
}

Run it with cargo run and you should see the rider move from the Lobby to Floor 3, with events printed along the way. The summary output looks like:

--- Summary ---
Total ticks: 482
1 delivered, avg wait 87.3t, 0% util

What just happened?

  1. The builder created a Simulation containing a World with three stop entities and one elevator entity, plus a SCAN dispatch strategy.
  2. spawn_rider_by_stop_id created a rider entity at the Lobby with a route to Floor 3.
  3. Each step() ran the seven-phase tick loop. The dispatch phase noticed a waiting rider and sent the elevator to the Lobby. The reposition phase was a no-op (no reposition strategy configured). The movement phase moved the elevator using a trapezoidal velocity profile. The doors phase opened and closed doors. The loading phase boarded and exited the rider. The metrics phase updated aggregate stats.
  4. Events fired at each significant moment, and we pattern-matched on them to detect arrival.

Next up: Core Concepts dives deeper into the entity model, the tick loop phases, and the lifecycle of riders and elevators.