Inspiration
I work as a field technician on the railway. When a fault happens, the dispatcher has to figure out what is going on and notify several parties at the same time. Field staff like me get the same picture, estimate the cause, fix it within a tight time window, and then come back to write a pile of paperwork in the required report format.
So I kept thinking: what if an AI analyzed the likely causes the moment a fault occurs, and drafted the report after recovery? Even a plain list of candidate causes is a big hint for someone standing next to the equipment. The judgment still belongs to the human. That became the starting point of this project: people handle the assessment and the decisions, AI drafts the documents.
I also wanted to test one boundary that I believe must hold when you put an LLM into a safety-critical domain: the AI must not be able to operate the equipment. Not "should not". Must not, by design.
What it does
An educational railway control simulator for a virtual single station (up/down main lines, side platforms, two crossovers).
- Control simulation: a track diagram with 12 track circuits, 4 point machines, 6 signals, and 8 routes. It supports route setting, occupancy and fault indications, and fail-safe interlocking, so routes are rejected on conflict, occupancy, or fault, and signals drop to stop. Trains run automatically on a timetable, and stops, departures, and delays are recorded.
- AI Fault Advisor: when a fault occurs on a signal, point machine, or track circuit, it reads an immutable snapshot of the current station state and generates a notification for the dispatcher, a caution advisory for train crews, and a list of fail-safe recommended actions. After the fault is acknowledged and cleared, it produces an incident report grounded in the event and command journals: timeline, affected trains and delays, estimated cause, and suggestions to prevent recurrence. The report is ready to paste into the control log.
- Safety boundary by design: the AI only advises. It cannot execute any command. AI messages are kept fully separate from the domain state, the command journal, and session data, so deterministic replay (verified by state hash) is never broken. If the LLM server is unreachable, the error shows up isolated in the message list while control and train operations continue. The simulation never pauses during generation.
How we built it
- A pure-Python domain model (signalling, interlocking, and train operations) separated from a PySide6 control screen. The UI displays state and never mutates it directly.
- The advisor is an OpenAI-compatible client with the OpenAI API as the default (verified with
gpt-5.6-terra), so it also works against local LLM endpoints. - Development used a two-agent workflow. Claude Code led the design and review, and Codex CLI, powered by GPT-5.6, implemented the features and wrote the tests. For each unit of work I created an instruction file with the goal, target files, forbidden changes, and completion criteria, delegated it, reviewed the diff, and ran the full test suite before moving on. This let me spend my time on design decisions like the fail-safe interlocking rules and the "AI advises only" principle instead of typing code.
- GPT-5.6 ended up in the project twice. At build time it was the model behind Codex CLI writing the implementation and tests. At runtime it is the advisor model (
gpt-5.6-terra) that drafts the notifications and incident reports you see in the demo.
Challenges we ran into
- Preserving determinism. Session replay must match down to the state hash, but LLM responses are non-deterministic. I solved this by keeping AI messages in a UI annotation layer outside the domain.
- Non-blocking generation. The simulation must not stop during LLM calls, which take seconds to tens of seconds. The app builds an immutable snapshot, hands it to a worker thread, and discards stale responses with a generation counter when the model is replaced by a session replay.
- A design gap found during demo rehearsal. Injecting a fault into a departure (starter) signal did not stop a dwelling train from departing anyway. To keep the change low-risk this close to the deadline, I scoped it down to a minimal fix: hold departure while the starter signal has an active fault, instead of reworking the whole route model.
Accomplishments that we're proud of
- Error isolation that holds up even when the AI side fails completely, network outage included. The core simulation does not flinch.
- Keeping deterministic replay fully intact by separating AI messages from domain state.
- Actually running the two-agent workflow end to end, which roughly halved the development time per feature.
- 85 regression tests passing at submission. Verification stayed in place even at hackathon speed.
What we learned
In a safety-first domain, the important design question is not what the AI can do. It is what the AI must be made unable to do. I also learned first-hand that in AI-collaborative development, the clearer you write the completion criteria in the instructions, the better the output quality gets.
And as a field technician, this project convinced me that AI can grow in the direction of reducing human error rather than replacing human judgment.
What's next for Railway Control Simulator with AI Fault Advisor
- Extend to multiple stations and inter-station sections (M5)
- Rework departures into a proper two-route model
- Add real-world fault case studies and response strategies
- Add voice notifications (TTS) and multilingual notifications to the advisor
Built with
Python 3.12 · PySide6 · OpenAI API (gpt-5.6-terra) · Codex CLI(gpt 5.6 sol) · Claude Code · pytest
Log in or sign up for Devpost to join the conversation.