Stops, Lines, and Groups
This chapter covers the spatial and organizational building blocks of an elevator-core simulation: where elevators travel, how shafts are modeled, and how cars are grouped for dispatch.
Stops
A stop is a named position along a 1D shaft axis. Unlike traditional elevator simulators that assume uniform floor spacing, elevator-core places stops at arbitrary distances. A 5-story office might have stops at 0.0, 3.5, 7.0, 10.5, and 14.0. A space elevator might have stops at 0.0 and 35,786,000.0. The engine does not care – the physics just scale.
use elevator_core::prelude::*;
use elevator_core::__doctest_prelude::*;
fn main() -> Result<(), SimError> {
let sim = SimulationBuilder::new()
.stop(StopId(0), "Basement", -3.0)
.stop(StopId(1), "Lobby", 0.0)
.stop(StopId(2), "Mezzanine", 2.5)
.stop(StopId(3), "Floor 2", 6.0)
.elevator(ElevatorConfig::default())
.build()?;
Ok(())
}
Positions are plain f64 values – the library does not enforce meters, feet, or any other unit. Negative positions are fine (basements below a lobby at 0.0, for instance). There is no privileged zero or required origin.
Lines
A line represents a physical path: a shaft, a tether, a track. Every elevator belongs to exactly one line, and a line defines which stops its elevators can reach.
For simple single-shaft buildings, you never need to think about lines. The builder auto-creates a default line that includes all stops and all elevators:
use elevator_core::prelude::*;
use elevator_core::__doctest_prelude::*;
fn main() -> Result<(), SimError> {
// This implicitly creates one line containing both elevators and all stops.
SimulationBuilder::new()
.stop(StopId(0), "Ground", 0.0)
.stop(StopId(1), "Top", 10.0)
.elevator(ElevatorConfig { id: elevator_core::config::ElevatorConfigId(0), ..Default::default() })
.elevator(ElevatorConfig { id: elevator_core::config::ElevatorConfigId(1), ..Default::default() })
.build()?;
Ok(())
}
Lines matter when you have separate shafts serving different sets of stops – a low-rise bank that only reaches floors 1-20 and a high-rise bank that serves 20-40, for example.
Groups
A group is a dispatch unit: one or more lines that share a single dispatch strategy. The dispatch system evaluates demand and assigns elevators within each group independently.
The simplest simulations have one group (auto-created by the builder). Multi-group configurations are common in tall buildings where low-rise and high-rise banks operate independently, each with their own dispatch algorithm.
A stop can appear in multiple groups. A sky lobby served by both the low-rise and high-rise banks is a single stop entity referenced by lines in both groups.
Entity relationships
The full hierarchy looks like this:
graph TD
G["Group
*GroupId*"] -->|owns| L1["Line A
*EntityId*"]
G -->|owns| L2["Line B
*EntityId*"]
L1 -->|contains| E1["Elevator 1
*EntityId*"]
L1 -->|contains| E2["Elevator 2
*EntityId*"]
L2 -->|contains| E3["Elevator 3
*EntityId*"]
L1 -->|serves| S0["Stop: Lobby
*EntityId*"]
L1 -->|serves| S1["Stop: Floor 10
*EntityId*"]
L2 -->|serves| S0
L2 -->|serves| S2["Stop: Floor 20
*EntityId*"]
Key invariants:
- An elevator always belongs to exactly one line (
elevator.linepoints to aLineentity) - A line always belongs to exactly one group (
line.grouppoints to aGroupId) - A stop may be shared across lines and groups
- Riders aboard an elevator appear in both
elevator.ridersand carryRiderPhase::Riding(elevator_id)
Identity types
The library uses five identity types. Knowing which to reach for saves confusion:
| Type | Identifies | When you use it |
|---|---|---|
EntityId | Any entity at runtime (stop, elevator, rider) | Event payloads, world lookups, dispatch decisions |
StopId | A stop in the config (e.g., StopId(0)) | Builder API, config files, spawn_rider |
GroupId | An elevator group (e.g., GroupId(0)) | Multi-group dispatch, group-specific hooks |
ElevatorId | A specific elevator entity | Public API surfaces that act on a car (set_service_mode, push_destination, manual control) |
RiderId | A specific rider entity | Public API surfaces that act on a rider (despawn_rider, settle_rider, set_rider_tag) |
ElevatorId and RiderId are phantom-typed wrappers over EntityId. They exist to give the public API surface type safety – spawn_rider returns a RiderId, not a bare EntityId, so you can’t accidentally pass a stop id where a rider id is required. Internally, World and event payloads use the untyped EntityId.
StopId is a config-level concept. When the simulation boots, each StopId is mapped to an EntityId. At runtime you work with EntityId everywhere. Convert when needed:
#![allow(unused)]
fn main() {
use elevator_core::prelude::*;
use elevator_core::__doctest_prelude::*;
fn run(sim: &Simulation) {
let lobby_entity: Option<EntityId> = sim.stop_entity(StopId(0));
let _ = lobby_entity;
}
}
Coordinate system
- Axis. All positions are scalars along a single 1D axis. Higher values mean higher up (or further along for horizontal configurations). There is no 2D/3D geometry in the core.
- Units. Unspecified – positions, velocities, accelerations, and weights are
f64values. Keep them internally consistent. The suggested convention is meters, kilograms, and ticks, but the library does not enforce this. - Origin. No privileged zero. Stop 0 does not have to be at position 0.0. Negative positions are allowed.
Time
The fundamental unit of time is the tick. Each call to sim.step() advances the simulation by one tick.
Convert between ticks and seconds using the time API:
#![allow(unused)]
fn main() {
use elevator_core::prelude::*;
use elevator_core::__doctest_prelude::*;
fn run(sim: &Simulation) {
let seconds = sim.time().ticks_to_seconds(120); // 120 ticks -> seconds
let _ = seconds;
}
}
The conversion uses ticks_per_second from your config (default: 60). At 60 ticks/second, 120 ticks = 2.0 seconds.
Next steps
- Elevators – how elevator cars move through their phase lifecycle
- Configuration – defining stops, lines, and groups in code or RON files
- Dispatch Strategies – how groups assign elevators to calls