Skip to content

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Rail ML Prototype

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.

Was bereits funktioniert

  • 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 und wait
  • 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

Voraussetzungen

  • 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.

Schnellstart

npm ci
npm test
npm run demo

Ein anderes Szenario und ein anderer Lauf-Seed:

node src/demo.js parallel 7
node src/demo.js congested 42

Der 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.

Weboberfläche

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 web

Danach 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 web

Beispielsweise direkt mit dem größeren Regionalbahnhof starten:

RAIL_NETWORK=networks/regional.json PORT=43117 npm run web

Oder mit dem komplexen Bamberger Knoten:

RAIL_NETWORK=networks/bamberg.json PORT=43117 npm run web

Das größere Nürnberg-Korridormodell lässt sich ebenso direkt öffnen:

RAIL_NETWORK=networks/nuernberg.json PORT=43117 npm run web

Eine 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.

Deklarative Netzbeschreibung

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.

Reale Fahrpläne aus GTFS

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:timetable

GTFS 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 trackSegments zwischen 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 sectionSequence fü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.pl

Die 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.

Größeres Beispielnetz

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 smoke

Trajektorien erzeugen

npm run generate -- \
    --network networks/demo.json \
    --episodes 100 \
    --scenario all \
    --seed 1000 \
    --output data/trajectories.jsonl \
    --policy priority \
    --max-steps 100 \
    --reward-model balanced_v1

Der 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.

ML-Pipeline testen

python3 train_baseline.py \
    data/trajectories.jsonl \
    --model-output data/priority-policy-baseline.joblib

Die 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.

JavaScript-API

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.

Prolog-Oberfläche

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.

Topologie und Fakten

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.

Observations und Features

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.

Reward-Modelle

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.

Erweiterungspunkte

Neue Fahrstraße

In der Netz-JSON einen weiteren Eintrag unter routes ergänzen. Quelle, Ziel, Weichenanforderungen, Ressourcen und Layout werden vor dem Start validiert:

npm test

Eine 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.

Neues Signal

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.

Neue Zugleistung und neues Szenario

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
}

Neues Reward-Modell

  1. reward_model(Name). ergänzen.
  2. Eine neue reward/8-Klausel implementieren.
  3. Beim Erzeugen der Umgebung rewardModel: "Name" setzen.

Späterer Adapter für ein echtes Stellwerk

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.

Bewusste Grenzen der ersten Version

  • 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.

Tests

npm test

Fü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,11

Der versionierte Pre-Push-Hook führt denselben vollständigen Testlauf vor jedem Push aus. Er wird pro Clone einmal aktiviert:

git config core.hooksPath .githooks

Ein 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

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages