Skip to content

Engine Core Developer Guide

Priyesh Patel edited this page Oct 17, 2026 · 8 revisions

Engine Core Developer Guide

The engine core for US-1.1 Deterministic event engine is on branch feature/engine-core. This page covers what exists, how to run it, and where each remaining task plugs in.

What exists

Task Issue State
1.1.1 Event types and record schema #96 Done: simulator/engine/events.py
1.1.2 Event queue and simulation clock #8 Done: simulator/engine/event_queue.py
1.1.6 Simulation runner #99 Done: simulator/engine/runner.py, event_log.py
1.1.3 Seed management #9 Open
1.1.4 Network model #97 Open
1.1.5 Scenario configuration #98 Open
1.1.7 Run manifest #10 Open
1.1.8 Determinism test in CI #11 Open

Setup

Full steps are in the README under "Getting started". In short:

py -3.12 -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements-dev.txt
.\.venv\Scripts\python.exe -m pytest -v

Adding --basetemp=run-output to a run of tests/simulator/engine/test_runner.py keeps the output of a small McGill run (events.jsonl and summary.json).

How a run works

The engine is a discrete-event simulation: the clock jumps from one event to the next instead of moving in fixed steps. run_scenario(scenario, build_streams, build_components, output_dir):

  1. Builds the random streams from scenario.master_seed, then the components (trains, passengers) from the scenario and streams
  2. Calls start(scheduler) on each component so it schedules its first events
  3. Processes events in order until scenario.duration_s. Each event is written to the log, then passed to every component's handle(event, scheduler), which may schedule more events
  4. Writes events.jsonl (one line per event) and summary.json (counts) to output_dir

The same scenario and seed always give byte-identical files.

Events

Eight event types: TrainArrival, DepartureDue, TrainDeparture, Hold, PassengerArrival, Boarding, Alighting, MissedConnection. All times are in seconds. Fields and units are documented in the docstrings of events.py.

  • DepartureDue is the hold decision point: train logic schedules it at arrival + dwell, and handling it records a Hold and schedules the TrainDeparture (at the same time if there is no hold). TimetableComponent in tests/conftest.py shows the no-hold case

  • Transferring passengers get one event per passenger (passenger_id, plus group_id for the transfer burst on PassengerArrival)

  • Everyone else only appears as counts on TrainDeparture: boarded_count (B), alighted_count (A), onboard_load (L)

  • Hold delay (hold_s, Hold) is kept separate from disruption delay (disruption_delay_s)

Tie order. Events at the same time are processed as: arrival, alighting, passenger arrival, boarding, departure due, hold, departure, missed connection. The scheduler rejects an event in the past, or one at the current time that comes earlier in this order than the event just processed. In practice, a component reacting to an event can only schedule things at the same time if they come later in the order, or at a later time.

Where the remaining tasks plug in

The runner depends only on small interfaces in runner.py. Each task replaces a stand-in in tests/conftest.py once merged.

Task Provides Interface Stand-in to remove
#98 Scenario The scenario object Has duration_s (seconds) and master_seed (non-negative int) MinimalScenario
#9 Seeds build_streams(master_seed) Returns the substreams (one SeedSequence substream per source, Mathematical Foundations section 9) SeedOnlyStreams
#97 Network build_components(scenario, streams) Returns a list of components, each with start(scheduler) and handle(event, scheduler) TimetableComponent, TINY_TIMETABLE
#10 Manifest manifest.json Written next to the run output; no wall clock time or absolute paths, or determinism breaks none
#11 CI GitHub Actions workflow Runs ruff check ., ruff format --check . and pytest none

TimetableComponent in tests/conftest.py is a 40-line example of a working component.

Rules

  • Schema changes bump SCHEMA_VERSION in events.py and update test_events.py
  • Tests go in tests/, mirroring the source tree. Expected values are worked out by hand, never computed with the code under test. Every task adds a main test and an edge-case test
  • AI declaration: every source and config file starts with # AI contribution: ... (No substantial / Below 50% / 50% or more AI-generated)
  • Limits: functions at most 50 lines, files at most 500. Ruff checks complexity but not function length
  • No randomness outside the seeded streams, and no Supabase or network access in tests

Design decisions behind the engine (heapq instead of SimPy, per-passenger events, tie order) are recorded in the Architecture changes table on the Architecture page.

AI acknowledgement: drafted with Claude, reviewed and edited by Priyesh Patel.

Clone this wiki locally