Eine kleine, tatsächlich ausführbare Simulationsumgebung für betriebliche Fahrstraßenentscheidungen. Die Simulationssemantik liegt in Prolog; Node.js bettet mit Tau Prolog einen vollständig in JavaScript geschriebenen Prolog-Interpreter ein.
Das Projekt ist bewusst kein sicherheitsgerichtetes Stellwerk. Der Agent fordert Fahrstraßen an. Eine spätere reale Integration soll dieselben Wünsche an ein vorhandenes Stellwerk schicken, das Zulässigkeit, Sicherung und Ausführung selbst garantiert.
- expliziter, unveränderlich weitergereichter Episodenzustand ohne
assert/retract - deklarative, versionierte JSON-Netzbeschreibungen für unterschiedliche Stellwerke
- validierte Übersetzung der Netzbeschreibung in Prolog-Fakten
- dynamisches SVG-Gleisbild aus denselben Fakten wie die Simulation
- zoombares und verschiebbares Gleisbild für große Knoten
- skalierbare, DiB-/iLBS-orientierte Arbeitsplatzschale mit Bereichsübersicht, gesamthafter Stellwerkslupe und getrennten Nebenanwendungen
- Zugkarten mit direktem Gleisbildfokus für aktuellen Ort, Start, Ziel und Fahrplanhalte
- deklarative Hauptsignale mit explizitem Standort, Funktion, Richtung und zugeordneten Fahrstraßen
- Fahrpläne mit Soll-Ankunft, Soll-Abfahrt und Mindesthaltezeit
- importierbare reale Sollfahrpläne aus DELFI-/gtfs.de-Daten mit Quellenmanifest
- dynamischer Bildfahrplan mit Soll- und aufgezeichneten Ist-Linien
- zeitgesteuerter Netzeinbruch noch nicht eingefahrener Züge
- Haltzwang am nächsten Fahrplanhalt; Durchfahrten nur ohne geplanten Halt
- Start-Ziel-Bedienung über ein Kontextmenü im Gleisbild
- atomare Durchfahrstraßen aus deklarativen Teilfahrstraßen
- bidirektionale Fahrten im Regionalnetz
- Aktionen auf Fahrstraßenebene:
request_route(Route),cancel_route(Route)vor der Einfahrt undwait - Action Masking über
available_actions/2 - asynchrone Fahrten über mehrere Ticks
- parallele, konfliktfreie Fahrten
- Ressourcenkonflikte zwischen aktiven Fahrstraßen
- deklarativer Flankenschutz mit lageverträglich gemeinsam genutzten Schutzweichen
- deklarative Durchrutschwege mit Schutzabschnitten und Schutzweichen hinter dem Ziel
- abschnittsweise Freigabe geräumter Abschnitte und Gleissegmente
- konkrete Gründe für abgelehnte Fahrstraßen
- strukturierte Observations
- austauschbare Reward-Modelle
- reproduzierbare, Seed-basierte Anfangsverspätungen und Netzeinbrüche
- reproduzierbar streuende Fahrzeiten je Zug und Fahrstraße
- Events pro Zustandsübergang
- pro Netz deterministisch erzeugte Feature- und Action-Schemata
- JSONL-Trajektorien für Offline-Training
- ein kleiner Scikit-learn-Imitationslern-Baseline-Test
- automatisierte Invarianten- und Episodentests
Die zentrale Semantik ist:
State + Action
|
v
Prolog transition/5
|
+--> NextState
+--> Events
+--> Done
RewardModel + Transition
|
v
Reward + Breakdown
- Node.js 20 oder neuer
- npm
- optional für den ML-Smoke-Test: Python 3 mit den Paketen aus
requirements.txt
Getestet wurde die mitgelieferte Version mit Node.js 22 und Python 3.
npm ci
npm test
npm run demoEin anderes Szenario und ein anderer Lauf-Seed:
node src/demo.js parallel 7
node src/demo.js congested 42Der Seed bestimmt die konkrete Betriebslage: Anfangsverspätungen,
Netzeinbrüche und Fahrzeiten werden aus ihm reproduzierbar gezogen. Gleicher
Seed bedeutet identisches Replay; unterschiedliche Seeds erzeugen auch mit
der deterministischen Prioritätspolicy unterschiedliche Trajektorien. Seed 0
ist der explizite störungsfreie Referenzlauf.
Die lokale Weboberfläche ist als DiB-/iLBS-orientierte Schulungsadaption
aufgebaut. Sie ist kein offizieller DB-Bedienplatz und kein
sicherheitsgerichtetes Stellwerk. Die technische Bestandsaufnahme, belegte
Vorbilder, Grenzen und nächsten Migrationsschritte stehen in
docs/DIB_MIGRATION.md.
Das ausführliche Benutzerhandbuch erklärt alle Bedienelemente, Anzeigezustände und regulären Abläufe anhand reproduzierbarer Screenshots.
npm run webDanach im Browser öffnen:
http://127.0.0.1:3000
Die betriebliche Arbeitsfläche besteht aus einer kompakten Bereichsübersicht, der zoombaren und verschiebbaren Stellwerkslupe und den Nebenanwendungen Disposition, Fahrstraße, Meldungen und Bildfahrplan. Netz, Szenario, Seed, Reward-Modell, Automatik und Betriebsqualität liegen getrennt in der standardmäßig eingeklappten Schulungssteuerung. Eine permanente Statuszeile hält Episodenzustand, Simulationszeit, Fahrstraßen und Bewertung sichtbar.
Tag- und Nachtschaltung sowie der Zustand des Nebenfensters werden lokal im Browser gespeichert. Ein Klick in die Bereichsübersicht zentriert die Stellwerkslupe; deren aktueller Ausschnitt wird dort als Rahmen angezeigt. Die Übersicht ist derzeit aus derselben Topologie abgeleitet. Für sehr große Anlagen ist eine separat projektierte Übersicht mit eigener Detailstufe im Migrationsplan vorgesehen.
Falls Port 3000 bereits belegt ist, kann ein anderer lokaler Port gewählt
werden, zum Beispiel mit PORT=43117 npm run web.
Der Server entdeckt beim Start alle gültigen rail-network-v7-Dateien unter
networks/. Im Feld Stellwerk kann ohne Serverneustart zwischen
Demo-Stellwerk, Rangierbahnhof, Regionalbahnhof sowie den Knoten Bamberg und
Nürnberg gewechselt werden. Szenarien, Gleisbild, Fahrstraßen und Zustand
werden dabei atomar auf das gewählte Netz umgestellt.
RAIL_NETWORK ist weiterhin optional nutzbar, um die anfängliche Auswahl
vorzugeben:
RAIL_NETWORK=networks/yard.json PORT=43117 npm run webBeispielsweise direkt mit dem größeren Regionalbahnhof starten:
RAIL_NETWORK=networks/regional.json PORT=43117 npm run webOder mit dem komplexen Bamberger Knoten:
RAIL_NETWORK=networks/bamberg.json PORT=43117 npm run webDas größere Nürnberg-Korridormodell lässt sich ebenso direkt öffnen:
RAIL_NETWORK=networks/nuernberg.json PORT=43117 npm run webEine verfügbare Fahrstraße kann über die Fahrstraßenliste oder per
Start-Ziel-Bedienung im Gleisbild angefordert werden. Der Fahrweg erscheint
erst als Vorschau, Auswahl oder aktive Fahrstraße. Bei einer blockierten
Fahrstraße zeigt der Inspektor die konkreten Gründe aus explainRoute/1.
Neben manuellen wait-Schritten kann die eingebaute Prioritätspolicy eine
Episode automatisch abspielen.
Das rechte Nebenfenster hält Zugdisposition, Fahrstraßeninspektor,
quittierpflichtige betriebliche Meldungen und Bildfahrplan auf derselben
Arbeitsebene wie das Gleisbild. Der Fahrstraßen-Tab öffnet sich automatisch bei
einer Fahrstraßenauswahl. Normale Fortschrittsereignisse bleiben als
Information sichtbar, lösen aber keine Quittieranforderung aus.
Die Leitstand-Automatik berücksichtigt dabei Durchfahrstraßen und bevorzugt
fahrplanmäßig fällige Fahrstraßen. Vor einer Sollfahrt wartet sie bis zum
spätesten planmäßigen Einstellzeitpunkt; bei Konkurrenz gewinnt die kleinste
Zeitreserve. Erst danach entscheiden Zugpriorität, Planabweichung und die
Bevorzugung einer passenden Durchfahrt. Für granulare
Trainingsläufe bleibt highestPrioritySegmentPolicy verfügbar; sie disponiert
bewusst ausschließlich einzelne Teilfahrstraßen.
Per Rechtsklick auf einen Abschnitt kann außerdem ein Fahrstraßenstart und
danach ein Ziel gewählt werden. Das Kontextmenü zeigt alle deklarierten
Varianten einschließlich Bahnsteigdurchfahrten und ihrer aktuellen
Verfügbarkeit; erst die gewählte, vollständig geprüfte Variante wird atomar
eingestellt.
Das Auto-Tempo lässt sich dabei während der Wiedergabe zwischen 0,5 und 6
Sekunden pro Simulationsschritt einstellen; voreingestellt sind 2 Sekunden.
Das Gleisbild verwendet eine kompakte, stellwerksnahe Symbolik auf schwarzem Grund: kantige Gleisverläufe, dünne gelbe Grundlinien, rote Belegtmeldungen, gelbe Reservierungen und grün hervorgehobene aktive Fahrstraßen. Es ist eine Simulationsdarstellung und keine norm- oder herstellerspezifische Bedienoberfläche. Während einer Fahrt wird genau der aktuell befahrene Gleisfreimeldeabschnitt rot angezeigt; die übrigen Ressourcen der Fahrstraße bleiben gesichert.
Alle Netze werden von demselben Renderer und mit derselben Symbolsprache gezeichnet. Die deklarierte Abschnitts- und Segmentgeometrie ist dabei die einzige Laufzeitgeometrie: Fahrstraßen liegen exakt auf den zugehörigen physischen Segmenten, Weichensymbole werden aus der jeweils benutzten Verbindung abgeleitet, und Zug- sowie Signalpositionen sind in demselben Koordinatensystem verankert. Es gibt keine zweite, nur optisch darübergelegte Stelltafel. Die Weichenverbindungen bleiben auch in der eingepassten Übersicht sichtbar; kollisionsfrei platzierbare Weichenkennungen werden ab 225 % Zoom eingeblendet, Kennungen einer ausgewählten oder aktiven Fahrstraße bereits vorher.
Die Nebenanwendung Bildfahrplan zeichnet den zeitlichen Betrieb auf. Die Wegachse entsteht automatisch aus den Abschnittskoordinaten des aktiven Netzes. Gestrichelte Linien zeigen die Soll-Lage aus den Fahrplan-Calls, durchgezogene Linien den seit dem letzten Reset im Browser beobachteten Ist-Verlauf einschließlich Halten und Planabweichung.
Die Oberfläche ist eine lokale Simulations- und Diagnoseansicht. Sie enthält keine Stellwerks- oder Sicherheitslogik und steuert keine reale Infrastruktur.
Ein Stellwerk wird vollständig in einer Datei im Format rail-network-v7
beschrieben. Das mitgelieferte Hauptbeispiel liegt in
networks/demo.json, ein strukturell anderes Testnetz in
networks/yard.json, ein größeres synthetisches Netz in
networks/regional.json und der aus veröffentlichten
Infrastrukturunterlagen abgeleitete Ausbauzustand von Bamberg Hbf in
networks/bamberg.json. Das deutlich größere,
aus OpenStreetMap-Gleisgeometrie abgeleitete Kernnetz von Nürnberg Hbf liegt in
networks/nuernberg.json. Für Editor-Unterstützung
gibt es networks/schema.json.
Das Bamberg-Netz bildet die dokumentierte betriebliche Gliederung nach: vier Bahnsteige mit den Bahnsteiggleisen 1 und 3–8, das bahnsteiglose Durchfahrgleis 2, die Durchfahr-/Gütergleise 9–11, getrennte Personen- und Güterfahrwege sowie die nördlichen Richtungen Lichtenfels und Schweinfurt. Das SVG ist ein stellwerksgerechter Spurplan und keine maßstäbliche Vermessung. Modelliert sind reguläre Fahrwege des enthaltenen Fahrplans; Nebengleise, Abstellanlagen, exakte reale Signalstandorte und bauzeitliche Zwischenzustände liegen außerhalb des Ausschnitts. Die dargestellten Hauptsignale sind funktional angeordnete Simulationsentitäten, keine Rekonstruktion des realen Bamberger Signalplans.
Bekannter Modellierungsbedarf: Die direkten Verbindungen vom südlichen Güterkopf über Gleis 2 zum nördlichen Güterkopf erzeugen derzeit zwei lange, kreuzende Diagonalen. Sie müssen später durch explizite Übergangs- beziehungsweise Kreuzungsabschnitte mit gemeinsamen Konfliktressourcen ersetzt werden. Eine rein optische Änderung der SVG-Pfade reicht nicht, weil auch die Fahrstraßensemantik betroffen ist.
Grundlagen der Modellierung sind der DB-Orientierungsplan Bamberg, das im Planfeststellungsverfahren veröffentlichte Brandschutzkonzept der Bahnsteige mit Gleisnutzung und Ausbauzustand sowie die OpenRailwayMap-Lage für die topologische Orientierung. Die Stellwerksdatenbank dokumentiert das Bamberger Sp-Dr-S60-Stellwerk Baf; dessen konkrete Sicherungslogik wird nicht nachgebildet.
Das Nürnberg-Netz bildet den Personenbahnhof mit allen Gleisen 1–23 und den unmittelbaren westlichen und östlichen Bahnhofsköpfen ab. 97 einzeln modellierte Weichen stammen aus einem eingefrorenen OSM-Datenstand; ihre Stelllagen bezeichnen die tatsächlich von einer Fahrstraße benutzte Verbindung am jeweiligen Weichenknoten. 104 gemeinsam genutzte Gleissegmente sorgen dafür, dass Konflikte entlang derselben physischen Gleiskanten entstehen. Fünf Zulaufkorridore und 92 gerichtete Fahrstraßen decken die enthaltenen Beispielverkehre und Alternativgleise über alle 23 Bahnsteiggleise ab.
OSM bestimmt die Topologie, nicht die Bildschirmkoordinaten. Die kuratierte
schematische Anordnung liegt getrennt in
networks/layouts/nuernberg.json: Sie ordnet
die reale Knotenfolge in horizontale Gleise und rechtwinklige beziehungsweise
45-Grad-Verbindungen, ohne dafür Gleiselemente auszublenden oder zusätzliche
Scheinverbindungen einzuführen. Der Generator überführt diese Anker in die
kanonischen Segmentpfade von networks/nuernberg.json
und setzt jede Fahrstraßengeometrie daraus zusammen. Dadurch bleiben
Topologie, Konfliktressourcen und Gleisbild deckungsgleich, während der Plan
lesbar und stellwerksnah bleibt. Zwei im OSM-Datenstand als Brücken geführte
Gleiskreuzungen bleiben als deklarierte Überwerfungen erhalten; der gemeinsame
Renderer kennzeichnet ihr oberes Gleis, ohne an der Kreuzung eine Verbindung
zu erfinden.
Der Ausschnitt ist trotzdem nicht das gesamte Stellwerk Nhf: Abstellgruppe Ost, Betriebswerk, Hauptgüterbahnhof, Jansenbrücke und entfernte Abzweige enden an klar benannten Randknoten. OSM liefert zudem keine verlässliche Sp-Dr-S600-Fahrstraßentabelle, Freimeldegrenzen, Flankenschutz- oder Durchrutschwegdaten. Signalzuordnung, betriebliche Abschnittsgrenzen, Gleiszuweisungen der acht Beispielzugleistungen und Fahrzeiten bleiben daher Simulationsannahmen und dürfen nicht für reale Fahrmöglichkeiten oder sicherungstechnische Aussagen verwendet werden.
Als überprüfbare Grundlagen dienen der aktuelle
DB-InfraGO-Gleisplan Nürnberg Hbf, die
Stellwerksdatenbank zu Nhf, der
DB-Orientierungsplan Nürnberg Hbf
mit den Gleisen 1–23 und der
DB-Regelfahrplan Nürnberg Hbf
für reale Korridore und beispielhafte Gleisnutzungen. Das Netz übernimmt daraus
die betriebliche Gliederung, nicht die vollständige Sicherungstopologie. Die
OSM-Quelldaten, ODbL-Attribution und der reproduzierbare Generator sind unter
networks/sources dokumentiert.
Das Nürnberger Szenario nn_gtfs_werktag_0700 verwendet reale Sollzeiten vom
Montag, 20. Juli 2026. Es enthält eine gegenläufige S2-Betriebslage: Zug
261420 fährt Altdorf–Nürnberg–Roth mit Halt 07:06–07:11 Uhr, Zug 1196219
Roth–Nürnberg–Altdorf mit Halt 07:08–07:14 Uhr. Ein Tick entspricht in diesem
Extrakt 30 Sekunden. Mit Seed 0 fährt die Leitstand-Automatik alle vier
Ankünfte an den Modellgrenzen und in Nürnberg ohne Planabweichung.
Die Daten stammen aus dem freien deutschen Regionalverkehrs-GTFS von
gtfs.de, bereitgestellt von DELFI e.V. unter
CC BY 4.0. Der kleine,
eingecheckte und prüfbare Datenextrakt liegt in
data/timetables/nuernberg-2026-07-20.json,
die Auswahl samt Archiv-Hashes in
data/timetables/nuernberg-2026-07-20-selection.json.
Die großen Rohfeeds werden nicht versioniert.
Solange der ausgewählte Betriebstag noch im jeweiligen Feed enthalten ist, kann der Extrakt aus entpackten GTFS-Verzeichnissen neu erzeugt und danach in das Netz übernommen werden:
npm run import:timetable -- \
--selection data/timetables/nuernberg-2026-07-20-selection.json \
--rv-dir /tmp/gtfs-rv \
--output data/timetables/nuernberg-2026-07-20.json
npm run build:timetableGTFS liefert für Nürnberg in diesem Feed keine verlässliche Bahnsteiggleis- Zuordnung. Die Zuordnung zu Gleis 22/23 ist daher ausdrücklich eine korridorverträgliche Modellabbildung; reale Zeiten, Linie, Fahrtrichtung und GTFS-Trip-IDs bleiben im Extrakt erhalten. Der Netzvalidator lehnt jeden Fahrplanpunkt ab, dessen Zeitabstand kürzer als die schnellste gerichtete Fahrmöglichkeit des Modells ist. Der Episodentest prüft zusätzlich die konfliktfreie Pünktlichkeit des eingebauten Referenzszenarios.
Eine Beschreibung enthält:
- Netzkennung, Anzeigename und Topologieversion
- Abschnitte einschließlich Typ, SVG-Position und optional eigenem SVG-Pfad für eine maßgeschneiderte Gleisfreimeldegeometrie
- physische
trackSegmentszwischen Abschnitten mit eigener SVG-Geometrie und Position für die Zuganzeige - optionale
gradeSeparatedCrossings, die echte Überwerfungen zwischen zwei Segmenten als obere und untere Ebene kennzeichnen, ohne sie topologisch zu verbinden - Weichen mit zulässigen und initialen Lagen
- Hauptsignale mit Funktion, Quellabschnitt, Fahrtrichtung, SVG-Position und vollständig zugeordneter Menge kontrollierter Fahrstraßen
- Fahrstraßen mit Quelle, Ziel, Fahrzeit, Fahrwegweichen sowie optionalem Flankenschutz und Durchrutschweg
- optional zulässige Endziele einer Teilfahrstraße zur Richtungsprüfung
- explizit zu reservierende Abschnitts- und Weichenressourcen
- eine optional explizite, geordnete
sectionSequencefür die abschnittsgenaue Fortschreibung eines fahrenden Zugs - SVG-Pfad und Labelposition jeder Fahrstraße
- optionale
routePlans, die verbundene Teilfahrstraßen zu einer atomaren Durchfahrt zusammensetzen - netzweit wiederverwendbare Zugleistungen (
services) mit Priorität und geordneter Folge von Betriebsstellen - Szenarien, die Zugleistungen auswählen und ihre Anfangsverspätung festlegen
Verkürztes Beispiel:
{
"$schema": "./schema.json",
"schemaVersion": "rail-network-v7",
"id": "my_interlocking",
"name": "Mein Stellwerk",
"topologyVersion": "my_topology_v1",
"viewBox": [0, 0, 800, 400],
"sections": [
{ "id": "entry", "label": "Einfahrt", "kind": "entry", "position": [80, 200] },
{ "id": "exit", "label": "Ausfahrt", "kind": "exit", "position": [720, 200] }
],
"trackSegments": [
{
"id": "entry_exit_track",
"label": "Einfahrt – Ausfahrt",
"from": "entry",
"to": "exit",
"path": "M 130 200 L 670 200",
"position": [400, 200]
}
],
"gradeSeparatedCrossings": [],
"switches": [],
"signals": [
{
"id": "sig_entry_r",
"label": "A",
"kind": "entry",
"section": "entry",
"direction": "right",
"position": [136, 200],
"routeIds": ["entry_exit"]
}
],
"routes": [
{
"id": "entry_exit",
"from": "entry",
"to": "exit",
"durationTicks": 3,
"switchRequirements": [],
"flankProtection": [],
"overlapSections": [],
"overlapSwitchRequirements": [],
"resources": ["section:entry", "section:exit"],
"sectionSequence": ["entry", "exit"],
"path": "M 130 200 L 670 200",
"labelPosition": [400, 180]
}
],
"services": [
{
"id": "t1",
"priority": 2,
"calls": [
{ "section": "entry", "departureTick": 0 },
{ "section": "exit", "arrivalTick": 3 }
]
}
],
"scenarios": [
{
"id": "basic",
"disturbances": {
"initialDelay": { "maximumExtraTicks": 3 },
"travelTime": { "maximumExtraTicks": 2 }
},
"services": [
{ "serviceId": "t1", "initialDelayTicks": 0 }
]
}
]
}gradeSeparatedCrossings ist reine, aber topologisch bedeutsame
Darstellungsmetadaten. over und under referenzieren zwei vorhandene
trackSegments, deren Linien sich ohne Gleisverbindung kreuzen. Der generische
Renderer stellt das obere Segment frei und zeichnet ein kurzes Brückendeck mit
Querstrichen; dessen Reservierungs- und Belegungsfarbe folgt weiterhin dem
Segmentzustand. Der Validator lehnt unbekannte Segmente, Selbstbezüge und
doppelte beziehungsweise umgedrehte Paare ab; beide Pfade müssen sich genau
einmal im Inneren ihrer Linienzüge schneiden.
switchRequirements beschreibt Weichen im eigentlichen Fahrweg.
flankProtection enthält zusätzliche Schutzweichenlagen, die seitliche
Gefährdungen des Fahrwegs abweisen. Beispielsweise schützt die Einfahrt nach
Gleis 1 im Rangiernetz ihren Fahrweg durch die Gegenlage der Ausfahrweiche:
"flankProtection": [
{ "switchId": "w_merge", "position": "diverging" }
]Eine Weiche darf innerhalb derselben Basisfahrstraße nicht zugleich Fahrweg- und Schutzweiche sein. Bei einer zusammengesetzten Durchfahrt hat eine Fahrwegweichenanforderung Vorrang vor dem Flankenschutz eines einzelnen Teilwegs; verbleibende widersprüchliche Schutzlagen lehnt der Compiler ab.
overlapSections und overlapSwitchRequirements beschreiben den
Durchrutschweg hinter dem Ziel. Diese Abschnitte und Weichen werden beim
Einstellen mitgesichert, gehören aber ausdrücklich nicht zur
sectionSequence oder route_traversal: Der Zug hält am Ziel und befährt den
Durchrutschweg nicht. Schutzabschnitte müssen vom Ziel aus in Fahrtrichtung
erreichbar sein. Bei zusammengesetzten Durchfahrten gilt ausschließlich der
Durchrutschweg des letzten Teilwegs; interne Teilwegziele werden durchfahren.
Jede Zugleistung beginnt mit einer Soll-Abfahrt am Einbruchabschnitt und endet
mit einer Soll-Ankunft an einem Abschnitt vom Typ exit oder boundary.
Zwischenhalte besitzen Soll-Ankunft, Soll-Abfahrt und optional
minimumDwellTicks. Die Soll-Abfahrt muss mindestens Soll-Ankunft plus
Mindesthaltezeit betragen. Alle aufeinanderfolgenden Betriebsstellen müssen im
gerichteten Fahrstraßengraph erreichbar sein. Der Compiler lehnt inkonsistente
Fahrpläne vor dem Start ab.
initialDelayTicks ist die feste Basisabweichung einer Zugleistung im
Szenario. Beim Reset kommt eine aus Seed und Service-ID reproduzierbar gezogene
Zusatzabweichung zwischen null und
disturbances.initialDelay.maximumExtraTicks hinzu. Die Summe verschiebt den
tatsächlichen Netzeinbruch gegenüber der ersten Soll-Abfahrt. Bis dahin hat die
Zugleistung den Status pending und
belegt keine Infrastruktur. Ist der Einbruchabschnitt zum Einbruchzeitpunkt
belegt oder bereits für eine Fahrstraße reserviert, wartet die Zugleistung
außerhalb des Netzes und baut weitere Planabweichung auf.
Ein Tick wird synchron in zwei Phasen ausgewertet: Zuerst bewegen sich die bereits im Netz befindlichen Züge, danach werden fällige Einbrüche gegen diesen gemeinsamen Nachzustand geprüft. Wird derselbe Einbruchabschnitt von mehreren Zügen beansprucht, gewinnt unabhängig von der JSON-Reihenfolge zuerst der frühere Soll-Einbruch, dann die höhere Priorität und schließlich die alphabetisch kleinere Zug-ID. Ein im selben Tick geräumter Abschnitt darf anschließend neu belegt werden.
Die beobachtete Planabweichung bleibt während planmäßiger Fahrzeit und beim zeitlosen Einstellen eines Folgewegs unverändert. Ein zusätzlicher Wartetick in einem Abschnitt erhöht sie um genau einen Tick; an Fahrplanankünften und -abfahrten wird sie wieder gegen die zugehörige Soll-Zeit ausgewertet. Dadurch entsteht bei mehrteiligen Fahrwegen kein vorübergehender Schein-Delay aus der seit der vorherigen Soll-Abfahrt verstrichenen regulären Fahrzeit.
Beim Einstellen einer Fahrstraße wird einmalig eine zusätzliche Fahrzeit
zwischen null und disturbances.travelTime.maximumExtraTicks gezogen und im
Zugzustand gespeichert. Das Ereignis travel_time_realized protokolliert
Grundfahrzeit, Zusatz und realisierte Fahrzeit. Die Ziehung wird während einer
Transition niemals wiederholt; Action-Replay mit demselben Seed bleibt daher
bytegenau reproduzierbar. Zurücknehmen und erneutes Einstellen derselben
Fahrstraße am selben Fahrplanpunkt erzeugt denselben Wert und erlaubt kein
„Würfeln bis die Fahrzeit passt“.
Fehlt sectionSequence, leitet der Compiler sie aus der Reihenfolge aller
section:*-Ressourcen ab. Zusammengesetzte routePlans verbinden die
Abschnittsfolgen ihrer Teilfahrstraßen automatisch und entfernen den doppelten
Übergangsabschnitt. durationTicks muss lang genug für alle Elemente der
Traversierungsfolge sein, damit jedes befahrene Segment und jeder
Zwischenabschnitt wenigstens einen Tick lang belegt werden kann.
Für vorhandene trackSegments ermittelt der Compiler den eindeutigen kürzesten
Segmentweg zwischen from und to. Bei mehreren möglichen Wegen muss die
Fahrstraße ein explizites segmentSequence angeben. Segmentressourcen werden
automatisch in die Fahrstraßensicherung aufgenommen. Während der Fahrt meldet
die Engine abwechselnd die befahrenen Segmente und dazwischenliegende
Abschnitte als belegt.
Beim Start validiert src/network-compiler.js Referenzen, Eindeutigkeit,
Ressourcen, Weichenlagen, Erreichbarkeit und Layoutdaten. Danach erzeugt er
Prolog-Fakten und konsultiert sie gemeinsam mit dem generischen Kern aus
src/rail_simulator.pl. Für Prüfung oder Archivierung können die Fakten auch
explizit ausgegeben werden:
npm run compile-network -- networks/yard.json --output data/yard-network.plDie erzeugte Datei enthält unter anderem section/1, switch_position/2,
route/6, route_flank_requirements/2, route_overlap_sections/2,
route_overlap_switch_requirements/2, route_sections/2,
route_traversal/2, track_segment/1,
service/3, scenario/1, scenario_service/3, scenario_disturbance/3,
stabile Seed-Salts und die
Layoutfakten für die UI.
regional.json beschreibt den „Regionalbahnhof Nord“ mit neun Abschnitten,
sechs Weichen und drei Bahnsteigabschnitten. Es enthält 24 bidirektionale
Teilfahrstraßen sowie 24 aus jeweils zwei Teilfahrstraßen materialisierte
Durchfahrten. Teilfahrstraßen dauern fünf bis sieben Ticks; Durchfahrten zehn
bis vierzehn Ticks. Neben Start und Ziel reservieren sie Zwischenabschnitte
und alle benötigten Weichen.
Die Außenabschnitte sind als bidirektionale boundary-Abschnitte deklariert.
Züge können deshalb sowohl West → Ost als auch Ost → West verkehren. Explizite
destinations verhindern dabei betriebliche Rückwärtsumwege, obwohl das
physische Netz in beiden Richtungen zusammenhängend ist.
Bei solchen langen Fahrstraßen prüft die Engine alle als section:*
angegebenen Ressourcen auf Belegung. Ein Zug in einem Zwischenabschnitt
blockiert die Fahrstraße deshalb ebenso wie ein belegter Zielabschnitt.
Der komplette Test inklusive Dataset-Export und Training einer kleinen Baseline:
python3 -m pip install -r requirements.txt
npm run smokenpm run generate -- \
--network networks/demo.json \
--episodes 100 \
--scenario all \
--seed 1000 \
--output data/trajectories.jsonl \
--policy priority \
--max-steps 100 \
--reward-model balanced_v1Der Generator erhöht den Seed pro Episode. Dadurch unterscheiden sich auch
Episoden der deterministischen priority-Policy in Einbruchslage,
Fahrstraßendauer, Zustandsfolge und Reward. Seed und konkrete
Anfangsrealisierung werden in jedem Trajektoriendatensatz protokolliert; die
Störungsprofile stehen im Manifest.
Mögliche Werte:
--scenario basic|congested|parallel|all--network <Pfad zu rail-network-v7 JSON>--policy priority|random--reward-model balanced_v1|throughput_v1
Neben der JSONL-Datei entsteht eine Manifestdatei:
data/trajectories.jsonl
data/trajectories.jsonl.manifest.json
Das Manifest enthält die Reihenfolge aller Features, das Action-Vokabular und die Versionsinformationen der Umgebung.
python3 train_baseline.py \
data/trajectories.jsonl \
--model-output data/priority-policy-baseline.joblibDie Baseline hängt den Action-Mask-Vektor an den Observation-Feature-Vektor und lernt mit einem Random Forest die eingebaute Prioritätspolitik zu imitieren. Das ist nur ein End-to-End-Smoke-Test der Datenpipeline. Eine hohe Accuracy ist hier kein Beleg für eine gute Dispositionsstrategie; die Zielpolicy ist klein und deterministisch.
const {
RailEnvironment,
highestPriorityPolicy,
runEpisode,
} = require("./src");
async function main()
{
const environment = await RailEnvironment.create({
rewardModel: "balanced_v1",
networkPath: "networks/demo.json",
});
const reset = await environment.reset({
scenario: "congested",
seed: 42,
});
console.log(reset.observation);
console.log(reset.availableActions);
const result = await environment.step({
type: "request_route",
routeId: "p1_e",
});
console.log(result.reward);
console.log(result.events);
// Die Fahrstraße ist jetzt eingestellt und bis zur Einfahrt rücknehmbar.
// Erst ein wait-Tick lässt den Zug losfahren.
const summary = await runEpisode(environment, {
scenario: "basic",
seed: 7,
maxSteps: 100,
policy: highestPriorityPolicy,
});
console.log(summary.terminalReason);
}
main();Die relevante Environment-Oberfläche ist:
reset({ scenario, seed })
-> observation, availableActions, metadata
step(action)
-> observation, reward, done, terminalReason,
availableActions, events, rewardBreakdown
explainRoute(routeId)
-> konkrete Ablehnungsgründe
Eine erfolgreiche request_route- oder cancel_route-Aktion ist eine
zeitlose Bedienhandlung. Sie verändert den Stellwerkszustand, aber nicht die
Simulationszeit. Eine abgelehnte oder unbekannte Aktion läuft dagegen bewusst
durch den normalen Transitionszweig: Sie verbraucht einen Tick und erhält
zusätzlich die Ablehnungsstrafe. Für maskierte Agenten tritt dieser Fall nicht
auf; bei unmaskierten Agenten bildet er eine zeitbehaftete Fehlbedienung ab.
Eine unzulässige Rücknahme bleibt dabei eine bekannte Aktion und unterscheidet
zwischen unbekannter, nicht eingestellter und bereits befahrener Fahrstraße.
Der Kern liegt in src/rail_simulator.pl:
initial_state(+Scenario, +Seed, -State).
initial_state_diagnostics(+Scenario, +Seed, -State, -Problems).
available_actions(+State, -Actions).
transition(+State, +Action, -NextState, -Events, -Done).
reward(+Model, +State, +Action, +NextState, +Events, +Done,
-Reward, -Breakdown).
step(+State, +Action, +RewardModel, -NextState, -Events,
-Reward, -Done, -Breakdown).
observation(+State, -Observation).
route_problems(+State, +Route, -Problems).
state_invariant(+State, -Problems).Der Host verwendet die diagnostische Initialisierung. Kann eine deklarierte Zugleistung wegen unvollständiger oder inkonsistenter Fakten nicht materialisiert werden, bricht der Reset mit einem benannten Problem ab, statt die Zugleistung still aus dem Anfangszustand wegzulassen.
Der variable Zustand ist ein normaler Prolog-Term:
state(
Time,
[
train(
t1,
at(west_a),
east_exit,
3,
0,
schedule(0, [
call(platform_1, 4, 6, 2),
call(east_exit, 8, none, 0)
], disturbance(0, 0, 0, 2))
),
train(
t2,
pending(west_b, 3),
east_exit,
2,
3,
schedule(0, [
call(platform_2, 5, 7, 2),
call(east_exit, 9, none, 0)
], disturbance(0, 3, 0, 2))
)
],
[
switch_state(w_in, straight),
switch_state(w_platform, diverging),
switch_state(w_exit, straight)
]
).Dadurch sind Episoden voneinander isoliert, Reset und Replay sind trivial, und mehrere Umgebungen können später in getrennten Interpreter-Sessions parallel laufen.
Das deklarative Demo-Netz beschreibt folgende Anlage:
west_a ----\
>-- junction --<-- platform_1 --\
west_b ----/ \-- platform_2 ----> east_exit
Der Compiler übersetzt beispielsweise die deklarierte Fahrstraße wa_j in:
route(
wa_j,
west_a,
junction,
2,
[required_switch(w_in, straight)],
[section(west_a), section(junction), switch(w_in)]
).
route_flank_requirements(wa_j, []).
route_overlap_sections(wa_j, []).
route_overlap_switch_requirements(wa_j, []).Eingestellte Fahrstraßen reservieren zunächst alle Ressourcen. Nach der
Einfahrt werden geräumte Abschnitte und Gleissegmente hinter dem Zug
abschnittsweise freigegeben. Weichen bleiben konservativ bis zum Abschluss der
Fahrstraße gesperrt, solange die Netzbeschreibung keine eigenen
Weichenfreigabepunkte modelliert. Das gilt auch für Schutzweichen. Zwei
Fahrten dürfen parallel laufen, wenn sich ihre noch gesperrten physischen
Ressourcen nicht überschneiden und alle gemeinsam benötigten Weichen dieselbe
Lage verlangen. Gegensätzliche Lagen blockieren sich; berührt mindestens eine
Anforderung den Flankenschutz, meldet der Inspektor den konkreten
flank_protection_conflict.
Durchrutschwegabschnitte und -weichen werden unabhängig von der
Teilfahrstraßenauflösung bis zur Ankunft beziehungsweise Rücknahme gehalten.
Sie blockieren sowohl belegte Schutzabschnitte als auch konfligierende aktive
Fahrstraßen und liefern dafür eigene overlap_*-Begründungen.
Layoutfakten wie section_definition/5, signal_definition/8 und
route_layout/4 werden über
network_catalog/1 an die Weboberfläche gegeben. Damit enthält das Frontend
keine hart codierten Abschnitts-, Signal-, Weichen- oder Fahrstraßenkennungen.
Die rein zeichnerische Über-/Unterordnung aus gradeSeparatedCrossings bleibt
deklarative Host-Metadaten und wird zusammen mit diesem Katalog ausgeliefert;
sie erzeugt bewusst keine zusätzliche Prolog-Ressource oder Gleisverbindung.
Die strukturierte Observation enthält:
- Simulationszeit
- Zustand jedes Abschnitts und Gleissegments: frei, belegt oder reserviert
- aktuelle Weichenlagen
- Anzeige jedes Hauptsignals: Halt oder Fahrt einschließlich zugehöriger Fahrstraße und Zug
- Status, aktueller Abschnitt, Priorität und aktuelle Planabweichung jedes Zugs
- Abfahrtsreferenz und nächster Fahrplanhalt mit Soll-Zeiten und Mindesthalt
- Seed, feste und gezogene Anfangsabweichung sowie die Fahrzeit-Streubreite
- aktive Fahrstraßen, aktuell befahrener Abschnitt und verbleibende Ticks
Die strukturierte Diagnose-Observation ist bewusst vollständig beobachtbar und enthält den Replay-Seed. Der feste ML-Featurevektor übernimmt den rohen Seed nicht, wohl aber bereits realisierte Störungen. Für ein späteres partiell beobachtbares Trainingsmodell müssen Policy-Kontext und Diagnosemetadaten als eigene Schnittstellen getrennt werden; das bloße Entfernen des Seed-Felds würde nicht genügen.
src/feature-encoder.js bildet diese Observation auf einen pro Netz festen
numerischen Vektor ab. Abschnitts-, Weichen-, Signal-, Fahrstraßen- und Zugreihenfolge
werden deterministisch aus der Netzbeschreibung übernommen. Das Action-Schema
besteht aus Anforderungs- und Rücknahmeaktionen für alle Fahrstraßen des
geladenen Netzes plus wait. Die Action Mask
hat dieselbe Reihenfolge und enthält 1 für aktuell angebotene Aktionen.
Der Encoder ist absichtlich außerhalb von Prolog. Dadurch kann sich die ML-Repräsentation ändern, ohne die Simulationssemantik umzubauen.
Die Ortssemantik hängt von der Granularität der Netzbeschreibung ab. In Netzen
mit trackSegments steht ein abgefahrener Zug sofort auf dem ersten Segment;
der geräumte Startabschnitt ist frei. In segmentlosen Netzen besteht die
Traversal aus der Abschnittsfolge einschließlich Start und Ziel, sodass der
Startabschnitt während der ersten Fahrt-Ticks noch als belegt erscheinen kann.
Für netzübergreifendes Training sollte dieser Unterschied im Feature-Design
berücksichtigt werden; die Ressourcensperrung bleibt in beiden Varianten
gleich sicher.
balanced_v1 berücksichtigt:
- positiv: Ankunft, gewichtet nach Zugpriorität
- negativ: Zunahme der Planabweichung, gewichtet nach Zugpriorität
- positiv: aufgeholte Planabweichung, ebenfalls nach Priorität gewichtet
- negativ: Nutzung eines deklarierten Ersatzbahnsteigs (
−4 × Priorität) - negativ: Weichenbewegungen
- negativ: abgelehnte Aktionen
- negativ: Rücknahme einer eingestellten Fahrstraße (
−0,5) - stark negativ: Deadlock
Die Terminalstrafe entsteht ausschließlich beim Übergang in den Deadlock. Weitere idempotente Aufrufe auf demselben Terminalzustand liefern keine erneute Strafe.
throughput_v1 gewichtet Ankünfte stärker und Planabweichungen schwächer;
eine Bahnsteigabweichung kostet dort −2 × Priorität, jede Rücknahme −0,5.
Dadurch bleiben nullzeitige Zyklen aus
Einstellen und Zurücknehmen möglich, sind für einen Agenten aber nicht mehr
folgenlos.
Transition und Reward sind getrennt. Für ein neues Reward-Modell muss die Fahrphysik nicht verändert werden.
In der Netz-JSON einen weiteren Eintrag unter routes ergänzen. Quelle, Ziel,
Weichenanforderungen, Ressourcen und Layout werden vor dem Start validiert:
npm testEine durchgehende Start-Ziel-Fahrstraße wird unter routePlans aus mindestens
zwei verbundenen Routen aufgebaut. Der Compiler vereinigt Fahrzeit,
Weichenanforderungen und Ressourcen und lehnt nicht verbundene oder
widersprüchliche Pläne vor dem Serverstart ab. Zusammengesetzte Traversals
dürfen kein Segment und keinen Zwischenabschnitt mehrfach befahren; Schleifen
wie A→B→A werden ebenfalls bei der Validierung zurückgewiesen.
Jede Teil- und Durchfahrstraße muss genau einem Eintrag unter signals
zugeordnet sein. Dadurch kann keine neue Fahrstraße unbemerkt ohne zugehörige
Hauptsignalanzeige entstehen.
Ein Signal deklariert Kennung, Beschriftung, Funktion (entry, exit,
intermediate oder block), den geschützten Quellabschnitt, seine Richtung im
Gleisbild, eine absolute SVG-Position und die kontrollierten routeIds. Der
Validator prüft unbekannte oder doppelt zugeordnete Fahrstraßen, falsche
Quellabschnitte und widersprüchliche Richtungen.
Die Grundanzeige ist Halt. Nach dem erfolgreichen Einstellen einer
zugeordneten Fahrstraße zeigt das Signal Fahrt. Rücknahme und Abfahrt lösen
den Haltfall aus. Der Signalzustand wird deterministisch aus der gesicherten
Fahrstraße im Prolog-Zustand ready abgeleitet und als Observation sowie
Feature (0 = Halt, 1 = Fahrt) ausgegeben. Der Host-Adapter projiziert ihn
ohne zusätzlichen Prolog-Suchlauf; er ist keine zweite Sicherungslogik. Das entspricht
der Signalabhängigkeit als Modellrichtung, abstrahiert aber reale Signalbilder,
Geschwindigkeitsbegriffe, Vorsignale und den Haltfall nach vollständiger
Vorbeifahrt des Zuges.
Fachliche Referenz dieser Abstraktion sind § 14 EBO – insbesondere Grundstellung Zughalt und Signalabhängigkeit – sowie das DB-InfraGO-Signalbuch Ril 301, nach dem ein Hauptsignal anzeigt, ob der anschließende Gleisabschnitt befahren werden darf. Diese Quellen machen aus dem Prototype ausdrücklich kein sicherheitsgerichtetes Stellwerk.
Unter services die Zugleistung mit Priorität und mindestens zwei calls
ergänzen. Ein Szenario referenziert diese Leistung unter services, setzt
initialDelayTicks und deklariert sein disturbances-Profil. service/3,
scenario_service/3, scenario_disturbance/3, Initialzustand und
Zugslots im Feature-Schema entstehen automatisch; die Engine muss nicht
geändert werden.
Ein Zwischenhalt kann mit alternativeSections betriebliche Ersatzbahnsteige
freigeben. Das geplante section bleibt das Sollgleis; eine Fahrt zu einer
Alternative ist in Action-Maske und Sicherungslogik zulässig, erzeugt bei der
Ankunft aber ein eigenes platform_deviation-Ereignis und einen separaten
Reward-Anteil. Validator und Compiler verlangen Bahnsteigabschnitte sowie eine
durchgängige Route vom vorherigen und zum folgenden Fahrplanhalt:
{
"section": "gleis_8",
"alternativeSections": ["gleis_7", "gleis_9"],
"arrivalTick": 7,
"departureTick": 9,
"minimumDwellTicks": 2
}reward_model(Name).ergänzen.- Eine neue
reward/8-Klausel implementieren. - Beim Erzeugen der Umgebung
rewardModel: "Name"setzen.
Die sinnvolle Grenze bleibt die Fahrstraßenebene:
Agent -> request_route(FS_17)
-> Stellwerk akzeptiert, wartet oder lehnt ab
-> Adapter liefert neue Observation und Events
Der reale Adapter sollte dieselbe fachliche Form wie RailEnvironment anbieten. Er muss intern nicht Prolog verwenden. Das Modell sieht dadurch im Simulator und später im Shadow-/Advisory-Mode dieselben Aktionstypen.
- keine Sicherheitslogik eines realen Stellwerks
- keine direkte Weichen- oder Signalsteuerung durch den Agenten
- Hauptsignale zeigen nur die vereinfachten Aspekte Halt/Fahrt; keine Unterscheidung von Hp 1/Hp 2, keine Zusatz- oder Vorsignale und keine eigenständige Sicherungslogik
- keine vorzeitige Weichenauflösung; Weichen bleiben bis zum Fahrstraßenende gesperrt
- keine Datenbank und kein Event Store
- keine freie Generierung neuer Wege aus einzelnen Gleis- oder Signalelementen
- keine echte Zugdynamik oder Zuglängen
- noch keine Weichenstörungen, Gleissperrungen oder liegengebliebenen Züge
- endliche Fahrplanszenarien statt eines unbegrenzt laufenden Betriebstags
- keine Netzwerk- oder Hardwareintegration
- kein Gymnasium-Wrapper
- Teilfahrstraßen und zulässige Durchfahrpläne bleiben bewusst deklarativ
Das sind keine versteckten TODOs im Kern. Sie sind bewusst weggelassen, damit die erste Version klein bleibt und trotzdem bereits echte Episoden, Action Masks, Rewards, Replay-Daten und ML-Inputs erzeugt.
npm testFür längere Policy-Probes über alle Netze und mehrere Seeds gibt es zusätzlich die Szenario-Batterie. Ein optionaler Dateifilter und eine kommaseparierte Seed-Liste begrenzen den Lauf:
npm run battery -- nuernberg 0,3,7,11Der versionierte Pre-Push-Hook führt denselben vollständigen Testlauf vor jedem Push aus. Er wird pro Clone einmal aktiviert:
git config core.hooksPath .githooksEin fehlgeschlagener Test bricht den Push ab.
Abgedeckt sind unter anderem:
- identisches Replay für denselben Seed und unterschiedliche Realisierungen für verschiedene Seeds
- Seed-basierte Anfangsabweichungen und streuende Fahrzeiten
- Action Masking
- Konflikte und konkrete Ablehnungsgründe
- parallele konfliktfreie Fahrten
- ungültige Aktionen
- identische Transitionen bei verschiedenen Reward-Modellen
- vollständige Episoden für alle Szenarien aller deklarierten Netze
- geplanter Netzeinbruch, Haltzwang und Mindesthaltezeiten
- Invarianten unter zufälligen Aktionsfolgen
- stabile Feature-Dimensionen
- parsebarer JSONL-Export