Repository navigation
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.
| 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 |
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 -vAdding --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).
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):
- Builds the random streams from
scenario.master_seed, then the components (trains, passengers) from the scenario and streams - Calls
start(scheduler)on each component so it schedules its first events - Processes events in order until
scenario.duration_s. Each event is written to the log, then passed to every component'shandle(event, scheduler), which may schedule more events - Writes
events.jsonl(one line per event) andsummary.json(counts) tooutput_dir
The same scenario and seed always give byte-identical files.
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.
-
DepartureDueis the hold decision point: train logic schedules it at arrival + dwell, and handling it records aHoldand schedules theTrainDeparture(at the same time if there is no hold).TimetableComponentintests/conftest.pyshows the no-hold case -
Transferring passengers get one event per passenger (
passenger_id, plusgroup_idfor the transfer burst onPassengerArrival) -
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.
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.
-
Schema changes bump
SCHEMA_VERSIONinevents.pyand updatetest_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.