|
// ============================================================================= |
|
// 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 |
|
); |