Skip to content

Instantly share code, notes, and snippets.

@ciscoheat
Created March 21, 2026 18:30
Show Gist options
  • Select an option

  • Save ciscoheat/4e07e19cec4f454bd4116c815aea29f7 to your computer and use it in GitHub Desktop.

Select an option

Save ciscoheat/4e07e19cec4f454bd4116c815aea29f7 to your computer and use it in GitHub Desktop.
Railway operating system - DCI implementation

Mental Model: The Railway "Operating System" (Signals & Switches)

To understand how trains move fluently without colliding, stop thinking about tracks and start thinking about exclusive logical permissions.

1. The Core Concept: The "Virtual Bubble"

Trains cannot steer or stop quickly. Therefore, the system creates a "moving bubble" of protected space around every train.

  • The Block: The track is divided into segments (Blocks).
  • The Rule: Only one "ID" (train) can occupy a Block's "Memory Slot" at a time.
  • The Feedback: Track circuits pass a low-voltage current through the rails. The train’s metal wheels short the circuit, telling the system: "Occupied."

2. The Interlocking (The Logic Gate)

The Interlocking is the "Software" that sits between the Dispatcher and the physical track. It operates on Dependency Logic.

The Handshake:

  1. Dispatcher: "I want Train A to go to Platform 4."
  2. Interlocking: Checks if Platform 4 is empty AND if any other trains are crossing that path.
  3. The Move: If clear, it sends power to the Switch Motors.
  4. The Lock: Once the switch is flush, a metal "lock bar" physically slides into place. The switch is now "Mechanically Locked."
  5. The Release: Only after the lock is confirmed does the Signal turn Green.

3. The Protocol: "Signal-Follows-Switch"

In this model, the Signal is just a visual status report of the track's mechanical state.

  • If a switch is even 1/4 inch out of place, the circuit is broken.
  • The Signal defaults to Red (Fail-safe).
  • Fluency Tip: Modern systems use "Approach Lighting" or "Pre-set Routes" so that as one train clears a block, the next signal "drops" to green just in time for the following train to maintain speed.

4. The Safety Layer: ATP & Braking Curves

If the "OS" (Signaling) fails to influence the "User" (The Driver), the Kernel (ATP) takes over.

Component Function Analogy
Transponder (Balise) Sends speed limits to the train wirelessly. A digital speed limit sign.
Onboard Computer Calculates the "Braking Curve." An invisible tether.
Emergency Brake Tripped if the train exceeds the curve. The "Blue Screen of Death" for unsafe movement.

5. Summary: "The Locked Corridor"

  1. Request: "I need a path."
  2. Alignment: Switches move and lock.
  3. Verification: Logic gate confirms "Path is safe and exclusive."
  4. Authority: Signal changes from Red to Green.
  5. Execution: Train enters; its presence "shorts" the circuit, locking all switches behind it and ahead of it until it leaves.
// =============================================================================
// Railway Operating System — DCI Implementation
// Mental model: Signals & Switches ("The Locked Corridor")
//
// Contexts:
// 1. RouteRequest — Dispatcher asks for a path; interlocking grants or
// denies it (sections 2 & 5 of the mental model).
// 2. BlockClearance — A train vacates a block; signals and approach lighting
// are updated so the following train can maintain speed
// (section 3).
// 3. ATPSupervision — Balise transmits a speed authority to a train's
// on-board computer; ATP applies emergency brake if the
// braking curve is exceeded (section 4).
// =============================================================================
// -----------------------------------------------------------------------------
// DATA — "What the system is"
// Pure domain objects; no context-specific logic lives here.
// -----------------------------------------------------------------------------
export type SignalAspect = "RED" | "GREEN" | "APPROACH";
export class Block {
readonly id: string;
occupantId: string | null = null; // trains "short" this slot
constructor(id: string) {
this.id = id;
}
get isOccupied(): boolean {
return this.occupantId !== null;
}
occupy(trainId: string) {
this.occupantId = trainId;
}
clear() {
this.occupantId = null;
}
}
export class Switch {
readonly id: string;
private _isAligned = false;
private _isLocked = false;
constructor(id: string) {
this.id = id;
}
get isAligned(): boolean {
return this._isAligned;
}
get isLocked(): boolean {
return this._isLocked;
}
/** Physically moves the switch motor to the requested position. */
align(toRoute: string): boolean {
// Simulate motor movement; returns true when flush (no 1/4-inch gap).
this._isAligned = true; // production: check encoder feedback
this._isLocked = false;
console.log(`Switch ${this.id}: aligned for route "${toRoute}".`);
return this._isAligned;
}
/** Slides the lock bar into place once the switch is flush. */
lock(): boolean {
if (!this._isAligned) return false;
this._isLocked = true;
console.log(`Switch ${this.id}: mechanically locked.`);
return true;
}
/** Releases the lock when the path is no longer needed. */
unlock() {
this._isLocked = false;
this._isAligned = false;
console.log(`Switch ${this.id}: unlocked and reset.`);
}
}
export class Signal {
readonly id: string;
aspect: SignalAspect = "RED"; // fail-safe default
constructor(id: string) {
this.id = id;
}
set(aspect: SignalAspect) {
this.aspect = aspect;
console.log(`Signal ${this.id}: → ${aspect}`);
}
}
export class Train {
readonly id: string;
currentBlockId: string | null = null;
speedKmh: number = 0;
constructor(id: string) {
this.id = id;
}
enterBlock(block: Block) {
this.currentBlockId = block.id;
block.occupy(this.id);
console.log(`Train ${this.id}: entered block ${block.id}.`);
}
leaveBlock(block: { id: string; clear: () => void }) {
block.clear();
this.currentBlockId = null;
console.log(`Train ${this.id}: cleared block ${block.id}.`);
}
}
export class Balise {
readonly location: string;
speedLimitKmh: number;
gradientPermille: number;
constructor(location: string, speedLimit: number, gradient = 0) {
this.location = location;
this.speedLimitKmh = speedLimit;
this.gradientPermille = gradient;
}
}
export class OnboardComputer {
/** Latest ATP-sanctioned speed authority (km/h). */
speedAuthorityKmh: number = 999;
/**
* Calculates the minimum distance (m) required to stop from currentSpeed,
* applying the given deceleration and gradient correction.
*/
brakingCurveDistance(
currentSpeedKmh: number,
decelerationMs2: number,
gradientPermille: number,
): number {
const v = currentSpeedKmh / 3.6;
const g = 9.81 * (gradientPermille / 1000); // downhill makes it harder
const effectiveDecel = Math.max(0.1, decelerationMs2 - g);
return (v * v) / (2 * effectiveDecel);
}
updateSpeedAuthority(limitKmh: number) {
this.speedAuthorityKmh = limitKmh;
}
applyEmergencyBrake(trainId: string) {
console.error(
`Train ${trainId}: *** EMERGENCY BRAKE APPLIED *** (ATP intervention — speed exceeded braking curve)`,
);
}
}
// -----------------------------------------------------------------------------
// CONTEXT 1: RouteRequest
//
// Use case: A Dispatcher requests a path for a train to a platform.
// The Interlocking checks that all Blocks on the route are clear, aligns and
// locks every Switch, then releases the governing Signal to GREEN.
// ("Signal-Follows-Switch" — the signal is just a status report.)
// -----------------------------------------------------------------------------
/**
* A Dispatcher requests an exclusive path for a train.
* Implements the "Handshake" and "Locked Corridor" mental model:
* Request → Alignment → Verification → Authority → Execution.
* @DCI-context
*/
export function RouteRequest(
Dispatcher: { trainId: string; destination: string },
routeBlocks: { id: string; isOccupied: boolean }[],
routeSwitches: { align: (route: string) => boolean; lock: () => boolean }[],
GoverningSignal: { set: (a: SignalAspect) => void },
) {
//#region Dispatcher Role ///////////////////////////////////////////////
function Dispatcher_requestPath() {
console.log(
`Dispatcher: requesting path for Train "${Dispatcher.trainId}" → "${Dispatcher.destination}".`,
);
Interlocking_checkBlocks();
}
//#endregion
//#region Interlocking Role ///////////////////////////////////////////////
// The "Software" — dependency logic between physical elements.
const Interlocking = {
blocks: routeBlocks,
switches: routeSwitches,
};
function Interlocking_checkBlocks() {
const conflict = Interlocking.blocks.find((b) => b.isOccupied);
if (conflict) {
console.warn(
`Interlocking: DENIED — block "${conflict.id}" is occupied.`,
);
GoverningSignal_hold(); // keep RED
return;
}
Interlocking_alignSwitches();
}
function Interlocking_alignSwitches() {
const allAligned = Interlocking.switches.every((sw) =>
sw.align(Dispatcher.destination),
);
if (!allAligned) {
console.warn(`Interlocking: DENIED — a switch failed to align.`);
GoverningSignal_hold();
return;
}
Interlocking_lockSwitches();
}
function Interlocking_lockSwitches() {
const allLocked = Interlocking.switches.every((sw) => sw.lock());
if (!allLocked) {
console.warn(
`Interlocking: DENIED — a switch failed to lock (possible 1/4-inch gap).`,
);
GoverningSignal_hold();
return;
}
// All switches mechanically confirmed — release the signal.
GoverningSignal_authorize();
}
//#endregion
//#region GoverningSignal Role ///////////////////////////////////////////////
// The signal is a *visual status report* of the mechanical state; it never
// goes green on its own — only the Interlocking can release it.
function GoverningSignal_authorize() {
GoverningSignal.set("GREEN");
console.log(
`RouteRequest: path granted — Train "${Dispatcher.trainId}" may proceed to "${Dispatcher.destination}".`,
);
}
function GoverningSignal_hold() {
GoverningSignal.set("RED");
}
//#endregion
// System Operation
try {
Dispatcher_requestPath();
} catch (e) {
GoverningSignal_hold();
console.error("RouteRequest: unexpected fault —", e);
}
}
// -----------------------------------------------------------------------------
// CONTEXT 2: BlockClearance
//
// Use case: A train vacates a block. The Interlocking releases the locks,
// resets the signal protecting the vacated block, and — if a following train
// is approaching — drops the next signal to APPROACH ("approach lighting"),
// letting it maintain speed rather than stop at RED.
// -----------------------------------------------------------------------------
/**
* A train clears a block; switches are unlocked and approach lighting is set
* for the following train so it can maintain speed into the freed corridor.
* @DCI-context
*/
export function BlockClearance(
LeavingTrain: {
id: string;
leaveBlock: (b: { id: string; clear: () => void }) => void;
},
VacatedBlock: { id: string; clear: () => void },
vacatedSwitches: { unlock: () => void }[],
VacatedSignal: { set: (a: SignalAspect) => void },
ApproachSignal: { set: (a: SignalAspect) => void } | null, // signal seen by the *following* train
FollowingTrain: { id: string } | null, // may not exist
) {
//#region LeavingTrain Role ///////////////////////////////////////////////
function LeavingTrain_vacate() {
LeavingTrain.leaveBlock(VacatedBlock);
Interlocking_onBlockCleared();
}
//#endregion
//#region Interlocking Role ///////////////////////////////////////////////
const Interlocking = {
switches: vacatedSwitches,
};
function Interlocking_onBlockCleared() {
Interlocking.switches.forEach((sw) => sw.unlock());
VacatedSignal_reset();
}
//#endregion
//#region VacatedSignal Role ///////////////////////////////////////////////
function VacatedSignal_reset() {
// Signal defaults to RED until a new RouteRequest re-authorises it.
VacatedSignal.set("RED");
ApproachSignal_dropForFollowingTrain();
}
//#endregion
//#region ApproachSignal Role ///////////////////////////////////////////////
// Implements "Approach Lighting / Pre-set Routes": the signal visible to the
// *following* train is dropped to APPROACH just in time so that train need
// not stop, maintaining overall throughput.
function ApproachSignal_dropForFollowingTrain() {
if (!ApproachSignal || !FollowingTrain) return;
// Only drop to APPROACH if the following train is actually en route.
console.log(
`BlockClearance: approach lighting activated for Train "${FollowingTrain.id}" — signal ahead changing to APPROACH.`,
);
ApproachSignal.set("APPROACH");
}
//#endregion
// System Operation
try {
LeavingTrain_vacate();
} catch (e) {
console.error("BlockClearance: unexpected fault —", e);
}
}
// -----------------------------------------------------------------------------
// CONTEXT 3: ATPSupervision
//
// Use case: A train's wheels pass over a Balise (transponder). The Balise
// sends the applicable speed limit wirelessly to the OnboardComputer, which
// recalculates the braking curve. If the train is already over-speed, the
// Kernel (ATP) applies the emergency brake — the "Blue Screen of Death".
// -----------------------------------------------------------------------------
/**
* A train passes over a balise; the on-board computer updates the speed
* authority and triggers emergency braking if the braking curve is violated.
* @DCI-context
*/
export function ATPSupervision(
SupervisedTrain: { id: string; speedKmh: number },
Balise: { location: string; speedLimitKmh: number; gradientPermille: number },
OnboardComputer: {
speedAuthorityKmh: number;
updateSpeedAuthority: (l: number) => void;
brakingCurveDistance: (v: number, d: number, g: number) => number;
applyEmergencyBrake: (id: string) => void;
},
decelerationMs2 = 1.1, // typical service-brake deceleration (m/s²)
) {
//#region Balise Role ///////////////////////////////////////////////
// Acts like a "digital speed limit sign" passed wirelessly to the train.
function Balise_transmitAuthority() {
console.log(
`Balise @${Balise.location}: transmitting speed authority ${Balise.speedLimitKmh} km/h (gradient ${Balise.gradientPermille}‰).`,
);
OnboardComputer_receiveAuthority(
Balise.speedLimitKmh,
Balise.gradientPermille,
);
}
//#endregion
//#region OnboardComputer Role ///////////////////////////////////////////////
// The "invisible tether" — calculates whether the train can still stop
// within the authorised limit from its current speed.
function OnboardComputer_receiveAuthority(
limitKmh: number,
gradientPermille: number,
) {
OnboardComputer.updateSpeedAuthority(limitKmh);
OnboardComputer__checkBrakingCurve(gradientPermille);
}
function OnboardComputer__checkBrakingCurve(gradientPermille: number) {
// Private — only called by OnboardComputer RoleMethods.
const stoppingDistance = OnboardComputer.brakingCurveDistance(
SupervisedTrain.speedKmh,
decelerationMs2,
gradientPermille,
);
const isOverSpeed =
SupervisedTrain.speedKmh > OnboardComputer.speedAuthorityKmh;
console.log(
`ATP: speed ${SupervisedTrain.speedKmh} km/h, authority ${OnboardComputer.speedAuthorityKmh} km/h, ` +
`stopping distance ${stoppingDistance.toFixed(1)} m.`,
);
if (isOverSpeed) {
OnboardComputer_triggerEmergencyBrake();
} else {
console.log(
`ATP: braking curve satisfied — Train "${SupervisedTrain.id}" is safe to continue.`,
);
}
}
function OnboardComputer_triggerEmergencyBrake() {
OnboardComputer.applyEmergencyBrake(SupervisedTrain.id);
}
//#endregion
// System Operation
try {
Balise_transmitAuthority();
} catch (e) {
// Safety-critical: any fault in ATP supervision must result in a stop.
OnboardComputer.applyEmergencyBrake(SupervisedTrain.id);
console.error(
"ATPSupervision: fault during supervision — emergency brake applied as fail-safe:",
e,
);
}
}
// =============================================================================
// Example — wiring the three contexts together for a single "corridor run"
// =============================================================================
// --- data objects ---
const trainA = new Train("A");
const platform4 = new Block("Platform-4");
const approachBlock = new Block("Approach-Block");
const sw1 = new Switch("SW-1");
const sw2 = new Switch("SW-2");
const entrySignal = new Signal("SIG-Entry");
const approachSignal = new Signal("SIG-Approach");
const balise1 = new Balise("KM-12.4", 80, 5); // 80 km/h, slight downhill
const atp = new OnboardComputer();
trainA.speedKmh = 75; // train is travelling at 75 km/h
// 1. Dispatcher asks for a path → Interlocking aligns, locks, signals GREEN.
RouteRequest(
{ trainId: trainA.id, destination: "Platform-4" },
[platform4, approachBlock],
[sw1, sw2],
entrySignal,
);
// 2. Train enters the block (wheels short the circuit).
trainA.enterBlock(platform4);
// 3. ATP reads the balise just before the platform throat.
ATPSupervision(trainA, balise1, atp);
// 4. Train clears the block; approach lighting activated for the next train.
trainA.leaveBlock(platform4);
BlockClearance(
trainA,
platform4,
[sw1, sw2],
entrySignal,
approachSignal, // following train will see APPROACH instead of RED
new Train("B"), // the following train
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment