Spaces:
Running on Zero
Running on Zero
RailSimEnv v1: OpenEnv server, eval explorer, 3D replay and train graph
Browse files- README.md +89 -5
- app.py +52 -0
- core/rail_core/__init__.py +14 -0
- core/rail_core/baselines.py +263 -0
- core/rail_core/build.py +243 -0
- core/rail_core/check.py +364 -0
- core/rail_core/data.py +207 -0
- core/rail_core/generate.py +226 -0
- core/rail_core/hardness.py +59 -0
- core/rail_core/model.py +307 -0
- core/rail_core/network.py +80 -0
- core/rail_core/ops.py +160 -0
- core/rail_core/pack.py +100 -0
- core/rail_core/prompts.py +192 -0
- core/rail_core/reference.py +131 -0
- core/rail_core/replay.py +109 -0
- core/rail_core/report.py +72 -0
- core/rail_core/reward.py +68 -0
- core/rail_core/select.py +189 -0
- core/rail_core/solve.py +608 -0
- openenv.yaml +38 -0
- openenv/rail_openenv/__init__.py +0 -0
- openenv/rail_openenv/environment.py +266 -0
- openenv/rail_openenv/gradio_ui.py +37 -0
- openenv/rail_openenv/rubric.py +99 -0
- openenv/rail_openenv/server.py +338 -0
- openenv/rail_openenv/session.py +115 -0
- openenv/rail_openenv/web/explorer/index.html +313 -0
- openenv/rail_openenv/web/index.html +592 -0
- openenv/rail_openenv/web/twin/SOURCES.md +45 -0
- openenv/rail_openenv/web/twin/app.js +286 -0
- openenv/rail_openenv/web/twin/geo.js +138 -0
- openenv/rail_openenv/web/twin/graph.js +313 -0
- openenv/rail_openenv/web/twin/index.html +80 -0
- openenv/rail_openenv/web/twin/motion.js +398 -0
- openenv/rail_openenv/web/twin/scene.js +765 -0
- openenv/rail_openenv/web/twin/style.css +161 -0
- openenv/rail_openenv/web/twin/twin.json.gz +3 -0
- openenv/rail_openenv/web/twin/world.js +514 -0
- requirements.txt +2 -0
- runs/2026-10-07-sonnet-5-5-eval45/index.json +0 -0
- runs/2026-10-08-deepseek-v4-1-flash-eval45/index.json +0 -0
- runs/2026-10-08-glm-5-3-flash-eval45/index.json +4831 -0
- runs/2026-10-08-qwen3-8-2-4t-eval45/index.json +0 -0
- runs/calib-sonnet-5-5/index.json +1900 -0
- tasks/rail-v1-eval/manifest.json +92 -0
- tasks/rail-v1-eval/selection.json +116 -0
- tasks/rail-v1-eval/tasks.jsonl +0 -0
- tasks/rail-v1-train/manifest.json +106 -0
- tasks/rail-v1-train/tasks.jsonl +0 -0
README.md
CHANGED
|
@@ -1,13 +1,97 @@
|
|
| 1 |
---
|
| 2 |
-
title: RailSimEnv
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: blue
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.29.1
|
| 8 |
-
python_version:
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: RailSimEnv v1
|
| 3 |
+
emoji: 🚆
|
| 4 |
colorFrom: blue
|
| 5 |
+
colorTo: gray
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.29.1
|
| 8 |
+
python_version: "3.12"
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
+
short_description: Dispatch trains on a real single-track Dutch line
|
| 12 |
+
tags: [openenv, simulation, rl-environment, railway, scheduling, operations-research]
|
| 13 |
---
|
| 14 |
|
| 15 |
+
# RailSimEnv v1 · OpenEnv
|
| 16 |
+
|
| 17 |
+
Dispatch Arriva's trains on the single-track Groningen – Leeuwarden line (Netherlands) after a real 2025 disruption:
|
| 18 |
+
a broken-down train, a collision at a level crossing, a signal or points failure, on its real day and time, with every
|
| 19 |
+
train where it really was. Hold, short-turn and cancel trains so opposing trains cross safely. One graded submit per
|
| 20 |
+
episode against the proven CP-SAT optimum. Serves the rail-v1 packs: 45 eval tasks chosen for hardness and 159 train
|
| 21 |
+
tasks on other days.
|
| 22 |
+
|
| 23 |
+
| page | what |
|
| 24 |
+
|---|---|
|
| 25 |
+
| `/web` | OpenEnv's web UI. **Explore**: the eval explorer, the 3D replay and the train graph. **Playground**: reset, step and the MCP tools by hand |
|
| 26 |
+
| `/explorer/` | the leaderboard and every model's plan on every eval task; pick one to watch it |
|
| 27 |
+
| `/twin/` | the 3D replay: any plan on the line's real track from OpenStreetMap, signals, level crossings, closures, the train graph and the cost as it accrues |
|
| 28 |
+
| `/viewer/` | the train graph (time across, distance down) of any plan, with the plan's holds, short-turns and rule breaks |
|
| 29 |
+
| `/reset`, `/step`, `/state`, `/ws`, `/mcp` | OpenEnv |
|
| 30 |
+
| `/rail_ops/...` | OpenEnv Task API (`splits`, `num_tasks`, `task`, `task_range`) |
|
| 31 |
+
| `/api/...` | read-only JSON for the pages |
|
| 32 |
+
|
| 33 |
+
## Tools
|
| 34 |
+
|
| 35 |
+
| tool | returns |
|
| 36 |
+
|---|---|
|
| 37 |
+
| `get_situation()` | the line, the closures, every train's timetable and position, the train sets and their chains of runs |
|
| 38 |
+
| `check_plan(plan)` | simulates a draft: every train's times, delay and lost stops, every rule broken, the cost; 10 per episode |
|
| 39 |
+
| `submit_plan(plan)` | ends the episode with one grade |
|
| 40 |
+
|
| 41 |
+
`plan` lists only the trains you change: `[{"train": "37413", "to": "HDG"}, {"train": "37324", "from": "HDG"},
|
| 42 |
+
{"train": "37419", "hold": {"GK": "08:36"}}, {"train": "37330", "cancel": true}]`. Trains left alone run as early as
|
| 43 |
+
the physics allow, never ahead of the timetable. `reset(task_id=...)`, `reset(split=..., index=...)` or
|
| 44 |
+
`reset(seed=...)`.
|
| 45 |
+
|
| 46 |
+
**Rules:** no two opposing trains in a single-track section at once; 3-minute headways for trains going the same way;
|
| 47 |
+
never more trains at a station than tracks; no train in a closed section; runs start where their train set is, and
|
| 48 |
+
every set ends the window where its next run leaves; no stop more than 2 hours late.
|
| 49 |
+
|
| 50 |
+
**Cost**, in stop-minutes: every minute of delay at every stop served, 30 per stop not served, and lateness handed on
|
| 51 |
+
to the next window.
|
| 52 |
+
|
| 53 |
+
## Reward
|
| 54 |
+
|
| 55 |
+
Graded once, on `submit_plan`, by `RailPlanRubric` (every other action scores 0.0). A plan that breaks any rule scores
|
| 56 |
+
0.2 × the share of runs it leaves clean. A legal plan scores 0.2 + 0.8 × exp(−gap / 0.5), with
|
| 57 |
+
gap = (cost − optimum) / (optimum − floor + 100), so the proven CP-SAT optimum scores 1.0. No submission scores 0. The
|
| 58 |
+
children `runs_clean` and `cost_quality` are reported in the final observation's `metadata.rubric`.
|
| 59 |
+
|
| 60 |
+
## Results on the 45 eval tasks
|
| 61 |
+
|
| 62 |
+
One rollout per model, the same rules for every model (12 turns, 64k output tokens per turn):
|
| 63 |
+
|
| 64 |
+
| model | mean reward (95% CI) | legal plans | at the optimum |
|
| 65 |
+
|---|---|---:|---:|
|
| 66 |
+
| Qwen3.8-2.4T-A95B | **0.556** (0.452-0.665) | 39 | 12 |
|
| 67 |
+
| DeepSeek-V4.1-Flash | **0.534** (0.427-0.643) | 33 | 12 |
|
| 68 |
+
| Claude Sonnet 5.5 | **0.467** (0.384-0.553) | 45 | 4 |
|
| 69 |
+
| GLM-5.3-Flash | **0.360** (0.251-0.477) | 24 | 6 |
|
| 70 |
+
| the real dispatchers (real tier, scored as if legal) | 0.502 | | |
|
| 71 |
+
| greedy dispatcher | 0.180 | | |
|
| 72 |
+
| change nothing | 0.105 | 0 | 0 |
|
| 73 |
+
|
| 74 |
+
Every plan is in the eval explorer, and replays in 3D exactly as it was graded.
|
| 75 |
+
|
| 76 |
+
## Connect an agent
|
| 77 |
+
|
| 78 |
+
```python
|
| 79 |
+
from openenv.core.env_server.mcp_types import CallToolAction
|
| 80 |
+
from openenv.core.mcp_client import MCPToolClient
|
| 81 |
+
|
| 82 |
+
env = MCPToolClient("https://bphigh-railsimenv.hf.space").sync()
|
| 83 |
+
obs = env.reset(split="eval", index=0)
|
| 84 |
+
rules = obs.observation.metadata["instructions"] # the system prompt
|
| 85 |
+
situation = env.step(CallToolAction(tool_name="get_situation", arguments={}))
|
| 86 |
+
plan = [{"train": "37413", "to": "HDG"}, {"train": "37324", "from": "HDG"}]
|
| 87 |
+
print(env.step(CallToolAction(tool_name="check_plan", arguments={"plan": plan})).observation)
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
## Data and licences
|
| 91 |
+
|
| 92 |
+
| data | source | licence |
|
| 93 |
+
|---|---|---|
|
| 94 |
+
| trains, timetables, delays, cancellations, platforms; disruptions | [Rijden de Treinen](https://www.rijdendetreinen.nl/en/open-data) open data | CC BY 4.0 ("Rijden de Treinen") |
|
| 95 |
+
| track layout; the 3D replay's map (track, platforms, buildings, land cover, water, roads) | © OpenStreetMap contributors | ODbL 1.0 |
|
| 96 |
+
|
| 97 |
+
The rolling-stock circulation, the disruption effects by cause and the cost weights are modelling choices.
|
app.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""RailSimEnv v1 on a Hugging Face Space: the OpenEnv server (reset, step, state, the MCP tools, the Task API and the
|
| 2 |
+
web UI at /web), the eval explorer (/explorer/), the 3D replay (/twin/) and the train graph (/viewer/), on one port.
|
| 3 |
+
|
| 4 |
+
The Space runs on ZeroGPU hardware but never asks for a GPU: the environment is plain Python and the grader re-simulates
|
| 5 |
+
a plan in milliseconds. ZeroGPU wants one decorated function, so `_noop` exists and is never called.
|
| 6 |
+
|
| 7 |
+
python app.py # http://127.0.0.1:7860/web/ (the Space's page), /explorer/, /twin/, /viewer/
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import os
|
| 13 |
+
import sys
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
|
| 16 |
+
ROOT = Path(__file__).resolve().parent
|
| 17 |
+
sys.path[:0] = [str(ROOT / "core"), str(ROOT / "openenv")]
|
| 18 |
+
os.environ.setdefault("RAIL_TASKS_DIR", f"{ROOT / 'tasks' / 'rail-v1-eval'}:{ROOT / 'tasks' / 'rail-v1-train'}")
|
| 19 |
+
os.environ.setdefault("RAIL_RUNS_DIR", str(ROOT / "runs"))
|
| 20 |
+
os.environ.setdefault("ENABLE_WEB_INTERFACE", "true")
|
| 21 |
+
|
| 22 |
+
# The web UI shows a README: this card, without its front matter.
|
| 23 |
+
card = (ROOT / "README.md").read_text(encoding="utf-8")
|
| 24 |
+
if card.startswith("---"):
|
| 25 |
+
card = card.split("---", 2)[2].lstrip()
|
| 26 |
+
readme = Path(os.environ.get("TMPDIR", "/tmp")) / "railsim_readme.md"
|
| 27 |
+
readme.write_text(card, encoding="utf-8")
|
| 28 |
+
os.environ.setdefault("ENV_README_PATH", str(readme))
|
| 29 |
+
|
| 30 |
+
try:
|
| 31 |
+
import spaces
|
| 32 |
+
except ImportError: # running elsewhere
|
| 33 |
+
spaces = None
|
| 34 |
+
|
| 35 |
+
if spaces is not None:
|
| 36 |
+
@spaces.GPU(duration=1)
|
| 37 |
+
def _noop(): # ZeroGPU requires one decorated function; nothing calls it
|
| 38 |
+
pass
|
| 39 |
+
|
| 40 |
+
import uvicorn # noqa: E402
|
| 41 |
+
|
| 42 |
+
from rail_openenv.server import create_server # noqa: E402
|
| 43 |
+
|
| 44 |
+
app = create_server()
|
| 45 |
+
|
| 46 |
+
if __name__ == "__main__":
|
| 47 |
+
if spaces is not None:
|
| 48 |
+
from spaces import zero
|
| 49 |
+
|
| 50 |
+
zero.startup()
|
| 51 |
+
uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", "7860")), ws_ping_interval=60,
|
| 52 |
+
ws_ping_timeout=600)
|
core/rail_core/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Rail dispatch on the single-track Groningen - Leeuwarden line: tasks from Arriva's real 2025 days, the physics and
|
| 2 |
+
checker, CP-SAT references, a reward."""
|
| 3 |
+
|
| 4 |
+
from .check import PlanResult, evaluate, simulate
|
| 5 |
+
from .model import (RULES, Closure, Order, Plan, PlanError, Run, Task, Unit, hhmm, parse_plan, parse_time,
|
| 6 |
+
plan_from_list, plan_to_list)
|
| 7 |
+
from .pack import TaskPack, load_pack
|
| 8 |
+
from .prompts import PROMPT_VERSION, brief, rules, situation
|
| 9 |
+
from .report import plan_report
|
| 10 |
+
from .reward import Grade, grade, score
|
| 11 |
+
|
| 12 |
+
__all__ = ["Closure", "Grade", "Order", "PROMPT_VERSION", "Plan", "PlanError", "PlanResult", "RULES", "Run", "Task",
|
| 13 |
+
"TaskPack", "Unit", "brief", "evaluate", "grade", "hhmm", "load_pack", "parse_plan", "parse_time",
|
| 14 |
+
"plan_from_list", "plan_report", "plan_to_list", "rules", "score", "simulate", "situation"]
|
core/rail_core/baselines.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Cheap dispatching policies: the lazy bounds a task must stay hard against, and the solver's starting point.
|
| 2 |
+
|
| 3 |
+
naive change nothing: every train runs as scheduled and as early as it can (usually breaks the rules)
|
| 4 |
+
greedy first come, first served: trains are planned one at a time in the order they are ready to leave; each
|
| 5 |
+
takes the earliest times that clear every train already planned (holding at its scheduled stops), and
|
| 6 |
+
a train whose path is closed for more than `turn_after` minutes is short-turned at the last turn point
|
| 7 |
+
before the closure, its train starting back from there. This is how a dispatcher works by rule of
|
| 8 |
+
thumb; it never cancels and never makes a train wait to let a later one through.
|
| 9 |
+
greedy_fast the same, but fast trains (Sneltrein) are planned before stopping trains that are ready at the
|
| 10 |
+
same time (within 10 minutes): priority to the fast service
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import heapq
|
| 16 |
+
from collections import defaultdict
|
| 17 |
+
|
| 18 |
+
from .check import evaluate, network, run_dir
|
| 19 |
+
from .model import Order, Plan, Run, Task
|
| 20 |
+
|
| 21 |
+
MAXWAIT = 240
|
| 22 |
+
STEP_BUDGET = 60_000 # departure times tried per run before the greedy gives up on it
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class _Book:
|
| 26 |
+
"""What is already planned: section occupancies and station presences."""
|
| 27 |
+
|
| 28 |
+
def __init__(self, task: Task):
|
| 29 |
+
self.task = task
|
| 30 |
+
self.net = network(task)
|
| 31 |
+
self.sec: dict[int, list] = defaultdict(list) # (dir, enter, exit)
|
| 32 |
+
self.pres: dict[str, list] = defaultdict(list) # (dir, t0, t1)
|
| 33 |
+
self.hf, self.ho = task.rule("h_follow"), task.rule("h_opp")
|
| 34 |
+
self.closed = [c for c in task.closures if c.kind == "closed"]
|
| 35 |
+
self.tracks_out = [c for c in task.closures if c.kind == "track"]
|
| 36 |
+
self.slow = [c for c in task.closures if c.kind == "slow"]
|
| 37 |
+
|
| 38 |
+
def section_ok(self, sec: int, d: int, t0: int, t1: int, history: bool = False) -> bool:
|
| 39 |
+
s = self.net.sections[sec]
|
| 40 |
+
for dd, a, b in self.sec[sec]:
|
| 41 |
+
if dd == d:
|
| 42 |
+
if (t0 >= a and (t0 - a < self.hf or t1 - b < self.hf)) or \
|
| 43 |
+
(t0 < a and (a - t0 < self.hf or b - t1 < self.hf)):
|
| 44 |
+
return False
|
| 45 |
+
elif s.single and not (b + self.ho <= t0 or t1 + self.ho <= a):
|
| 46 |
+
return False
|
| 47 |
+
if not history:
|
| 48 |
+
for c in self.closed:
|
| 49 |
+
lo, hi = sorted((self.net.index[c.where[0]], self.net.index[c.where[-1]]))
|
| 50 |
+
if lo <= sec < hi and t0 < c.end and t1 > c.start:
|
| 51 |
+
return False
|
| 52 |
+
return True
|
| 53 |
+
|
| 54 |
+
def station_ok(self, code: str, d: int, t0: int, t1: int) -> bool:
|
| 55 |
+
p = self.net.by_code[code]
|
| 56 |
+
if p.tracks is None or p.kind == "junction":
|
| 57 |
+
return True
|
| 58 |
+
others = [(dd, a, b) for dd, a, b in self.pres[code] if not (b < t0 or a > t1)]
|
| 59 |
+
if p.directional:
|
| 60 |
+
return not any(dd in (d, 0) for dd, _, _ in others)
|
| 61 |
+
# peak count over [t0, t1] including this train
|
| 62 |
+
times = sorted({t0} | {a for _, a, _ in others if t0 <= a <= t1} |
|
| 63 |
+
{c.start for c in self.tracks_out if c.where[0] == code and t0 <= c.start <= t1})
|
| 64 |
+
for t in times:
|
| 65 |
+
n = 1 + sum(1 for _, a, b in others if a <= t <= b)
|
| 66 |
+
cap = p.tracks - sum(1 for c in self.tracks_out if c.where[0] == code and c.start <= t < c.end)
|
| 67 |
+
if n > cap:
|
| 68 |
+
return False
|
| 69 |
+
return True
|
| 70 |
+
|
| 71 |
+
def slow_extra(self, sec: int, t: int) -> int:
|
| 72 |
+
x = 0
|
| 73 |
+
for c in self.slow:
|
| 74 |
+
lo, hi = sorted((self.net.index[c.where[0]], self.net.index[c.where[-1]]))
|
| 75 |
+
if lo <= sec < hi and c.start <= t < c.end:
|
| 76 |
+
x = max(x, c.extra)
|
| 77 |
+
return x
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _closure_wait(book: _Book, r: Run, i: int, t: int) -> int:
|
| 81 |
+
"""Minutes a train entering route[i] -> route[i+1] at t would wait for a closure."""
|
| 82 |
+
sec = book.net.section(r.route[i], r.route[i + 1])
|
| 83 |
+
w = 0
|
| 84 |
+
for c in book.closed:
|
| 85 |
+
lo, hi = sorted((book.net.index[c.where[0]], book.net.index[c.where[-1]]))
|
| 86 |
+
if lo <= sec < hi and c.start <= t + r.seg[i] and t < c.end:
|
| 87 |
+
w = max(w, c.end - t)
|
| 88 |
+
return w
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def _path(book: _Book, r: Run, first: int, last: int, a0: int | None, ready: int, wait_from: int | None):
|
| 92 |
+
"""Earliest conflict-free times for one run over route[first..last] given what is booked: (arr, dep, stopped)
|
| 93 |
+
or None. a0: the fixed arrival at `first` of a run already moving (else the run starts at `first`, where its
|
| 94 |
+
train has stood since `wait_from` if that is a station with few tracks)."""
|
| 95 |
+
net, task = book.net, book.task
|
| 96 |
+
L = task.rule("stop_loss")
|
| 97 |
+
d = run_dir(net, r)
|
| 98 |
+
fail: set = set()
|
| 99 |
+
steps = [0]
|
| 100 |
+
arr: dict[int, int] = {}
|
| 101 |
+
dep: dict[int, int] = {}
|
| 102 |
+
stopped: dict[int, bool] = {}
|
| 103 |
+
|
| 104 |
+
def go(i: int, a: int) -> bool:
|
| 105 |
+
if i == last:
|
| 106 |
+
arr[i], stopped[i] = a, True
|
| 107 |
+
return True
|
| 108 |
+
if (i, a) in fail:
|
| 109 |
+
return False
|
| 110 |
+
code = r.route[i]
|
| 111 |
+
p = net.by_code[code]
|
| 112 |
+
starting = i == first and a0 is None
|
| 113 |
+
if starting:
|
| 114 |
+
d0 = max(ready, r.dep[i])
|
| 115 |
+
elif r.stops[i]:
|
| 116 |
+
d0 = max(a, r.dep[i])
|
| 117 |
+
else:
|
| 118 |
+
d0 = a
|
| 119 |
+
if i == first and a0 is not None:
|
| 120 |
+
d0 = max(d0, task.start)
|
| 121 |
+
sec = net.section(code, r.route[i + 1])
|
| 122 |
+
for t in range(d0, d0 + MAXWAIT):
|
| 123 |
+
steps[0] += 1
|
| 124 |
+
if steps[0] > STEP_BUDGET:
|
| 125 |
+
return False
|
| 126 |
+
if starting:
|
| 127 |
+
a_here, acc, halt, t0 = None, (0 if r.stops[i] else L), True, wait_from
|
| 128 |
+
elif r.stops[i] or (i == first and a0 is not None and t > a and False):
|
| 129 |
+
a_here, acc, halt, t0 = a, 0, True, a
|
| 130 |
+
elif t == a:
|
| 131 |
+
a_here, acc, halt, t0 = a, 0, False, a
|
| 132 |
+
else:
|
| 133 |
+
break # greedy only waits where the train has a scheduled stop
|
| 134 |
+
if t0 is not None and not book.station_ok(code, d, t0, t):
|
| 135 |
+
if halt:
|
| 136 |
+
break # a longer wait here only makes it worse
|
| 137 |
+
continue
|
| 138 |
+
nxt = t + r.seg[i] + acc + book.slow_extra(sec, t)
|
| 139 |
+
if r.arr[i + 1] is not None:
|
| 140 |
+
nxt = max(nxt, r.arr[i + 1])
|
| 141 |
+
if i + 1 == last and not r.stops[last]:
|
| 142 |
+
nxt += L
|
| 143 |
+
if not book.section_ok(sec, d, t, nxt):
|
| 144 |
+
continue
|
| 145 |
+
if a_here is not None:
|
| 146 |
+
arr[i] = a_here
|
| 147 |
+
dep[i], stopped[i] = t, halt
|
| 148 |
+
if go(i + 1, nxt):
|
| 149 |
+
return True
|
| 150 |
+
fail.add((i, a))
|
| 151 |
+
return False
|
| 152 |
+
|
| 153 |
+
if not go(first, a0 if a0 is not None else 0):
|
| 154 |
+
return None
|
| 155 |
+
return arr, dep, stopped
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def greedy(task: Task, fast_first: bool = False, turn_after: int = 20) -> Plan:
|
| 159 |
+
book = _Book(task)
|
| 160 |
+
net = book.net
|
| 161 |
+
runs = task.run_map
|
| 162 |
+
turn = task.rule("turn_min")
|
| 163 |
+
plan: Plan = {}
|
| 164 |
+
for r in task.runs: # trains between points when the task starts
|
| 165 |
+
if r.started and r.fixed_from is not None:
|
| 166 |
+
k = r.fixed_to
|
| 167 |
+
book.sec[net.section(r.route[k - 1], r.route[k])].append((run_dir(net, r), r.fixed_from, r.fixed_arr))
|
| 168 |
+
state = {}
|
| 169 |
+
for u in task.units:
|
| 170 |
+
finite = u.at is not None and net.by_code[u.at].tracks is not None
|
| 171 |
+
state[u.id] = {"k": 0, "ready": u.ready, "wait_from": task.start if finite else None, "start_at": None,
|
| 172 |
+
"park": None}
|
| 173 |
+
if finite:
|
| 174 |
+
stay = (0, task.start, task.end + 1440)
|
| 175 |
+
book.pres[u.at].append(stay)
|
| 176 |
+
state[u.id]["park"] = (u.at, stay)
|
| 177 |
+
heap = []
|
| 178 |
+
units = {u.id: u for u in task.units}
|
| 179 |
+
|
| 180 |
+
def push(u):
|
| 181 |
+
s = state[u.id]
|
| 182 |
+
if s["k"] >= len(u.runs):
|
| 183 |
+
return
|
| 184 |
+
r = runs[u.runs[s["k"]]]
|
| 185 |
+
t = r.fixed_arr if r.started else max(s["ready"] or 0, r.dep[0])
|
| 186 |
+
pri = (t // 10, 0 if r.kind == "fast" else 1, t) if fast_first else (t, 0, 0)
|
| 187 |
+
heapq.heappush(heap, (pri, u.id))
|
| 188 |
+
|
| 189 |
+
for u in task.units:
|
| 190 |
+
push(u)
|
| 191 |
+
while heap:
|
| 192 |
+
_, uid = heapq.heappop(heap)
|
| 193 |
+
u = units[uid]
|
| 194 |
+
s = state[uid]
|
| 195 |
+
r = runs[u.runs[s["k"]]]
|
| 196 |
+
if s["park"]: # its provisional stay is replaced by the real one below
|
| 197 |
+
code, stay = s["park"]
|
| 198 |
+
book.pres[code].remove(stay)
|
| 199 |
+
s["park"] = None
|
| 200 |
+
has_next = s["k"] + 1 < len(u.runs)
|
| 201 |
+
first = r.fixed_to if r.started else (r.idx(s["start_at"]) if s["start_at"] else 0)
|
| 202 |
+
last = len(r.route) - 1
|
| 203 |
+
if has_next: # short-turn before a long closure, if its train has a run to come back on
|
| 204 |
+
nxt = runs[u.runs[s["k"] + 1]]
|
| 205 |
+
t = r.fixed_arr if r.started else max(s["ready"] or 0, r.dep[first])
|
| 206 |
+
for i in range(first, last):
|
| 207 |
+
if i > first and r.stops[i]:
|
| 208 |
+
t = max(t, r.dep[i])
|
| 209 |
+
if _closure_wait(book, r, i, t) > turn_after:
|
| 210 |
+
lo = first if r.started else first + 1
|
| 211 |
+
# only where the next run can start (some runs work part of the line, e.g. Groningen - Zuidhorn)
|
| 212 |
+
cands = [j for j in range(lo, i + 1) if net.by_code[r.route[j]].turn
|
| 213 |
+
and r.route[j] in nxt.route[:-1]]
|
| 214 |
+
if cands:
|
| 215 |
+
last = cands[-1]
|
| 216 |
+
break
|
| 217 |
+
t += r.seg[i]
|
| 218 |
+
a0 = r.fixed_arr if r.started else None
|
| 219 |
+
wait_from = None if r.started else s["wait_from"]
|
| 220 |
+
res = _path(book, r, first, last, a0, s["ready"] or 0, wait_from)
|
| 221 |
+
if res is None and last != len(r.route) - 1:
|
| 222 |
+
last = len(r.route) - 1
|
| 223 |
+
res = _path(book, r, first, last, a0, s["ready"] or 0, wait_from)
|
| 224 |
+
if res is None:
|
| 225 |
+
plan[r.id] = Order(start=r.route[first] if (not r.started and first > 0) else None)
|
| 226 |
+
s["k"] += 1
|
| 227 |
+
s["ready"], s["wait_from"], s["start_at"] = None, None, None
|
| 228 |
+
push(u)
|
| 229 |
+
continue
|
| 230 |
+
arr, dep, stopped = res
|
| 231 |
+
d = run_dir(net, r)
|
| 232 |
+
if wait_from is not None:
|
| 233 |
+
book.pres[r.route[first]].append((d, wait_from, dep[first]))
|
| 234 |
+
for i in range(first, last):
|
| 235 |
+
book.sec[net.section(r.route[i], r.route[i + 1])].append((d, dep[i], arr[i + 1]))
|
| 236 |
+
if (i > first or r.started) and net.by_code[r.route[i]].tracks is not None:
|
| 237 |
+
book.pres[r.route[i]].append((d, arr[i], dep[i]))
|
| 238 |
+
plan[r.id] = Order(start=r.route[first] if (not r.started and first > 0) else None,
|
| 239 |
+
end=r.route[last] if last < len(r.route) - 1 else None,
|
| 240 |
+
hold={r.route[i]: dep[i] for i in range(first, last)
|
| 241 |
+
if net.by_code[r.route[i]].kind != "junction"})
|
| 242 |
+
end_code = r.route[last]
|
| 243 |
+
s["k"] += 1
|
| 244 |
+
s["ready"] = arr[last] + turn
|
| 245 |
+
s["start_at"] = end_code if last < len(r.route) - 1 else None
|
| 246 |
+
p_end = net.by_code[end_code]
|
| 247 |
+
if p_end.tracks is not None and p_end.kind != "junction":
|
| 248 |
+
s["wait_from"] = arr[last]
|
| 249 |
+
stay = (0, arr[last], task.end + 1440)
|
| 250 |
+
book.pres[end_code].append(stay)
|
| 251 |
+
s["park"] = (end_code, stay)
|
| 252 |
+
else:
|
| 253 |
+
s["wait_from"] = None
|
| 254 |
+
push(u)
|
| 255 |
+
from .solve import _tidy
|
| 256 |
+
return _tidy(task, plan)
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
def naive(task: Task) -> Plan:
|
| 260 |
+
return {}
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
POLICIES = {"naive": naive, "greedy": greedy, "greedy_fast": lambda t: greedy(t, fast_first=True)}
|
core/rail_core/build.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Build task packs (candidates -> references -> filters), resumable, deterministic for the same data and arguments.
|
| 2 |
+
|
| 3 |
+
python -m rail_core.build --split eval --out ../tasks/pool-eval --n 400 # a candidate pool, every tier
|
| 4 |
+
python -m rail_core.build --split train --out ../tasks/rail-v1-train --n 300
|
| 5 |
+
|
| 6 |
+
Candidates: the record's own disruptions on their own days (tier `real`), then seeded tiers (`ops.TIERS`) on every
|
| 7 |
+
day of the split at peak-hour starts, cycling seeds. A candidate is kept when:
|
| 8 |
+
the window can be rebuilt from the record (generate.build_task) and has at least MIN_RUNS runs
|
| 9 |
+
CP-SAT's plan is within 1 % of its proven bound, measured on the avoidable cost (cost - floor + gap_k)
|
| 10 |
+
the naive plan (change nothing) scores at most MAX_NAIVE_REWARD and the greedy dispatchers at most
|
| 11 |
+
MAX_GREEDY_REWARD
|
| 12 |
+
Every candidate and why it was dropped goes to build_log.jsonl, with the hardness features of every kept task
|
| 13 |
+
(`hardness.features`). Choosing the eval set from a pool is `rail_core.select`.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import argparse
|
| 19 |
+
import gzip
|
| 20 |
+
import hashlib
|
| 21 |
+
import json
|
| 22 |
+
import random
|
| 23 |
+
import time
|
| 24 |
+
from concurrent.futures import ProcessPoolExecutor
|
| 25 |
+
from pathlib import Path
|
| 26 |
+
|
| 27 |
+
from .generate import build_task
|
| 28 |
+
from .hardness import features
|
| 29 |
+
from .model import Closure, hhmm
|
| 30 |
+
from .network import load_network
|
| 31 |
+
from .ops import (LATE_CAUSES, PEAKS, TIERS, candidate_id, closures_for, real_disruptions, seeded_closures,
|
| 32 |
+
split_days)
|
| 33 |
+
from .reference import add_reference
|
| 34 |
+
|
| 35 |
+
MIN_RUNS = 15
|
| 36 |
+
MAX_NAIVE_REWARD = 0.6
|
| 37 |
+
MAX_GREEDY_REWARD = 0.85
|
| 38 |
+
MAX_GAP = 0.01
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def _rng(cid: str) -> random.Random:
|
| 42 |
+
return random.Random(int(hashlib.sha256(cid.encode()).hexdigest()[:12], 16))
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def make_candidate(day: str, start: int, window: int, tier: str, seed: int, split: str,
|
| 46 |
+
real: dict | None = None):
|
| 47 |
+
"""The task for one candidate (no reference yet), or (None, reason)."""
|
| 48 |
+
net = load_network()
|
| 49 |
+
cid = candidate_id(day, start, window, tier, seed)
|
| 50 |
+
rng = _rng(cid)
|
| 51 |
+
notices: list[str] = []
|
| 52 |
+
why: list[str] = []
|
| 53 |
+
task = None
|
| 54 |
+
if tier == "real":
|
| 55 |
+
# The record's start is when the disruption was announced; dispatchers have often cut trains short a few
|
| 56 |
+
# minutes before. Start the window (and the disruption) earlier until no train was cut before it.
|
| 57 |
+
for early in (0, 5, 10, 15, 20, 30, 45):
|
| 58 |
+
s0 = real["start"] - early
|
| 59 |
+
closures = closures_for(net, real["cause"], real["stations"], s0, real["end"], _rng(cid), real=True)
|
| 60 |
+
why = []
|
| 61 |
+
task = build_task(day, s0, s0 + window, closures, task_id=cid, split=split, difficulty=tier,
|
| 62 |
+
notices=notices, why=why)
|
| 63 |
+
if task is not None or "cut short" not in (why[0] if why else ""):
|
| 64 |
+
break
|
| 65 |
+
if task is not None and early:
|
| 66 |
+
task.notices.append(f"The record announces this disruption at {hhmm(real['start'])}; trains were already "
|
| 67 |
+
f"being cut short, so it starts here at {hhmm(task.start)}.")
|
| 68 |
+
else:
|
| 69 |
+
closures, notices = seeded_closures(net, tier, start, window, rng)
|
| 70 |
+
task = build_task(day, start, start + window, closures, task_id=cid, split=split, difficulty=tier,
|
| 71 |
+
notices=notices, why=why)
|
| 72 |
+
if task is None:
|
| 73 |
+
return None, "rebuild: " + (why[0] if why else "?")
|
| 74 |
+
lo, hi = TIERS[tier].get("late", (0, 0)) if isinstance(TIERS[tier].get("late"), tuple) else (0, 0)
|
| 75 |
+
waiting = [u for u in task.units if u.at and not task.run(u.runs[0]).started]
|
| 76 |
+
for u in rng.sample(waiting, min(len(waiting), rng.randint(lo, hi))):
|
| 77 |
+
extra = rng.choice((10, 15, 20, 25, 30))
|
| 78 |
+
first = task.run(u.runs[0])
|
| 79 |
+
u.ready = max(u.ready or start, first.dep[0]) + extra
|
| 80 |
+
task.notices.append(f"Train set {u.id} at {u.at} is held for {rng.choice(LATE_CAUSES)}: it cannot leave "
|
| 81 |
+
f"before {hhmm(u.ready)} (its next run {first.id} is due at {hhmm(first.dep[0])}).")
|
| 82 |
+
if len(task.runs) < MIN_RUNS:
|
| 83 |
+
return None, f"too small: {len(task.runs)} runs"
|
| 84 |
+
maxd = task.rule("max_delay")
|
| 85 |
+
for r in task.runs:
|
| 86 |
+
if r.started and r.arr[r.fixed_to] is not None and r.fixed_arr - r.arr[r.fixed_to] > maxd - 30:
|
| 87 |
+
return None, f"{r.id} is already {r.fixed_arr - r.arr[r.fixed_to]} min late"
|
| 88 |
+
return task, ""
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def _one(args):
|
| 92 |
+
day, start, window, tier, seed, split, real, limit, workers = args
|
| 93 |
+
t0 = time.time()
|
| 94 |
+
stats = {"day": day, "start": start, "window": window, "tier": tier, "seed": seed}
|
| 95 |
+
try:
|
| 96 |
+
task, reason = make_candidate(day, start, window, tier, seed, split, real)
|
| 97 |
+
if task is None:
|
| 98 |
+
stats["reason"] = reason
|
| 99 |
+
return None, stats
|
| 100 |
+
ref = add_reference(task, time_limit=limit, workers=workers)
|
| 101 |
+
gap = (ref["optimal_cost"] - ref["lower_bound"]) / (ref["optimal_cost"] - ref["floor_cost"] + task.rule("gap_k"))
|
| 102 |
+
stats.update(task_id=task.task_id, runs=len(task.runs), optimal=ref["optimal_cost"], bound=ref["lower_bound"],
|
| 103 |
+
floor=ref["floor_cost"], proven=ref["proven_optimal"], naive=ref["naive_reward"],
|
| 104 |
+
greedy=ref["greedy_reward"], greedy_fast=ref["greedy_fast_reward"],
|
| 105 |
+
real_cost=ref.get("real", {}).get("cost"))
|
| 106 |
+
if gap > MAX_GAP:
|
| 107 |
+
stats["reason"] = f"not solved within 1% ({gap:.3f})"
|
| 108 |
+
return None, stats
|
| 109 |
+
if ref["naive_reward"] > MAX_NAIVE_REWARD:
|
| 110 |
+
stats["reason"] = f"too easy: naive {ref['naive_reward']}"
|
| 111 |
+
return None, stats
|
| 112 |
+
if max(ref["greedy_reward"], ref["greedy_fast_reward"]) > MAX_GREEDY_REWARD:
|
| 113 |
+
stats["reason"] = f"too easy: greedy {max(ref['greedy_reward'], ref['greedy_fast_reward'])}"
|
| 114 |
+
return None, stats
|
| 115 |
+
ref["hardness"] = features(task)
|
| 116 |
+
stats["hardness"] = ref["hardness"]
|
| 117 |
+
stats["reason"] = "kept"
|
| 118 |
+
return task.to_dict(), stats
|
| 119 |
+
except ValueError as e:
|
| 120 |
+
if "no feasible plan" in str(e): # the record's state at `start` already breaks a rule (e.g. two trains
|
| 121 |
+
stats["reason"] = "infeasible: no legal plan from the rebuilt state" # stuck head-on)
|
| 122 |
+
else:
|
| 123 |
+
stats["reason"] = f"error {type(e).__name__}: {e}"
|
| 124 |
+
return None, stats
|
| 125 |
+
except Exception as e: # a generator or solver bug on one candidate is logged, not fatal
|
| 126 |
+
stats["reason"] = f"error {type(e).__name__}: {e}"
|
| 127 |
+
return None, stats
|
| 128 |
+
finally:
|
| 129 |
+
stats["seconds"] = round(time.time() - t0, 1)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def candidates(split: str, max_seed: int, tiers: list[str]) -> list[tuple]:
|
| 133 |
+
out = []
|
| 134 |
+
if "real" in tiers:
|
| 135 |
+
for d in real_disruptions(split):
|
| 136 |
+
for window in (180,):
|
| 137 |
+
out.append((d["day"], d["start"], window, "real", 0, split, d))
|
| 138 |
+
seeded = [t for t in tiers if t != "real"]
|
| 139 |
+
for seed in range(max_seed):
|
| 140 |
+
batch = []
|
| 141 |
+
for day in split_days(split):
|
| 142 |
+
for a, b in PEAKS:
|
| 143 |
+
for start in range(a, b + 1, 30):
|
| 144 |
+
for tier in seeded:
|
| 145 |
+
lo, hi = TIERS[tier]["window"]
|
| 146 |
+
window = lo + 30 * ((seed + start // 30) % ((hi - lo) // 30 + 1))
|
| 147 |
+
batch.append((day, start, window, tier, seed, split, None))
|
| 148 |
+
key = lambda c: hashlib.sha1(repr(c[:5]).encode()).hexdigest()
|
| 149 |
+
out += sorted(batch, key=key)
|
| 150 |
+
return out
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def write_pack(out: Path, tasks: list[dict], log: list[dict], split: str, extra: dict | None = None):
|
| 154 |
+
out.mkdir(parents=True, exist_ok=True)
|
| 155 |
+
body = "".join(json.dumps(t, ensure_ascii=False, separators=(",", ":")) + "\n" for t in tasks).encode()
|
| 156 |
+
name = "tasks.jsonl"
|
| 157 |
+
if len(body) > 8_000_000:
|
| 158 |
+
(out / "tasks.jsonl.gz").write_bytes(gzip.compress(body, mtime=0))
|
| 159 |
+
(out / "tasks.jsonl").unlink(missing_ok=True)
|
| 160 |
+
name = "tasks.jsonl.gz"
|
| 161 |
+
else:
|
| 162 |
+
(out / name).write_bytes(body)
|
| 163 |
+
(out / "tasks.jsonl.gz").unlink(missing_ok=True)
|
| 164 |
+
tiers: dict[str, int] = {}
|
| 165 |
+
for t in tasks:
|
| 166 |
+
tiers[t["difficulty"]] = tiers.get(t["difficulty"], 0) + 1
|
| 167 |
+
manifest = {"name": out.name, "split": split, "tasks": len(tasks), "file": name, "tiers": tiers,
|
| 168 |
+
"sha256": hashlib.sha256(body).hexdigest(), "candidates": len(log),
|
| 169 |
+
"days": sorted({t["day"] for t in tasks}),
|
| 170 |
+
"proven_optimal": sum(bool(t["reference"].get("proven_optimal")) for t in tasks),
|
| 171 |
+
"runs": [min((len(t["runs"]) for t in tasks), default=0), max((len(t["runs"]) for t in tasks), default=0)],
|
| 172 |
+
"source": "Rijden de Treinen train archive and disruptions, 2024-2025 (CC BY 4.0); OpenStreetMap "
|
| 173 |
+
"(ODbL); see data/gnlw/SOURCE.md", **(extra or {})}
|
| 174 |
+
(out / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n")
|
| 175 |
+
(out / "build_log.jsonl").write_text("".join(json.dumps(s) + "\n" for s in log))
|
| 176 |
+
return manifest
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def main():
|
| 180 |
+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 181 |
+
ap.add_argument("--split", required=True, choices=["eval", "train"])
|
| 182 |
+
ap.add_argument("--n", type=int, required=True, help="tasks to keep")
|
| 183 |
+
ap.add_argument("--out", required=True)
|
| 184 |
+
ap.add_argument("--tiers", nargs="*", default=list(TIERS))
|
| 185 |
+
ap.add_argument("--time-limit", type=float, default=120.0)
|
| 186 |
+
ap.add_argument("--procs", type=int, default=2)
|
| 187 |
+
ap.add_argument("--workers", type=int, default=2, help="CP-SAT threads per candidate")
|
| 188 |
+
ap.add_argument("--max-seed", type=int, default=4)
|
| 189 |
+
ap.add_argument("--max-candidates", type=int, default=0, help="stop after trying this many (0: no cap)")
|
| 190 |
+
args = ap.parse_args()
|
| 191 |
+
out = Path(args.out)
|
| 192 |
+
part = out / "partial"
|
| 193 |
+
part.mkdir(parents=True, exist_ok=True)
|
| 194 |
+
log: list[dict] = []
|
| 195 |
+
kept: list[dict] = []
|
| 196 |
+
done = set()
|
| 197 |
+
if (part / "log.jsonl").is_file():
|
| 198 |
+
for line in (part / "log.jsonl").read_text().splitlines():
|
| 199 |
+
st = json.loads(line)
|
| 200 |
+
log.append(st)
|
| 201 |
+
done.add((st["day"], st["start"], st["window"], st["tier"], st["seed"]))
|
| 202 |
+
if (part / "kept.jsonl").is_file():
|
| 203 |
+
kept = [json.loads(line) for line in (part / "kept.jsonl").read_text().splitlines()]
|
| 204 |
+
queue = [c for c in candidates(args.split, args.max_seed, args.tiers) if c[:5] not in done]
|
| 205 |
+
print(f"resuming: {len(log)} tried, {len(kept)} kept, {len(queue)} queued", flush=True)
|
| 206 |
+
from concurrent.futures import FIRST_COMPLETED, wait
|
| 207 |
+
|
| 208 |
+
def more() -> bool:
|
| 209 |
+
return bool(queue) and len(kept) < args.n and (not args.max_candidates or len(log) + len(running)
|
| 210 |
+
< args.max_candidates)
|
| 211 |
+
|
| 212 |
+
with ProcessPoolExecutor(args.procs) as ex:
|
| 213 |
+
running = set()
|
| 214 |
+
while True:
|
| 215 |
+
while more() and len(running) < args.procs: # keep every worker busy
|
| 216 |
+
running.add(ex.submit(_one, queue.pop(0) + (args.time_limit, args.workers)))
|
| 217 |
+
if not running:
|
| 218 |
+
break
|
| 219 |
+
finished, running = wait(running, return_when=FIRST_COMPLETED)
|
| 220 |
+
for f in finished:
|
| 221 |
+
task, stats = f.result()
|
| 222 |
+
log.append(stats)
|
| 223 |
+
with open(part / "log.jsonl", "a") as fh:
|
| 224 |
+
fh.write(json.dumps(stats) + "\n")
|
| 225 |
+
if task and len(kept) < args.n:
|
| 226 |
+
kept.append(task)
|
| 227 |
+
with open(part / "kept.jsonl", "a") as fh:
|
| 228 |
+
fh.write(json.dumps(task, ensure_ascii=False, separators=(",", ":")) + "\n")
|
| 229 |
+
reasons: dict[str, int] = {}
|
| 230 |
+
for s_ in log:
|
| 231 |
+
k = s_["reason"].split(":")[0].split(" (")[0]
|
| 232 |
+
reasons[k] = reasons.get(k, 0) + 1
|
| 233 |
+
print(f"{len(log)} tried, {len(kept)} kept; {reasons}", flush=True)
|
| 234 |
+
if len(kept) >= args.n:
|
| 235 |
+
for f in running:
|
| 236 |
+
f.cancel()
|
| 237 |
+
break
|
| 238 |
+
kept.sort(key=lambda t: (t["difficulty"], t["day"], t["start"], t["task_id"]))
|
| 239 |
+
print(json.dumps(write_pack(out, kept, log, args.split), indent=2))
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
if __name__ == "__main__":
|
| 243 |
+
main()
|
core/rail_core/check.py
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""The physics and the grader: a plan in, every train's times, every rule it breaks and its cost out. Pure Python and
|
| 2 |
+
deterministic; nothing here needs a solver.
|
| 3 |
+
|
| 4 |
+
Physics (`simulate`). Each train set works its runs in order. A run leaves its first point at the latest of: when its
|
| 5 |
+
train is ready there, its scheduled departure (or passing minute), and the plan's hold. It then runs at its minimum
|
| 6 |
+
running times, but never reaches a point before its timetable minute there (drivers run to the timetable; a late
|
| 7 |
+
train makes up time). At a scheduled stop it leaves at the latest of arriving, its scheduled departure and any hold; at a
|
| 8 |
+
point where it has no stop it passes, unless held past its passing minute: then it stops, losing `stop_loss` minutes
|
| 9 |
+
braking and `stop_loss` accelerating. Junctions cannot hold a train. After a run its train is ready again `turn_min`
|
| 10 |
+
minutes after arriving, at the point where the run ended.
|
| 11 |
+
|
| 12 |
+
Rules (`evaluate`), each broken one is a violation:
|
| 13 |
+
single track two trains going opposite ways may not be in a single-track section at once; the second enters
|
| 14 |
+
at least h_opp minutes after the first has left
|
| 15 |
+
headway trains going the same way keep h_follow minutes apart at both ends of every section, so nothing
|
| 16 |
+
overtakes between points
|
| 17 |
+
platforms no more trains at a station than it has tracks (minus any out of use); at a directional station
|
| 18 |
+
(double track) one per direction
|
| 19 |
+
closed no train in a closed section while it is closed (a train already stuck inside waits there)
|
| 20 |
+
continuity a run starts where its train is; short-turns only at turn points; a running train cannot be
|
| 21 |
+
cancelled or turned behind itself; every train ends the window where its next run leaves from
|
| 22 |
+
max delay no stop reached more than max_delay minutes late
|
| 23 |
+
|
| 24 |
+
Cost, in stop-minutes (lower is better): the arrival delay at every scheduled stop a train serves, plus cancel_cost
|
| 25 |
+
for every scheduled stop it does not serve, plus, for each train set, its lateness for its first run after the
|
| 26 |
+
window times that run's number of stops (delay handed on to the next window).
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
from __future__ import annotations
|
| 30 |
+
|
| 31 |
+
from collections import defaultdict
|
| 32 |
+
from dataclasses import dataclass, field
|
| 33 |
+
|
| 34 |
+
from .model import Order, Plan, Run, Task, hhmm
|
| 35 |
+
from .network import Network
|
| 36 |
+
|
| 37 |
+
NEG = -10**9
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@dataclass
|
| 41 |
+
class RunResult:
|
| 42 |
+
train: str
|
| 43 |
+
unit: str
|
| 44 |
+
cancelled: bool = False
|
| 45 |
+
first: int | None = None # route index of its first served point
|
| 46 |
+
last: int | None = None # route index of its last served point
|
| 47 |
+
arr: dict[int, int] = field(default_factory=dict)
|
| 48 |
+
dep: dict[int, int] = field(default_factory=dict)
|
| 49 |
+
stopped: dict[int, bool] = field(default_factory=dict)
|
| 50 |
+
delay: int = 0
|
| 51 |
+
unserved: int = 0
|
| 52 |
+
max_late: int = 0
|
| 53 |
+
cost: int = 0
|
| 54 |
+
problems: list[str] = field(default_factory=list)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
@dataclass
|
| 58 |
+
class PlanResult:
|
| 59 |
+
feasible: bool
|
| 60 |
+
cost: int | None
|
| 61 |
+
delay_cost: int
|
| 62 |
+
cancel_cost: int
|
| 63 |
+
carry_cost: int
|
| 64 |
+
cancelled: int
|
| 65 |
+
short_turned: int
|
| 66 |
+
runs: list[RunResult]
|
| 67 |
+
violations: list[dict]
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
@dataclass
|
| 71 |
+
class _Occ: # a train in a section
|
| 72 |
+
sec: int
|
| 73 |
+
dir: int
|
| 74 |
+
enter: int
|
| 75 |
+
exit: int
|
| 76 |
+
train: str
|
| 77 |
+
history: bool = False
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
@dataclass
|
| 81 |
+
class _Pres: # a train standing at (or passing) a station, minutes inclusive
|
| 82 |
+
point: str
|
| 83 |
+
dir: int
|
| 84 |
+
t0: int
|
| 85 |
+
t1: int
|
| 86 |
+
train: str
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def network(task: Task) -> Network:
|
| 90 |
+
n = task.__dict__.get("_net")
|
| 91 |
+
if n is None:
|
| 92 |
+
n = task.__dict__["_net"] = Network.from_dict(task.network)
|
| 93 |
+
return n
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def run_dir(net: Network, r: Run) -> int:
|
| 97 |
+
return net.direction(r.origin, r.dest)
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def served_part(task: Task, r: Run, o: Order) -> tuple[int, int, list[str]]:
|
| 101 |
+
"""(first, last, problems) for the part of the route the plan runs."""
|
| 102 |
+
net = network(task)
|
| 103 |
+
probs = []
|
| 104 |
+
s = r.idx(o.start) if o.start else 0
|
| 105 |
+
e = r.idx(o.end) if o.end else len(r.route) - 1
|
| 106 |
+
if o.start and o.start != r.origin:
|
| 107 |
+
if r.started:
|
| 108 |
+
probs.append(f"{r.id} is already running from {r.origin}: it cannot start at {o.start}")
|
| 109 |
+
s = 0
|
| 110 |
+
elif not net.by_code[o.start].turn:
|
| 111 |
+
probs.append(f"{r.id} cannot start at {o.start}: trains can only be turned at "
|
| 112 |
+
+ ", ".join(p.code for p in net.points if p.turn))
|
| 113 |
+
if o.end and o.end != r.dest and not net.by_code[o.end].turn:
|
| 114 |
+
probs.append(f"{r.id} cannot end at {o.end}: trains can only be turned at "
|
| 115 |
+
+ ", ".join(p.code for p in net.points if p.turn))
|
| 116 |
+
if e <= s:
|
| 117 |
+
probs.append(f"{r.id}: 'to' {r.route[e]} must come after where it starts ({r.route[s]})")
|
| 118 |
+
e = len(r.route) - 1
|
| 119 |
+
if r.started and e < r.fixed_to:
|
| 120 |
+
probs.append(f"{r.id} is already past {r.route[e]}: it can end at {r.route[r.fixed_to]} at the earliest")
|
| 121 |
+
e = len(r.route) - 1
|
| 122 |
+
return s, e, probs
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def simulate(task: Task, plan: Plan):
|
| 126 |
+
"""Times for every run under the plan. Returns (results by train, structural violations, section occupancies,
|
| 127 |
+
station presences, carry cost per unit)."""
|
| 128 |
+
net = network(task)
|
| 129 |
+
loss, turn = task.rule("stop_loss"), task.rule("turn_min")
|
| 130 |
+
runs = task.run_map
|
| 131 |
+
results: dict[str, RunResult] = {}
|
| 132 |
+
viol: list[dict] = []
|
| 133 |
+
occ: list[_Occ] = []
|
| 134 |
+
pres: list[_Pres] = []
|
| 135 |
+
carry: dict[str, int] = {}
|
| 136 |
+
slow = [c for c in task.closures if c.kind == "slow"]
|
| 137 |
+
|
| 138 |
+
def slow_extra(sec: int, t: int) -> int:
|
| 139 |
+
x = 0
|
| 140 |
+
for c in slow:
|
| 141 |
+
lo, hi = sorted((net.index[c.where[0]], net.index[c.where[-1]]))
|
| 142 |
+
if lo <= sec < hi and c.start <= t < c.end:
|
| 143 |
+
x = max(x, c.extra)
|
| 144 |
+
return x
|
| 145 |
+
|
| 146 |
+
def finite(code: str) -> bool:
|
| 147 |
+
p = net.by_code[code]
|
| 148 |
+
return p.kind != "junction" and p.tracks is not None
|
| 149 |
+
|
| 150 |
+
for u in task.units:
|
| 151 |
+
pos, ready = u.at, u.ready
|
| 152 |
+
wait_from: int | None = task.start if (u.at and finite(u.at)) else None # standing at a station at start
|
| 153 |
+
for rid in u.runs:
|
| 154 |
+
r = runs[rid]
|
| 155 |
+
o = plan.get(rid) or Order()
|
| 156 |
+
res = RunResult(train=rid, unit=u.id)
|
| 157 |
+
results[rid] = res
|
| 158 |
+
d = run_dir(net, r)
|
| 159 |
+
if o.cancel:
|
| 160 |
+
if r.started:
|
| 161 |
+
res.problems.append(f"{rid} is already running: it cannot be cancelled (short-turn it instead)")
|
| 162 |
+
o = Order(end=o.end, hold=o.hold)
|
| 163 |
+
else:
|
| 164 |
+
res.cancelled = True
|
| 165 |
+
res.unserved = r.n_stops
|
| 166 |
+
continue
|
| 167 |
+
s, e, probs = served_part(task, r, o)
|
| 168 |
+
res.problems += probs
|
| 169 |
+
k = r.fixed_to if r.started else s
|
| 170 |
+
if not r.started and r.route[s] != pos:
|
| 171 |
+
res.problems.append(f"{rid} would start at {r.route[s]}, but its train ({u.id}) is at {pos}")
|
| 172 |
+
for code in o.hold:
|
| 173 |
+
i = r.idx(code)
|
| 174 |
+
if i < k or i >= e:
|
| 175 |
+
res.problems.append(f"{rid}: hold at {code} is outside the part it runs "
|
| 176 |
+
f"({r.route[k]} to {r.route[e]}, holds only where it leaves from)")
|
| 177 |
+
elif net.by_code[code].kind == "junction":
|
| 178 |
+
res.problems.append(f"{rid}: trains cannot be held at the junction {code}")
|
| 179 |
+
res.first, res.last = s, e
|
| 180 |
+
arr, dep, stopped = res.arr, res.dep, res.stopped
|
| 181 |
+
last_occ = None
|
| 182 |
+
if r.started:
|
| 183 |
+
arr[k] = r.fixed_arr
|
| 184 |
+
if r.fixed_from is not None:
|
| 185 |
+
last_occ = _Occ(net.section(r.route[k - 1], r.route[k]), d, r.fixed_from, r.fixed_arr, rid, True)
|
| 186 |
+
occ.append(last_occ)
|
| 187 |
+
i = k
|
| 188 |
+
while True:
|
| 189 |
+
code = r.route[i]
|
| 190 |
+
pt = net.by_code[code]
|
| 191 |
+
h = o.hold.get(code) if s <= i < e else None
|
| 192 |
+
accel = 0
|
| 193 |
+
if i == e:
|
| 194 |
+
stopped[i] = True
|
| 195 |
+
break
|
| 196 |
+
if i == k and not r.started: # where the run starts
|
| 197 |
+
t = max(ready if ready is not None else NEG, r.dep[i], h if h is not None else NEG)
|
| 198 |
+
stopped[i] = True
|
| 199 |
+
accel = 0 if r.stops[i] else loss
|
| 200 |
+
if wait_from is not None and finite(code):
|
| 201 |
+
pres.append(_Pres(code, d, wait_from, t, rid))
|
| 202 |
+
elif r.stops[i]:
|
| 203 |
+
t = max(arr[i], r.dep[i], h if h is not None else NEG)
|
| 204 |
+
stopped[i] = True
|
| 205 |
+
elif pt.kind != "junction" and h is not None and h > arr[i]:
|
| 206 |
+
arr[i] += loss # an unscheduled stop: it brakes ...
|
| 207 |
+
if last_occ is not None:
|
| 208 |
+
last_occ.exit = arr[i] # ... still in the section it is leaving
|
| 209 |
+
t = max(arr[i], h)
|
| 210 |
+
stopped[i] = True
|
| 211 |
+
accel = loss # ... and accelerates again
|
| 212 |
+
else:
|
| 213 |
+
t = arr[i]
|
| 214 |
+
stopped[i] = False
|
| 215 |
+
if r.started and i == k:
|
| 216 |
+
t = max(t, task.start) # standing there when the task starts
|
| 217 |
+
dep[i] = t
|
| 218 |
+
if (i != k or r.started) and finite(code):
|
| 219 |
+
pres.append(_Pres(code, d, arr[i], t, rid))
|
| 220 |
+
sec = net.section(code, r.route[i + 1])
|
| 221 |
+
nxt = t + r.seg[i] + accel + slow_extra(sec, t)
|
| 222 |
+
if r.arr[i + 1] is not None:
|
| 223 |
+
nxt = max(nxt, r.arr[i + 1]) # never ahead of the timetable
|
| 224 |
+
if i + 1 == e and not r.stops[e]:
|
| 225 |
+
nxt += loss # brakes to end where it has no scheduled stop
|
| 226 |
+
arr[i + 1] = nxt
|
| 227 |
+
last_occ = _Occ(sec, d, t, nxt, rid)
|
| 228 |
+
occ.append(last_occ)
|
| 229 |
+
i += 1
|
| 230 |
+
# cost
|
| 231 |
+
for j in range(k if r.started else s + 1, e + 1):
|
| 232 |
+
if r.stops[j] and r.arr[j] is not None and arr[j] >= task.start:
|
| 233 |
+
late = arr[j] - r.arr[j]
|
| 234 |
+
res.delay += max(0, late)
|
| 235 |
+
res.max_late = max(res.max_late, late)
|
| 236 |
+
res.unserved = sum(1 for j, st in enumerate(r.stops) if st and not (s <= j <= e))
|
| 237 |
+
pos, ready = r.route[e], arr[e] + turn
|
| 238 |
+
wait_from = arr[e] if finite(r.route[e]) else None
|
| 239 |
+
if wait_from is not None and pos is not None:
|
| 240 |
+
pres.append(_Pres(pos, 0, wait_from, task.end + 24 * 60, u.id))
|
| 241 |
+
if u.after:
|
| 242 |
+
if pos != u.after["station"]:
|
| 243 |
+
viol.append({"rule": "continuity", "trains": [u.after["run"]], "where": pos,
|
| 244 |
+
"detail": f"train set {u.id} ends the window at {pos}, but its next run "
|
| 245 |
+
f"{u.after['run']} leaves {u.after['station']} at {hhmm(u.after['dep'])}"})
|
| 246 |
+
elif ready is not None:
|
| 247 |
+
carry[u.id] = max(0, ready - u.after["dep"]) * int(u.after["stops"])
|
| 248 |
+
return results, viol, occ, pres, carry
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def evaluate(task: Task, plan: Plan) -> PlanResult:
|
| 252 |
+
net = network(task)
|
| 253 |
+
results, viol, occ, pres, carry = simulate(task, plan)
|
| 254 |
+
hf, ho, maxd = task.rule("h_follow"), task.rule("h_opp"), task.rule("max_delay")
|
| 255 |
+
bad: dict[str, set] = defaultdict(set)
|
| 256 |
+
|
| 257 |
+
def add(rule: str, trains: list[str], where: str, time: int | None, detail: str):
|
| 258 |
+
viol.append({"rule": rule, "trains": trains, "where": where, "time": hhmm(time), "detail": detail})
|
| 259 |
+
for t in trains:
|
| 260 |
+
if t in results:
|
| 261 |
+
bad[t].add(rule)
|
| 262 |
+
|
| 263 |
+
for rid, res in results.items():
|
| 264 |
+
for p in res.problems:
|
| 265 |
+
add("continuity", [rid], "", None, p)
|
| 266 |
+
if res.max_late > maxd:
|
| 267 |
+
add("max_delay", [rid], "", None, f"{rid} reaches a stop {res.max_late} minutes late (limit {maxd}): "
|
| 268 |
+
f"short-turn or cancel it")
|
| 269 |
+
|
| 270 |
+
# sections: headways and single track
|
| 271 |
+
by_sec: dict[int, list[_Occ]] = defaultdict(list)
|
| 272 |
+
for o in occ:
|
| 273 |
+
by_sec[o.sec].append(o)
|
| 274 |
+
for sec, lst in by_sec.items():
|
| 275 |
+
s = net.sections[sec]
|
| 276 |
+
name = f"{s.a}-{s.b}"
|
| 277 |
+
lst.sort(key=lambda o: (o.enter, o.exit))
|
| 278 |
+
for i, a in enumerate(lst):
|
| 279 |
+
for b in lst[i + 1:]:
|
| 280 |
+
if b.enter > a.exit + max(hf, ho) and b.enter > a.enter + hf:
|
| 281 |
+
break
|
| 282 |
+
if a.train == b.train:
|
| 283 |
+
continue
|
| 284 |
+
if a.dir == b.dir:
|
| 285 |
+
if b.enter - a.enter < hf or b.exit - a.exit < hf:
|
| 286 |
+
what = "overtakes" if b.exit < a.exit else "follows too closely"
|
| 287 |
+
add("headway", [a.train, b.train], name, b.enter,
|
| 288 |
+
f"{b.train} {what} {a.train} in {name}: enter {hhmm(a.enter)}/{hhmm(b.enter)}, "
|
| 289 |
+
f"leave {hhmm(a.exit)}/{hhmm(b.exit)} (same direction: {hf} min apart at both ends, "
|
| 290 |
+
f"no overtaking between points)")
|
| 291 |
+
elif s.single and not (a.exit + ho <= b.enter or b.exit + ho <= a.enter):
|
| 292 |
+
add("single_track", [a.train, b.train], name, b.enter,
|
| 293 |
+
f"{a.train} ({hhmm(a.enter)}-{hhmm(a.exit)}) and {b.train} ({hhmm(b.enter)}-{hhmm(b.exit)}) "
|
| 294 |
+
f"meet head-on in single-track {name}")
|
| 295 |
+
# closures
|
| 296 |
+
for c in task.closures:
|
| 297 |
+
if c.kind != "closed":
|
| 298 |
+
continue
|
| 299 |
+
lo, hi = sorted((net.index[c.where[0]], net.index[c.where[-1]]))
|
| 300 |
+
for o in occ:
|
| 301 |
+
if lo <= o.sec < hi and not o.history and o.enter < c.end and o.exit > c.start:
|
| 302 |
+
s = net.sections[o.sec]
|
| 303 |
+
add("closed", [o.train], f"{s.a}-{s.b}", o.enter,
|
| 304 |
+
f"{o.train} is in {s.a}-{s.b} ({hhmm(o.enter)}-{hhmm(o.exit)}) while it is closed "
|
| 305 |
+
f"{hhmm(c.start)}-{hhmm(c.end)} ({c.cause})")
|
| 306 |
+
# platforms
|
| 307 |
+
out_of_use: dict[str, list] = defaultdict(list)
|
| 308 |
+
for c in task.closures:
|
| 309 |
+
if c.kind == "track":
|
| 310 |
+
out_of_use[c.where[0]].append((c.start, c.end))
|
| 311 |
+
by_pt: dict[str, list[_Pres]] = defaultdict(list)
|
| 312 |
+
for p in pres:
|
| 313 |
+
by_pt[p.point].append(p)
|
| 314 |
+
for code, lst in by_pt.items():
|
| 315 |
+
pt = net.by_code[code]
|
| 316 |
+
groups = [[p for p in lst if p.dir == d] for d in (1, -1)] if pt.directional else [lst]
|
| 317 |
+
for g in groups:
|
| 318 |
+
cap = 1 if pt.directional else pt.tracks
|
| 319 |
+
events = sorted({p.t0 for p in g} | {a for a, _ in out_of_use.get(code, [])})
|
| 320 |
+
reported = set()
|
| 321 |
+
for t in events:
|
| 322 |
+
here = [p for p in g if p.t0 <= t <= p.t1]
|
| 323 |
+
c_t = cap - sum(1 for a, b in out_of_use.get(code, []) if a <= t < b)
|
| 324 |
+
if len(here) > c_t:
|
| 325 |
+
key = tuple(sorted(p.train for p in here))
|
| 326 |
+
if key in reported:
|
| 327 |
+
continue
|
| 328 |
+
reported.add(key)
|
| 329 |
+
tracks = ("one track each way" if pt.directional else f"{c_t} track" + ("s" if c_t != 1 else ""))
|
| 330 |
+
add("platforms", list(key), code, t,
|
| 331 |
+
f"{len(here)} trains at {code} at {hhmm(t)} but it has {tracks}: "
|
| 332 |
+
+ ", ".join(f"{p.train} {hhmm(p.t0)}-{hhmm(p.t1) if p.t1 < task.end + 1440 else 'end'}"
|
| 333 |
+
for p in here))
|
| 334 |
+
|
| 335 |
+
delay = sum(r.delay for r in results.values())
|
| 336 |
+
cancel = task.rule("cancel_cost") * sum(r.unserved for r in results.values())
|
| 337 |
+
carry_cost = sum(carry.values())
|
| 338 |
+
for rid, res in results.items():
|
| 339 |
+
res.cost = res.delay + task.rule("cancel_cost") * res.unserved
|
| 340 |
+
res.problems = sorted(bad.get(rid, set()))
|
| 341 |
+
feasible = not viol
|
| 342 |
+
n_cxl = sum(1 for r in results.values() if r.cancelled)
|
| 343 |
+
n_st = sum(1 for rid, r in results.items()
|
| 344 |
+
if not r.cancelled and (r.first != 0 or r.last != len(task.run(rid).route) - 1))
|
| 345 |
+
return PlanResult(feasible, delay + cancel + carry_cost if feasible else None, delay, cancel, carry_cost, n_cxl,
|
| 346 |
+
n_st, list(results.values()), viol)
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
def timetable(task: Task, res: RunResult) -> list[dict]:
|
| 350 |
+
"""The served points of one run with arrival and departure (passing) minutes, for display."""
|
| 351 |
+
r = task.run(res.train)
|
| 352 |
+
net = network(task)
|
| 353 |
+
out = []
|
| 354 |
+
if res.cancelled:
|
| 355 |
+
return out
|
| 356 |
+
for i in sorted(set(res.arr) | set(res.dep)):
|
| 357 |
+
code = r.route[i]
|
| 358 |
+
if net.by_code[code].kind == "junction":
|
| 359 |
+
continue
|
| 360 |
+
out.append({"at": code, "arr": hhmm(res.arr.get(i)), "dep": hhmm(res.dep.get(i)),
|
| 361 |
+
"stop": bool(res.stopped.get(i)), "sched": hhmm(r.arr[i] if r.stops[i] and r.arr[i] is not None
|
| 362 |
+
else r.dep[i] if r.stops[i] else None),
|
| 363 |
+
"late": (res.arr[i] - r.arr[i]) if (r.stops[i] and r.arr[i] is not None and i in res.arr) else None})
|
| 364 |
+
return out
|
core/rail_core/data.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""The curated record (data/gnlw): every Arriva train between Leeuwarden and Groningen, January-June 2025, with its
|
| 2 |
+
schedule, delays and cancellations; the derived rolling-stock links; the real disruptions; the minimum running times.
|
| 3 |
+
|
| 4 |
+
`day_runs(day)` turns one service day into model runs: the route through every point (junctions included), the
|
| 5 |
+
scheduled stops, the timetable's passing minutes at the points in between (interpolated by distance) and the
|
| 6 |
+
minimum running time of every section (the calibrated stop-to-stop minimum, split by distance).
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import csv
|
| 12 |
+
import gzip
|
| 13 |
+
import json
|
| 14 |
+
from dataclasses import dataclass, field
|
| 15 |
+
from functools import lru_cache
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
|
| 18 |
+
from .model import Run
|
| 19 |
+
from .network import DATA, Network, load_network
|
| 20 |
+
|
| 21 |
+
LINE_DIR = DATA / "gnlw"
|
| 22 |
+
KIND = {"Stoptrein": "stop", "Sneltrein": "fast"}
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@dataclass
|
| 26 |
+
class Stop:
|
| 27 |
+
code: str
|
| 28 |
+
sa: int | None # scheduled arrival
|
| 29 |
+
sd: int | None # scheduled departure
|
| 30 |
+
ad: int | None # arrival delay
|
| 31 |
+
dd: int | None # departure delay
|
| 32 |
+
arr_cxl: bool
|
| 33 |
+
dep_cxl: bool
|
| 34 |
+
|
| 35 |
+
@property
|
| 36 |
+
def aa(self) -> int | None: # actual arrival
|
| 37 |
+
return None if self.sa is None or self.ad is None else self.sa + self.ad
|
| 38 |
+
|
| 39 |
+
@property
|
| 40 |
+
def adep(self) -> int | None: # actual departure
|
| 41 |
+
return None if self.sd is None or self.dd is None else self.sd + self.dd
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@dataclass
|
| 45 |
+
class Service:
|
| 46 |
+
train: str
|
| 47 |
+
type: str
|
| 48 |
+
stops: list[Stop]
|
| 49 |
+
full_cxl: bool
|
| 50 |
+
part_cxl: bool
|
| 51 |
+
|
| 52 |
+
@property
|
| 53 |
+
def kind(self) -> str:
|
| 54 |
+
return KIND.get(self.type, "stop")
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
@dataclass
|
| 58 |
+
class Day:
|
| 59 |
+
date: str
|
| 60 |
+
services: dict[str, Service]
|
| 61 |
+
links: dict[str, str] # arriving train -> the train it forms next
|
| 62 |
+
runs: dict[str, Run] = field(default_factory=dict)
|
| 63 |
+
chains: list[list[str]] = field(default_factory=list)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def _int(x: str) -> int | None:
|
| 67 |
+
return int(x) if x not in ("", None) else None
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
@lru_cache(maxsize=1)
|
| 71 |
+
def _all_services() -> dict[str, dict[str, Service]]:
|
| 72 |
+
by_day: dict[str, dict[str, Service]] = {}
|
| 73 |
+
rows: dict[tuple, list[dict]] = {}
|
| 74 |
+
with gzip.open(LINE_DIR / "services_2025h1.csv.gz", "rt", encoding="utf-8") as f:
|
| 75 |
+
for r in csv.DictReader(f):
|
| 76 |
+
rows.setdefault((r["date"], r["svc"]), []).append(r)
|
| 77 |
+
for (day, _), rs in rows.items():
|
| 78 |
+
rs.sort(key=lambda r: int(r["seq"]))
|
| 79 |
+
s = Service(rs[0]["train"], rs[0]["type"],
|
| 80 |
+
[Stop(r["code"], _int(r["sched_arr"]), _int(r["sched_dep"]), _int(r["arr_delay"]),
|
| 81 |
+
_int(r["dep_delay"]), r["arr_cxl"] == "1", r["dep_cxl"] == "1") for r in rs],
|
| 82 |
+
rs[0]["full_cxl"] == "1", rs[0]["part_cxl"] == "1")
|
| 83 |
+
by_day.setdefault(day, {})
|
| 84 |
+
if s.train in by_day[day]: # a number used twice in a day: keep the first, number the second
|
| 85 |
+
s.train = f"{s.train}b"
|
| 86 |
+
by_day[day][s.train] = s
|
| 87 |
+
return by_day
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
@lru_cache(maxsize=1)
|
| 91 |
+
def _all_links() -> dict[str, dict[str, str]]:
|
| 92 |
+
out: dict[str, dict[str, str]] = {}
|
| 93 |
+
with open(LINE_DIR / "links_2025h1.csv", encoding="utf-8") as f:
|
| 94 |
+
for r in csv.DictReader(f):
|
| 95 |
+
out.setdefault(r["date"], {})[r["arriving"]] = r["forms"]
|
| 96 |
+
return out
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
@lru_cache(maxsize=1)
|
| 100 |
+
def runtimes() -> dict:
|
| 101 |
+
return json.loads((LINE_DIR / "runtimes.json").read_text())
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def disruptions() -> list[dict]:
|
| 105 |
+
with open(LINE_DIR / "disruptions_2025h1.csv", encoding="utf-8") as f:
|
| 106 |
+
return list(csv.DictReader(f))
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def days() -> list[str]:
|
| 110 |
+
return sorted(_all_services())
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def span_minutes(net: Network, kind: str, a: str, b: str) -> int:
|
| 114 |
+
"""Minimum running minutes between consecutive scheduled stops a and b."""
|
| 115 |
+
rt = runtimes()
|
| 116 |
+
hit = rt.get(kind, {}).get(f"{a}>{b}")
|
| 117 |
+
if hit:
|
| 118 |
+
return int(hit["min"])
|
| 119 |
+
# fallback: the stopping train's legs, less a minute per stop skipped
|
| 120 |
+
route = [c for c in net.route(a, b) if net.by_code[c].kind != "junction"]
|
| 121 |
+
total = 0
|
| 122 |
+
for x, y in zip(route, route[1:]):
|
| 123 |
+
leg = rt["stop"].get(f"{x}>{y}")
|
| 124 |
+
if leg is None:
|
| 125 |
+
raise KeyError(f"no running time for {kind} {a}>{b}")
|
| 126 |
+
total += int(leg["min"])
|
| 127 |
+
return max(1, total - (len(route) - 2))
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def clean_stops(net: Network, s: Service) -> list[Stop]:
|
| 131 |
+
"""The service's stops in route order with both times where it calls on the way. On disrupted days the record
|
| 132 |
+
holds services changed on the day: an extra stop with only one time, or stops listed out of order."""
|
| 133 |
+
timed = sorted(s.stops, key=lambda st: st.sd if st.sd is not None else (st.sa if st.sa is not None else 10**9))
|
| 134 |
+
first, last = timed[0].code, timed[-1].code
|
| 135 |
+
d = net.direction(first, last)
|
| 136 |
+
seen: dict[str, Stop] = {}
|
| 137 |
+
for st in s.stops:
|
| 138 |
+
seen.setdefault(st.code, st)
|
| 139 |
+
out = sorted(seen.values(), key=lambda st: d * net.index[st.code])
|
| 140 |
+
for i, st in enumerate(out):
|
| 141 |
+
if 0 < i < len(out) - 1:
|
| 142 |
+
if st.sa is None:
|
| 143 |
+
st.sa = st.sd
|
| 144 |
+
if st.sd is None:
|
| 145 |
+
st.sd = st.sa
|
| 146 |
+
if out[0].sd is None:
|
| 147 |
+
out[0].sd = out[0].sa
|
| 148 |
+
if out[-1].sa is None:
|
| 149 |
+
out[-1].sa = out[-1].sd
|
| 150 |
+
times = [t for st in out for t in (st.sa, st.sd) if t is not None]
|
| 151 |
+
if any(b < a for a, b in zip(times, times[1:])):
|
| 152 |
+
raise ValueError(f"service {s.train}: times not in route order")
|
| 153 |
+
return out
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def make_run(net: Network, s: Service, unit: str = "") -> Run:
|
| 157 |
+
stops_rec = clean_stops(net, s)
|
| 158 |
+
codes = [st.code for st in stops_rec]
|
| 159 |
+
route = net.route(codes[0], codes[-1])
|
| 160 |
+
stop_at = {st.code: st for st in stops_rec}
|
| 161 |
+
n = len(route)
|
| 162 |
+
stops = [c in stop_at for c in route]
|
| 163 |
+
arr: list[int | None] = [None] * n
|
| 164 |
+
dep: list[int | None] = [None] * n
|
| 165 |
+
seg = [0] * (n - 1)
|
| 166 |
+
idx = [i for i in range(n) if stops[i]]
|
| 167 |
+
for i in idx:
|
| 168 |
+
st = stop_at[route[i]]
|
| 169 |
+
arr[i] = st.sa if i > 0 else None
|
| 170 |
+
dep[i] = st.sd if i < n - 1 else None
|
| 171 |
+
for ia, ib in zip(idx, idx[1:]):
|
| 172 |
+
a, b = stop_at[route[ia]], stop_at[route[ib]]
|
| 173 |
+
t_min = span_minutes(net, s.kind, route[ia], route[ib])
|
| 174 |
+
t_sched = (b.sa if b.sa is not None else b.sd) - (a.sd if a.sd is not None else a.sa)
|
| 175 |
+
ka, kb = net.by_code[route[ia]].km, net.by_code[route[ib]].km
|
| 176 |
+
cum = [round(t_min * abs(net.by_code[route[i]].km - ka) / abs(kb - ka)) for i in range(ia, ib + 1)]
|
| 177 |
+
for j in range(1, len(cum)): # every section takes at least a minute
|
| 178 |
+
cum[j] = max(cum[j], cum[j - 1] + 1)
|
| 179 |
+
for j in range(ia, ib):
|
| 180 |
+
seg[j] = cum[j - ia + 1] - cum[j - ia]
|
| 181 |
+
t0 = a.sd if a.sd is not None else a.sa
|
| 182 |
+
for i in range(ia + 1, ib):
|
| 183 |
+
arr[i] = dep[i] = t0 + round(t_sched * abs(net.by_code[route[i]].km - ka) / abs(kb - ka))
|
| 184 |
+
return Run(id=s.train, kind=s.kind, route=route, stops=stops, arr=arr, dep=dep, seg=seg, unit=unit)
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
@lru_cache(maxsize=64)
|
| 188 |
+
def load_day(date: str) -> Day:
|
| 189 |
+
net = load_network()
|
| 190 |
+
services = dict(_all_services()[date])
|
| 191 |
+
for t in list(services): # a handful of services changed on the day are unreadable: drop them
|
| 192 |
+
try:
|
| 193 |
+
clean_stops(net, services[t])
|
| 194 |
+
except ValueError:
|
| 195 |
+
del services[t]
|
| 196 |
+
links = {a: f for a, f in _all_links().get(date, {}).items() if a in services and f in services}
|
| 197 |
+
day = Day(date, services, links)
|
| 198 |
+
formed = set(links.values())
|
| 199 |
+
starts = sorted((t for t in services if t not in formed), key=lambda t: services[t].stops[0].sd or 0)
|
| 200 |
+
for k, t in enumerate(starts):
|
| 201 |
+
chain = [t]
|
| 202 |
+
while chain[-1] in links:
|
| 203 |
+
chain.append(links[chain[-1]])
|
| 204 |
+
day.chains.append(chain)
|
| 205 |
+
for x in chain:
|
| 206 |
+
day.runs[x] = make_run(net, services[x], unit=f"T{k + 1:02d}")
|
| 207 |
+
return day
|
core/rail_core/generate.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""A task from a real day: the line as it really stood at `start`, every run that leaves before `end`, the closures.
|
| 2 |
+
|
| 3 |
+
The state at `start` comes from the 2025 record:
|
| 4 |
+
a train running then is placed where its actual departure from its last stop puts it (passing points by the
|
| 5 |
+
minimum running times); it keeps its real delay
|
| 6 |
+
a train set standing at a terminal is ready `turn_min` after its last run's actual arrival
|
| 7 |
+
Everything after `start` is decided by the plan; the record's later times are kept only to score what the real
|
| 8 |
+
dispatchers did (`reference.real`).
|
| 9 |
+
|
| 10 |
+
A window is refused (`None`) when it cannot be rebuilt honestly: a run before `start` was cancelled or cut short in
|
| 11 |
+
the record (its train is then not where the schedule says), or the plain timetable already conflicts inside it.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
from .check import evaluate, network
|
| 17 |
+
from .data import Day, Service, load_day
|
| 18 |
+
from .model import RULES, Closure, Order, Run, Task, Unit, hhmm
|
| 19 |
+
from .network import Network, load_network
|
| 20 |
+
|
| 21 |
+
# causes after which the train involved is itself out of service (it blocks the section until the line reopens)
|
| 22 |
+
FAILURES = ("broken down train", "collision", "defective train", "a collision", "an accident")
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _actual(svc: Service, code: str) -> tuple[int | None, int | None, bool]:
|
| 26 |
+
"""(actual arrival, actual departure, cancelled) at a stop of the record."""
|
| 27 |
+
for st in svc.stops:
|
| 28 |
+
if st.code == code:
|
| 29 |
+
return st.aa, st.adep, (st.arr_cxl or st.sa is None) and (st.dep_cxl or st.sd is None)
|
| 30 |
+
return None, None, True
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _is_failure(net: Network, r: Run, served: list[str], closed: list[Closure]) -> bool:
|
| 34 |
+
"""The run that broke down (or collided): cut short by a failure closure, last served next to the closed span."""
|
| 35 |
+
for c in closed:
|
| 36 |
+
if not any(f in c.cause.lower() for f in FAILURES):
|
| 37 |
+
continue
|
| 38 |
+
lo, hi = sorted((net.index[c.where[0]], net.index[c.where[-1]]))
|
| 39 |
+
last = served[-1] if served else r.origin
|
| 40 |
+
if lo - 1 <= net.index[last] <= hi + 1:
|
| 41 |
+
return True
|
| 42 |
+
return False
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def _served_in_record(svc: Service) -> list[str]:
|
| 46 |
+
return [st.code for st in svc.stops if not ((st.arr_cxl or st.sa is None) and (st.dep_cxl or st.sd is None))]
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _place(net: Network, r: Run, svc: Service, start: int, closed: list[Closure]):
|
| 50 |
+
"""Where a run is at `start`, from the record: None (it was cut short before `start`: the window cannot be
|
| 51 |
+
rebuilt), ("done", actual arrival), ("future", None), or (fixed_to, fixed_arr, fixed_from, stuck)."""
|
| 52 |
+
last_t = last_i = None
|
| 53 |
+
stop_at = len(r.route) - 1 # the first stop it had not reached by `start`
|
| 54 |
+
for i, code in enumerate(r.route):
|
| 55 |
+
if not r.stops[i]:
|
| 56 |
+
continue
|
| 57 |
+
aa, ad, cxl = _actual(svc, code)
|
| 58 |
+
t_sched = r.arr[i] if r.arr[i] is not None else r.dep[i]
|
| 59 |
+
if cxl:
|
| 60 |
+
if t_sched < start:
|
| 61 |
+
return None
|
| 62 |
+
stop_at = i
|
| 63 |
+
break
|
| 64 |
+
if i > 0 and (aa is None or aa > start):
|
| 65 |
+
stop_at = i
|
| 66 |
+
break
|
| 67 |
+
if i == len(r.route) - 1:
|
| 68 |
+
return ("done", aa)
|
| 69 |
+
if ad is None or ad > start:
|
| 70 |
+
return ("future", None) if i == 0 else (i, aa, None, False)
|
| 71 |
+
last_t, last_i = ad, i
|
| 72 |
+
if last_i is None:
|
| 73 |
+
return ("future", None)
|
| 74 |
+
t = last_t
|
| 75 |
+
for i in range(last_i, stop_at):
|
| 76 |
+
nxt = t + r.seg[i]
|
| 77 |
+
if r.arr[i + 1] is not None:
|
| 78 |
+
nxt = max(nxt, r.arr[i + 1])
|
| 79 |
+
sec = net.section(r.route[i], r.route[i + 1])
|
| 80 |
+
for c in closed:
|
| 81 |
+
lo, hi = sorted((net.index[c.where[0]], net.index[c.where[-1]]))
|
| 82 |
+
if lo <= sec < hi and c.start <= start < c.end and t < c.start:
|
| 83 |
+
return (i + 1, max(start, c.end + max(0, nxt - c.start)), t, True) # caught in the closed section
|
| 84 |
+
if nxt >= start or i + 1 == stop_at:
|
| 85 |
+
return (i + 1, max(start, nxt), t, False)
|
| 86 |
+
t = nxt
|
| 87 |
+
return ("done", t)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def build_task(date: str, start: int, end: int, closures: list[Closure] | None = None, *, task_id: str = "",
|
| 91 |
+
split: str = "train", difficulty: str = "standard", notices: list[str] | None = None,
|
| 92 |
+
disruptions: list[dict] | None = None, rules: dict | None = None,
|
| 93 |
+
lookback: int = 150, why: list | None = None) -> Task | None:
|
| 94 |
+
why = why if why is not None else []
|
| 95 |
+
net = load_network()
|
| 96 |
+
day = load_day(date)
|
| 97 |
+
rules = {**RULES, **(rules or {})}
|
| 98 |
+
turn = rules["turn_min"]
|
| 99 |
+
closures = list(closures or [])
|
| 100 |
+
closed = [c for c in closures if c.kind == "closed"]
|
| 101 |
+
runs: list[Run] = []
|
| 102 |
+
units: list[Unit] = []
|
| 103 |
+
notes = list(notices or [])
|
| 104 |
+
real_runs: dict[str, dict] = {}
|
| 105 |
+
failed = None # (unit, run, last stop): the train that broke down
|
| 106 |
+
for chain in day.chains:
|
| 107 |
+
u_runs: list[Run] = []
|
| 108 |
+
at, ready, after = None, None, None
|
| 109 |
+
last_hist: Service | None = None
|
| 110 |
+
for rid in chain:
|
| 111 |
+
r0 = day.runs[rid]
|
| 112 |
+
svc = day.services[rid]
|
| 113 |
+
sched_dep = r0.dep[0]
|
| 114 |
+
if sched_dep < start - lookback: # long before the window: history
|
| 115 |
+
last_hist = svc
|
| 116 |
+
at, ready = r0.dest, None
|
| 117 |
+
aa, _, cxl = _actual(svc, r0.dest)
|
| 118 |
+
if aa is not None and not cxl:
|
| 119 |
+
ready = aa + turn
|
| 120 |
+
continue
|
| 121 |
+
if last_hist is not None:
|
| 122 |
+
if last_hist.full_cxl or last_hist.part_cxl:
|
| 123 |
+
why.append(f"{last_hist.train} (before the window) was cut short: its train is not where the "
|
| 124 |
+
f"schedule says")
|
| 125 |
+
return None
|
| 126 |
+
last_hist = None
|
| 127 |
+
if sched_dep >= end:
|
| 128 |
+
after = {"run": rid, "station": r0.origin, "dep": sched_dep, "stops": r0.n_stops}
|
| 129 |
+
break
|
| 130 |
+
if svc.full_cxl or svc.part_cxl:
|
| 131 |
+
served = _served_in_record(svc)
|
| 132 |
+
cut_before = [c for c in r0.route if r0.stops[r0.route.index(c)] and c not in served]
|
| 133 |
+
early = [c for c in cut_before if (r0.arr[r0.idx(c)] or r0.dep[r0.idx(c)]) < start]
|
| 134 |
+
if early:
|
| 135 |
+
if failed is None and _is_failure(net, r0, served, closed):
|
| 136 |
+
failed = (r0.unit, rid, served[-1] if served else r0.origin)
|
| 137 |
+
u_runs = []
|
| 138 |
+
break
|
| 139 |
+
why.append(f"{rid} was cut short before the window ({', '.join(early)})")
|
| 140 |
+
return None
|
| 141 |
+
r = Run(**{**r0.__dict__})
|
| 142 |
+
r.route, r.stops, r.arr, r.dep, r.seg = list(r0.route), list(r0.stops), list(r0.arr), list(r0.dep), list(r0.seg)
|
| 143 |
+
placed = _place(net, r, svc, start, closed)
|
| 144 |
+
if placed is None:
|
| 145 |
+
why.append(f"{rid}: a stop before the window was cancelled")
|
| 146 |
+
return None
|
| 147 |
+
if isinstance(placed, tuple) and placed[0] == "done":
|
| 148 |
+
at, ready = r.dest, max(start, placed[1] + turn)
|
| 149 |
+
continue
|
| 150 |
+
if isinstance(placed, tuple) and placed[0] == "future":
|
| 151 |
+
u_runs.append(r)
|
| 152 |
+
elif isinstance(placed, tuple):
|
| 153 |
+
fixed_to, fixed_arr, fixed_from, stuck = placed
|
| 154 |
+
if u_runs: # a later run of the same set cannot be running already
|
| 155 |
+
why.append(f"{rid} is running but an earlier run of its set {r.unit} has not")
|
| 156 |
+
return None
|
| 157 |
+
r.fixed_to, r.fixed_arr, r.fixed_from, r.stuck = fixed_to, fixed_arr, fixed_from, stuck
|
| 158 |
+
u_runs.append(r)
|
| 159 |
+
at, ready = None, None
|
| 160 |
+
real_runs[rid] = _real(r, svc, start)
|
| 161 |
+
if not u_runs or (failed and u_runs[0].unit == failed[0]):
|
| 162 |
+
continue
|
| 163 |
+
if u_runs[0].started:
|
| 164 |
+
at, ready = None, None
|
| 165 |
+
else:
|
| 166 |
+
if at is None:
|
| 167 |
+
at = u_runs[0].origin
|
| 168 |
+
ready = start if ready is None else max(start, ready)
|
| 169 |
+
units.append(Unit(id=u_runs[0].unit, runs=[r.id for r in u_runs], at=at, ready=ready, after=after))
|
| 170 |
+
runs += u_runs
|
| 171 |
+
if not runs:
|
| 172 |
+
why.append("no runs")
|
| 173 |
+
return None
|
| 174 |
+
if failed:
|
| 175 |
+
c = closed[0]
|
| 176 |
+
notes.insert(0, f"Train {failed[1]} broke down after {failed[2]} and is stuck in {c.where[0]}-{c.where[-1]}; "
|
| 177 |
+
f"its train set is out of service for the rest of the window, so its later runs are already "
|
| 178 |
+
f"cancelled and are not yours to plan.")
|
| 179 |
+
task = Task(task_id=task_id or f"{date}-{hhmm(start).replace(':', '')}", split=split, line="GN-LW", day=date,
|
| 180 |
+
start=start, end=end, network=net.to_dict(), runs=runs, units=units, closures=closures, notices=notes,
|
| 181 |
+
disruptions=list(disruptions or []), difficulty=difficulty, reference={}, rules=rules)
|
| 182 |
+
# the plain timetable (no delays, no closures) must be conflict-free in the window
|
| 183 |
+
calm = Task.from_dict({**task.to_dict(), "closures": []})
|
| 184 |
+
for r in calm.runs:
|
| 185 |
+
r.fixed_to, r.fixed_arr, r.fixed_from, r.stuck = 0, None, None, False
|
| 186 |
+
for u in calm.units:
|
| 187 |
+
first = calm.run(u.runs[0])
|
| 188 |
+
u.at, u.ready = first.origin, None
|
| 189 |
+
calm_res = evaluate(calm, {})
|
| 190 |
+
if not calm_res.feasible:
|
| 191 |
+
why.append("the plain timetable conflicts in the window: " + calm_res.violations[0]["detail"][:160])
|
| 192 |
+
return None
|
| 193 |
+
task.reference["real"] = _real_cost(task, real_runs)
|
| 194 |
+
return task
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def _real(r: Run, svc: Service, start: int) -> dict:
|
| 198 |
+
"""What really happened to a run: actual arrivals at the stops it served, and the stops it did not."""
|
| 199 |
+
arr, unserved = {}, 0
|
| 200 |
+
for i, code in enumerate(r.route):
|
| 201 |
+
if not r.stops[i]:
|
| 202 |
+
continue
|
| 203 |
+
aa, ad, cxl = _actual(svc, code)
|
| 204 |
+
if cxl:
|
| 205 |
+
unserved += 1
|
| 206 |
+
elif i > 0 and aa is not None:
|
| 207 |
+
arr[code] = aa
|
| 208 |
+
return {"arr": arr, "unserved": unserved, "cancelled": svc.full_cxl, "short": svc.part_cxl and not svc.full_cxl}
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
def _real_cost(task: Task, real_runs: dict[str, dict]) -> dict:
|
| 212 |
+
"""The record's outcome scored with the task's cost (delays at served stops reached after `start`, cancel cost
|
| 213 |
+
for unserved stops). It is what Arriva and ProRail did with their information; it need not obey this model."""
|
| 214 |
+
delay = unserved = cxl = short = 0
|
| 215 |
+
for r in task.runs:
|
| 216 |
+
x = real_runs.get(r.id)
|
| 217 |
+
if x is None:
|
| 218 |
+
continue
|
| 219 |
+
for i, code in enumerate(r.route):
|
| 220 |
+
if r.stops[i] and code in x["arr"] and x["arr"][code] >= task.start and r.arr[i] is not None:
|
| 221 |
+
delay += max(0, x["arr"][code] - r.arr[i])
|
| 222 |
+
unserved += x["unserved"]
|
| 223 |
+
cxl += bool(x["cancelled"])
|
| 224 |
+
short += bool(x["short"])
|
| 225 |
+
return {"cost": delay + task.rule("cancel_cost") * unserved, "delay": delay, "unserved_stops": unserved,
|
| 226 |
+
"cancelled": cxl, "short_turned": short}
|
core/rail_core/hardness.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Hardness features of a task: model-independent properties that predict how badly an agent will do on it.
|
| 2 |
+
|
| 3 |
+
They come from the task and its reference only (no language model), so an eval set can be chosen by them without
|
| 4 |
+
ever looking at a model's results on the eval tasks themselves (see `select`).
|
| 5 |
+
|
| 6 |
+
runs, sets problem size
|
| 7 |
+
conflicts rule breaks if nothing is changed (single track, headway, platforms, closures)
|
| 8 |
+
decisions holds + short-turns + cancellations in the proven optimum: how much must change
|
| 9 |
+
turn_points distinct points where the optimum short-turns trains
|
| 10 |
+
avoidable optimum - floor: what good sequencing saves over the train sets planned alone
|
| 11 |
+
greedy_gap how far the best greedy dispatcher is from the optimum, in reward
|
| 12 |
+
closed_min, slow_min, track_min minutes of each kind of disruption inside the window
|
| 13 |
+
single_closed a closure on single track (no way round it)
|
| 14 |
+
mid_line a closure with turn points on both sides (trains must be turned at both ends of it)
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
from collections import Counter
|
| 20 |
+
|
| 21 |
+
from .check import evaluate, network
|
| 22 |
+
from .model import Task, plan_from_list
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def features(task: Task) -> dict:
|
| 26 |
+
net = network(task)
|
| 27 |
+
ref = task.reference
|
| 28 |
+
naive = evaluate(task, {})
|
| 29 |
+
rules = Counter(v["rule"] for v in naive.violations)
|
| 30 |
+
opt = ref.get("optimal_plan") or []
|
| 31 |
+
holds = sum(len(e.get("hold") or {}) for e in opt)
|
| 32 |
+
shorts = sum(1 for e in opt if ("to" in e or "from" in e) and not e.get("cancel"))
|
| 33 |
+
cancels = sum(1 for e in opt if e.get("cancel"))
|
| 34 |
+
turn_pts = {e.get("to") for e in opt if e.get("to")} | {e.get("from") for e in opt if e.get("from")}
|
| 35 |
+
clip = lambda c: max(0, min(c.end, task.end) - max(c.start, task.start))
|
| 36 |
+
single_closed = mid_line = False
|
| 37 |
+
for c in task.closures:
|
| 38 |
+
if c.kind != "closed":
|
| 39 |
+
continue
|
| 40 |
+
lo, hi = sorted((net.index[c.where[0]], net.index[c.where[-1]]))
|
| 41 |
+
if any(net.sections[k].single for k in range(lo, hi)):
|
| 42 |
+
single_closed = True
|
| 43 |
+
west = any(p.turn for p in net.points[:lo + 1])
|
| 44 |
+
east = any(p.turn for p in net.points[hi:])
|
| 45 |
+
if lo > 0 and hi < len(net.points) - 1 and west and east:
|
| 46 |
+
mid_line = True
|
| 47 |
+
greedy = max(ref.get("greedy_reward", 0.0), ref.get("greedy_fast_reward", 0.0))
|
| 48 |
+
return {
|
| 49 |
+
"runs": len(task.runs), "sets": len(task.units), "stops": sum(r.n_stops for r in task.runs),
|
| 50 |
+
"conflicts": len(naive.violations), "single_track": rules.get("single_track", 0),
|
| 51 |
+
"headway": rules.get("headway", 0), "platforms": rules.get("platforms", 0), "closed": rules.get("closed", 0),
|
| 52 |
+
"decisions": holds + shorts + cancels, "holds": holds, "short_turns": shorts, "cancels": cancels,
|
| 53 |
+
"turn_points": len(turn_pts), "avoidable": ref.get("optimal_cost", 0) - ref.get("floor_cost", 0),
|
| 54 |
+
"optimal": ref.get("optimal_cost", 0), "greedy_gap": round(1.0 - greedy, 4),
|
| 55 |
+
"closed_min": sum(clip(c) for c in task.closures if c.kind == "closed"),
|
| 56 |
+
"slow_min": sum(clip(c) for c in task.closures if c.kind == "slow"),
|
| 57 |
+
"track_min": sum(clip(c) for c in task.closures if c.kind == "track"),
|
| 58 |
+
"single_closed": single_closed, "mid_line": mid_line, "late_sets": sum("is held for" in n for n in task.notices),
|
| 59 |
+
}
|
core/rail_core/model.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tasks and plans.
|
| 2 |
+
|
| 3 |
+
A task is a window of one real 2025 day on the Groningen - Leeuwarden line after something went wrong. The agent is
|
| 4 |
+
the dispatcher: every train (a *run* from one end of the line to the other) runs by default exactly as scheduled and
|
| 5 |
+
as early as the physics allow (never ahead of its timetable). The plan says only what to change:
|
| 6 |
+
|
| 7 |
+
hold keep a train at a station until a minute (to let an opposing train through a single-track section,
|
| 8 |
+
or to wait for a section to reopen)
|
| 9 |
+
to short-turn: end the run early at a turn point, so the same train can start back from there
|
| 10 |
+
from start a run late at a turn point (where its train was short-turned)
|
| 11 |
+
cancel do not run it at all (its train stays where it is)
|
| 12 |
+
|
| 13 |
+
The physics, given a plan, are deterministic (`check.simulate`): a train leaves each point at the latest of when it
|
| 14 |
+
is ready, its scheduled departure (never early at a scheduled stop) and its hold; it runs at its minimum running
|
| 15 |
+
time. A train held where it has no scheduled stop loses `stop_loss` minutes braking and `stop_loss` accelerating.
|
| 16 |
+
Whether the result is legal (single-track and headway conflicts, platform tracks, closed sections, where each train
|
| 17 |
+
ends up) is `check.evaluate`.
|
| 18 |
+
|
| 19 |
+
Times are whole minutes after 00:00 of the service day (00:30 the next night is 24:30, minute 1470); plans may give
|
| 20 |
+
them as integers or "HH:MM".
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
from __future__ import annotations
|
| 24 |
+
|
| 25 |
+
import json
|
| 26 |
+
import re
|
| 27 |
+
from dataclasses import asdict, dataclass, field
|
| 28 |
+
from typing import Any
|
| 29 |
+
|
| 30 |
+
RULES = {
|
| 31 |
+
"h_follow": 3, # minutes between two trains going the same way, at both ends of a section
|
| 32 |
+
"h_opp": 0, # minutes after a train leaves a single-track section before an opposing train may enter it
|
| 33 |
+
# (0: in the same minute, as the timetable's crossings are planned)
|
| 34 |
+
"turn_min": 4, # minutes for a train to turn back (driver changes ends)
|
| 35 |
+
"stop_loss": 1, # minutes lost braking, and again accelerating, at a stop that is not in the timetable
|
| 36 |
+
"cancel_cost": 30, # per scheduled stop a train does not serve (the next train of its kind is 30 minutes later)
|
| 37 |
+
"max_delay": 120, # no train may reach a stop more than this many minutes late
|
| 38 |
+
"gap_k": 100, # reward: added to the avoidable cost (about three unserved stops)
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@dataclass
|
| 43 |
+
class Run:
|
| 44 |
+
"""One scheduled train from one end of its route to the other."""
|
| 45 |
+
id: str # train number
|
| 46 |
+
kind: str # stop (Stoptrein: every station) | fast (Sneltrein)
|
| 47 |
+
route: list[str] # every point from origin to destination in travel order, junctions included
|
| 48 |
+
stops: list[bool] # scheduled stop at route[i]
|
| 49 |
+
arr: list[int | None] # scheduled arrival at a stop, the timetable's passing minute elsewhere; None at origin
|
| 50 |
+
dep: list[int | None] # scheduled departure at a stop, the passing minute elsewhere; None at destination
|
| 51 |
+
seg: list[int] # minimum running minutes route[i] -> route[i + 1] for this train's stopping pattern
|
| 52 |
+
unit: str # the train (rolling-stock unit) that runs it
|
| 53 |
+
fixed_to: int = 0 # in progress: route index of the point it is at or approaching when the task starts
|
| 54 |
+
fixed_arr: int | None = None # in progress: its arrival there (cannot change)
|
| 55 |
+
fixed_from: int | None = None # in progress and between points: the minute it left route[fixed_to - 1]
|
| 56 |
+
stuck: bool = False # in progress inside a section that closed: it waits there for the reopening
|
| 57 |
+
|
| 58 |
+
@property
|
| 59 |
+
def origin(self) -> str:
|
| 60 |
+
return self.route[0]
|
| 61 |
+
|
| 62 |
+
@property
|
| 63 |
+
def dest(self) -> str:
|
| 64 |
+
return self.route[-1]
|
| 65 |
+
|
| 66 |
+
@property
|
| 67 |
+
def started(self) -> bool:
|
| 68 |
+
return self.fixed_arr is not None
|
| 69 |
+
|
| 70 |
+
@property
|
| 71 |
+
def n_stops(self) -> int:
|
| 72 |
+
return sum(self.stops)
|
| 73 |
+
|
| 74 |
+
def idx(self, code: str) -> int:
|
| 75 |
+
return self.route.index(code)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
@dataclass
|
| 79 |
+
class Unit:
|
| 80 |
+
"""A train set and the runs it works through the window, in order."""
|
| 81 |
+
id: str
|
| 82 |
+
runs: list[str]
|
| 83 |
+
at: str | None # where it stands before its first run (None: its first run is already moving)
|
| 84 |
+
ready: int | None # when it may leave `at`
|
| 85 |
+
after: dict | None = None # its first run after the window: {"run", "station", "dep", "stops"}
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
@dataclass
|
| 89 |
+
class Closure:
|
| 90 |
+
kind: str # closed (no train may be in the sections) | slow (+extra minutes) | track (one
|
| 91 |
+
# platform track out of use at a station)
|
| 92 |
+
where: list[str] # closed/slow: [a, b], every section between points a and b; track: [station]
|
| 93 |
+
start: int
|
| 94 |
+
end: int # exclusive
|
| 95 |
+
extra: int = 0
|
| 96 |
+
cause: str = ""
|
| 97 |
+
real: bool = False # from the 2025 disruption record (else generated)
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
@dataclass
|
| 101 |
+
class Task:
|
| 102 |
+
task_id: str
|
| 103 |
+
split: str
|
| 104 |
+
line: str
|
| 105 |
+
day: str
|
| 106 |
+
start: int # "now": the minute the agent takes over
|
| 107 |
+
end: int # runs leaving their origin before this minute are in the task
|
| 108 |
+
network: dict
|
| 109 |
+
runs: list[Run]
|
| 110 |
+
units: list[Unit]
|
| 111 |
+
closures: list[Closure]
|
| 112 |
+
notices: list[str]
|
| 113 |
+
disruptions: list[dict]
|
| 114 |
+
difficulty: str
|
| 115 |
+
reference: dict = field(default_factory=dict) # optimum, floor, baselines, the real outcome: never shown
|
| 116 |
+
rules: dict = field(default_factory=lambda: dict(RULES))
|
| 117 |
+
|
| 118 |
+
def to_dict(self, public: bool = False) -> dict:
|
| 119 |
+
d = asdict(self)
|
| 120 |
+
if public:
|
| 121 |
+
d.pop("reference")
|
| 122 |
+
return d
|
| 123 |
+
|
| 124 |
+
@classmethod
|
| 125 |
+
def from_dict(cls, d: dict) -> "Task":
|
| 126 |
+
d = dict(d)
|
| 127 |
+
d["runs"] = [Run(**r) for r in d["runs"]]
|
| 128 |
+
d["units"] = [Unit(**u) for u in d["units"]]
|
| 129 |
+
d["closures"] = [Closure(**c) for c in d["closures"]]
|
| 130 |
+
d.setdefault("reference", {})
|
| 131 |
+
d["rules"] = {**RULES, **(d.get("rules") or {})}
|
| 132 |
+
return cls(**d)
|
| 133 |
+
|
| 134 |
+
def run(self, rid: str) -> Run:
|
| 135 |
+
return self.run_map[rid]
|
| 136 |
+
|
| 137 |
+
@property
|
| 138 |
+
def run_map(self) -> dict[str, Run]:
|
| 139 |
+
m = self.__dict__.get("_run_map")
|
| 140 |
+
if m is None:
|
| 141 |
+
m = self.__dict__["_run_map"] = {r.id: r for r in self.runs}
|
| 142 |
+
return m
|
| 143 |
+
|
| 144 |
+
def rule(self, k: str) -> int:
|
| 145 |
+
return int(self.rules.get(k, RULES[k]))
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
# ---------------------------------------------------------------- plans
|
| 149 |
+
|
| 150 |
+
@dataclass
|
| 151 |
+
class Order:
|
| 152 |
+
"""What the plan changes for one run (absent from the plan: runs in full, as early as possible)."""
|
| 153 |
+
cancel: bool = False
|
| 154 |
+
start: str | None = None # "from": start at this turn point instead of the origin
|
| 155 |
+
end: str | None = None # "to": end at this turn point instead of the destination
|
| 156 |
+
hold: dict[str, int] = field(default_factory=dict) # point -> do not leave before this minute
|
| 157 |
+
|
| 158 |
+
def to_json(self, rid: str) -> dict:
|
| 159 |
+
d: dict[str, Any] = {"train": rid}
|
| 160 |
+
if self.cancel:
|
| 161 |
+
d["cancel"] = True
|
| 162 |
+
return d
|
| 163 |
+
if self.start:
|
| 164 |
+
d["from"] = self.start
|
| 165 |
+
if self.end:
|
| 166 |
+
d["to"] = self.end
|
| 167 |
+
if self.hold:
|
| 168 |
+
d["hold"] = {k: hhmm(v) for k, v in self.hold.items()}
|
| 169 |
+
return d
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
Plan = dict[str, Order]
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
class PlanError(ValueError):
|
| 176 |
+
"""The plan cannot be read at all."""
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def hhmm(m: int | None) -> str | None:
|
| 180 |
+
if m is None:
|
| 181 |
+
return None
|
| 182 |
+
return f"{m // 60:02d}:{m % 60:02d}"
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
_TIME = re.compile(r"^\s*(\d{1,2}):(\d{2})\s*$")
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def parse_time(v: Any) -> int:
|
| 189 |
+
if isinstance(v, bool):
|
| 190 |
+
raise ValueError(f"not a time: {v!r}")
|
| 191 |
+
if isinstance(v, int):
|
| 192 |
+
m = v
|
| 193 |
+
elif isinstance(v, float):
|
| 194 |
+
if v != v or v in (float("inf"), float("-inf")) or v != int(v):
|
| 195 |
+
raise ValueError(f"not a whole minute: {v!r}")
|
| 196 |
+
m = int(v)
|
| 197 |
+
elif isinstance(v, str):
|
| 198 |
+
s = v.strip()
|
| 199 |
+
if re.fullmatch(r"-?\d+", s):
|
| 200 |
+
m = int(s)
|
| 201 |
+
else:
|
| 202 |
+
g = _TIME.match(s)
|
| 203 |
+
if not g:
|
| 204 |
+
raise ValueError(f'not a time: {v!r} (use "HH:MM" or minutes after midnight)')
|
| 205 |
+
h, mi = int(g.group(1)), int(g.group(2))
|
| 206 |
+
if mi > 59:
|
| 207 |
+
raise ValueError(f"not a time: {v!r}")
|
| 208 |
+
m = h * 60 + mi
|
| 209 |
+
else:
|
| 210 |
+
raise ValueError(f"not a time: {v!r}")
|
| 211 |
+
if not 0 <= m <= 48 * 60:
|
| 212 |
+
raise ValueError(f"time out of range: {v!r}")
|
| 213 |
+
return m
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def _load(raw: Any) -> Any:
|
| 217 |
+
if isinstance(raw, str):
|
| 218 |
+
s = raw.strip()
|
| 219 |
+
fence = re.search(r"```(?:json)?\s*(.*?)```", s, re.S)
|
| 220 |
+
if fence:
|
| 221 |
+
s = fence.group(1).strip()
|
| 222 |
+
if not s.startswith(("[", "{")):
|
| 223 |
+
i = min([k for k in (s.find("["), s.find("{")) if k >= 0], default=-1)
|
| 224 |
+
if i < 0:
|
| 225 |
+
raise PlanError("the plan is not JSON: give a list like "
|
| 226 |
+
'[{"train": "37413", "hold": {"HDG": "07:31"}}, {"train": "37324", "cancel": true}]')
|
| 227 |
+
s = s[i:]
|
| 228 |
+
try:
|
| 229 |
+
raw, _ = json.JSONDecoder().raw_decode(s)
|
| 230 |
+
except json.JSONDecodeError as e:
|
| 231 |
+
raise PlanError(f"the plan is not valid JSON: {e}") from None
|
| 232 |
+
if isinstance(raw, dict):
|
| 233 |
+
if "plan" in raw and isinstance(raw["plan"], (list, dict)):
|
| 234 |
+
return _load(raw["plan"])
|
| 235 |
+
# {"37413": {...}, ...}
|
| 236 |
+
raw = [{"train": k, **(v if isinstance(v, dict) else {"cancel": v} if isinstance(v, bool) else {"bad": v})}
|
| 237 |
+
for k, v in raw.items()]
|
| 238 |
+
if not isinstance(raw, list):
|
| 239 |
+
raise PlanError("the plan must be a list of entries, one per train you change")
|
| 240 |
+
return raw
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
def parse_plan(task: Task, raw: Any) -> tuple[Plan, list[str]]:
|
| 244 |
+
"""(plan, entry problems). Raises PlanError only when nothing can be read. An empty list is a valid plan:
|
| 245 |
+
every train runs in full, as early as it can."""
|
| 246 |
+
entries = _load(raw)
|
| 247 |
+
runs = task.run_map
|
| 248 |
+
plan: Plan = {}
|
| 249 |
+
problems: list[str] = []
|
| 250 |
+
for n, e in enumerate(entries):
|
| 251 |
+
if not isinstance(e, dict):
|
| 252 |
+
problems.append(f"entry {n}: not an object")
|
| 253 |
+
continue
|
| 254 |
+
rid = e.get("train", e.get("run", e.get("id")))
|
| 255 |
+
if isinstance(rid, bool) or rid is None:
|
| 256 |
+
problems.append(f"entry {n}: no train number")
|
| 257 |
+
continue
|
| 258 |
+
rid = str(rid).strip()
|
| 259 |
+
if rid not in runs:
|
| 260 |
+
problems.append(f"entry {n}: unknown train {rid!r}")
|
| 261 |
+
continue
|
| 262 |
+
if rid in plan:
|
| 263 |
+
problems.append(f"entry {n}: train {rid} appears twice")
|
| 264 |
+
continue
|
| 265 |
+
r = runs[rid]
|
| 266 |
+
o = Order()
|
| 267 |
+
unknown = set(e) - {"train", "run", "id", "cancel", "from", "start", "to", "end", "hold", "note", "reason"}
|
| 268 |
+
if unknown:
|
| 269 |
+
problems.append(f"train {rid}: unknown field(s) {sorted(unknown)}")
|
| 270 |
+
c = e.get("cancel", False)
|
| 271 |
+
if c not in (True, False, None):
|
| 272 |
+
problems.append(f"train {rid}: cancel must be true or false")
|
| 273 |
+
o.cancel = c is True
|
| 274 |
+
for key, alt, attr in (("from", "start", "start"), ("to", "end", "end")):
|
| 275 |
+
v = e.get(key, e.get(alt))
|
| 276 |
+
if v is None:
|
| 277 |
+
continue
|
| 278 |
+
if not isinstance(v, str) or v.strip().upper() not in r.route:
|
| 279 |
+
problems.append(f"train {rid}: {key} {v!r} is not on its route {'-'.join(r.route)}")
|
| 280 |
+
continue
|
| 281 |
+
setattr(o, attr, v.strip().upper())
|
| 282 |
+
h = e.get("hold") or {}
|
| 283 |
+
if not isinstance(h, dict):
|
| 284 |
+
problems.append(f'train {rid}: hold must be an object like {{"HDG": "07:31"}}')
|
| 285 |
+
h = {}
|
| 286 |
+
for k, v in h.items():
|
| 287 |
+
code = str(k).strip().upper()
|
| 288 |
+
if code not in r.route:
|
| 289 |
+
problems.append(f"train {rid}: hold at {k!r}, which is not on its route")
|
| 290 |
+
continue
|
| 291 |
+
try:
|
| 292 |
+
o.hold[code] = parse_time(v)
|
| 293 |
+
except ValueError as ex:
|
| 294 |
+
problems.append(f"train {rid}: hold at {code}: {ex}")
|
| 295 |
+
plan[rid] = o
|
| 296 |
+
return plan, problems
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
def plan_to_list(plan: Plan) -> list[dict]:
|
| 300 |
+
return [o.to_json(rid) for rid, o in plan.items()]
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
def plan_from_list(task: Task, entries: list[dict]) -> Plan:
|
| 304 |
+
plan, problems = parse_plan(task, entries)
|
| 305 |
+
if problems:
|
| 306 |
+
raise PlanError("; ".join(problems))
|
| 307 |
+
return plan
|
core/rail_core/network.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""The line: points west to east (terminals, stations, halts, junctions) and the sections between them.
|
| 2 |
+
|
| 3 |
+
A section joins two neighbouring points and is single or double track. A point has a number of tracks (None: as many
|
| 4 |
+
as needed, the terminals), may be *directional* (a station on double track: one track per direction, so two trains
|
| 5 |
+
going the same way cannot both stand there and nothing overtakes), and may be a *turn* point where a train can be
|
| 6 |
+
short-turned. Junctions are where double track becomes single between two stations; trains pass them but never stop.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import json
|
| 12 |
+
from dataclasses import asdict, dataclass
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
|
| 15 |
+
DATA = Path(__file__).resolve().parents[4] / "data"
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@dataclass
|
| 19 |
+
class Point:
|
| 20 |
+
code: str
|
| 21 |
+
name: str
|
| 22 |
+
km: float
|
| 23 |
+
kind: str # terminal | station | halt | junction
|
| 24 |
+
tracks: int | None # None: unlimited (terminal)
|
| 25 |
+
directional: bool # one track per direction (station on double track)
|
| 26 |
+
turn: bool # trains may be short-turned here
|
| 27 |
+
|
| 28 |
+
@property
|
| 29 |
+
def stoppable(self) -> bool:
|
| 30 |
+
return self.kind != "junction"
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@dataclass
|
| 34 |
+
class Section:
|
| 35 |
+
a: str # west end
|
| 36 |
+
b: str # east end
|
| 37 |
+
single: bool
|
| 38 |
+
prorail: str = ""
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class Network:
|
| 42 |
+
def __init__(self, points: list[Point], sections: list[Section], line: str = "", name: str = ""):
|
| 43 |
+
self.line, self.name = line, name
|
| 44 |
+
self.points = points
|
| 45 |
+
self.sections = sections
|
| 46 |
+
self.index = {p.code: i for i, p in enumerate(points)}
|
| 47 |
+
self.by_code = {p.code: p for p in points}
|
| 48 |
+
for i, s in enumerate(sections):
|
| 49 |
+
if (self.index[s.a], self.index[s.b]) != (i, i + 1):
|
| 50 |
+
raise ValueError(f"section {s.a}-{s.b} is not between neighbouring points {i} and {i + 1}")
|
| 51 |
+
|
| 52 |
+
@classmethod
|
| 53 |
+
def from_dict(cls, d: dict) -> "Network":
|
| 54 |
+
return cls([Point(**p) for p in d["points"]], [Section(**s) for s in d["sections"]], d.get("line", ""),
|
| 55 |
+
d.get("name", ""))
|
| 56 |
+
|
| 57 |
+
def to_dict(self) -> dict:
|
| 58 |
+
return {"line": self.line, "name": self.name, "points": [asdict(p) for p in self.points],
|
| 59 |
+
"sections": [asdict(s) for s in self.sections]}
|
| 60 |
+
|
| 61 |
+
def section(self, x: str, y: str) -> int:
|
| 62 |
+
"""Index of the section between neighbouring points x and y (either order)."""
|
| 63 |
+
i, j = self.index[x], self.index[y]
|
| 64 |
+
if abs(i - j) != 1:
|
| 65 |
+
raise ValueError(f"{x} and {y} are not neighbours")
|
| 66 |
+
return min(i, j)
|
| 67 |
+
|
| 68 |
+
def route(self, a: str, b: str) -> list[str]:
|
| 69 |
+
"""Every point from a to b inclusive, in travel order."""
|
| 70 |
+
i, j = self.index[a], self.index[b]
|
| 71 |
+
step = 1 if j >= i else -1
|
| 72 |
+
return [self.points[k].code for k in range(i, j + step, step)]
|
| 73 |
+
|
| 74 |
+
def direction(self, a: str, b: str) -> int:
|
| 75 |
+
"""+1 eastbound (towards the last point), -1 westbound."""
|
| 76 |
+
return 1 if self.index[b] > self.index[a] else -1
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def load_network(line: str = "gnlw") -> Network:
|
| 80 |
+
return Network.from_dict(json.loads((DATA / line / "network.json").read_text()))
|
core/rail_core/ops.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Disruptions and tiers: what can go wrong on the line, from the 2025 record, and how tasks are drawn.
|
| 2 |
+
|
| 3 |
+
A disruption in the Rijden de Treinen record has a cause, a start, an end and the stations it was linked to. Its
|
| 4 |
+
effect in the model depends on the cause (an assumption, stated in SOURCE.md):
|
| 5 |
+
|
| 6 |
+
broken down train, collision, emergency call, defective track, person or animal on the track, power failure
|
| 7 |
+
-> the sections between the outermost stations named are closed for the whole disruption
|
| 8 |
+
signal failure, level crossing failure
|
| 9 |
+
-> trains run on sight there: every section in the span takes `SLOW_EXTRA` minutes longer
|
| 10 |
+
points failure
|
| 11 |
+
-> one track out of use at the passing station(s) in the span (no crossing there if it had two)
|
| 12 |
+
|
| 13 |
+
A disruption linked to every station of the line has no usable location; such causes are replayed on a section
|
| 14 |
+
drawn with the task's seed.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
import random
|
| 20 |
+
|
| 21 |
+
from .model import Closure
|
| 22 |
+
from .network import Network
|
| 23 |
+
|
| 24 |
+
SLOW_EXTRA = 4
|
| 25 |
+
CLOSE = ("broken down", "collision", "emergency", "defective railway track", "person", "animal", "power failure",
|
| 26 |
+
"damaged", "fire", "accident", "vandalism", "copper theft")
|
| 27 |
+
SLOW = ("signal", "level crossing")
|
| 28 |
+
POINTS = ("points",)
|
| 29 |
+
LOOPS = ("HDG", "FWD", "BP", "ZH")
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def effect(cause: str) -> str:
|
| 33 |
+
c = cause.lower()
|
| 34 |
+
if any(k in c for k in POINTS) and not any(k in c for k in SLOW):
|
| 35 |
+
return "track"
|
| 36 |
+
if any(k in c for k in SLOW):
|
| 37 |
+
return "slow"
|
| 38 |
+
return "closed"
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def span(net: Network, codes: list[str]) -> list[str] | None:
|
| 42 |
+
"""[west, east] of the stations named, or None when the record names (nearly) the whole line."""
|
| 43 |
+
idx = sorted(net.index[c] for c in codes if c in net.index)
|
| 44 |
+
if len(idx) < 2 or (idx[0] == 0 and idx[-1] == len(net.points) - 1 and len(idx) >= 8):
|
| 45 |
+
return None
|
| 46 |
+
return [net.points[idx[0]].code, net.points[idx[-1]].code]
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def closures_for(net: Network, cause: str, codes: list[str], start: int, end: int, rng: random.Random,
|
| 50 |
+
real: bool = True) -> list[Closure]:
|
| 51 |
+
kind = effect(cause)
|
| 52 |
+
where = span(net, codes)
|
| 53 |
+
if where is None: # no location in the record: draw one
|
| 54 |
+
where = _draw_span(net, kind, rng)
|
| 55 |
+
if kind == "track":
|
| 56 |
+
lo, hi = net.index[where[0]], net.index[where[-1]]
|
| 57 |
+
loops = [p.code for p in net.points[lo:hi + 1] if p.code in LOOPS] or [rng.choice(LOOPS)]
|
| 58 |
+
return [Closure("track", [c], start, end, cause=cause, real=real) for c in loops[:1]]
|
| 59 |
+
if kind == "slow":
|
| 60 |
+
return [Closure("slow", where, start, end, extra=SLOW_EXTRA, cause=cause, real=real)]
|
| 61 |
+
return [Closure("closed", where, start, end, cause=cause, real=real)]
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def _draw_span(net: Network, kind: str, rng: random.Random) -> list[str]:
|
| 65 |
+
stations = [p.code for p in net.points if p.kind != "junction"]
|
| 66 |
+
if kind == "track":
|
| 67 |
+
c = rng.choice(LOOPS)
|
| 68 |
+
return [c, c]
|
| 69 |
+
i = rng.randrange(len(stations) - 1)
|
| 70 |
+
j = min(len(stations) - 1, i + rng.choice((1, 1, 2)))
|
| 71 |
+
return [stations[i], stations[j]]
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
# ---------------------------------------------------------------- tiers and candidates
|
| 75 |
+
|
| 76 |
+
import csv
|
| 77 |
+
from datetime import date as _date
|
| 78 |
+
from functools import lru_cache
|
| 79 |
+
|
| 80 |
+
from .data import LINE_DIR, days as _days, disruptions as _real_disruptions
|
| 81 |
+
from .model import hhmm
|
| 82 |
+
|
| 83 |
+
# Eval days lie in these ISO weeks of 2025 (42 days); train days are every other day of January-June.
|
| 84 |
+
HELD_OUT = {4, 7, 11, 17, 22, 25}
|
| 85 |
+
PEAKS = [(6 * 60 + 30, 9 * 60), (15 * 60 + 30, 18 * 60 + 30)]
|
| 86 |
+
LATE_CAUSES = ("a technical inspection", "a late driver", "a door fault being fixed", "a cleaning crew overrun",
|
| 87 |
+
"coupling problems")
|
| 88 |
+
|
| 89 |
+
TIERS = {
|
| 90 |
+
# window minutes; what is seeded (each disruption's cause and duration drawn from the 2024-2025 record)
|
| 91 |
+
"real": dict(window=(150, 210)), # the record's own disruption, on its own day
|
| 92 |
+
"blockage": dict(window=(150, 210), closed=1, late=(0, 1)),
|
| 93 |
+
"loop": dict(window=(150, 210), track=1, slow=(0, 1), late=(0, 1)),
|
| 94 |
+
"compound": dict(window=(180, 240), closed=1, track=(0, 1), slow=(0, 1), late=(1, 3)),
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def split_of(day: str) -> str:
|
| 99 |
+
return "eval" if _date.fromisoformat(day).isocalendar()[1] in HELD_OUT else "train"
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def split_days(split: str) -> list[str]:
|
| 103 |
+
return [d for d in _days() if split_of(d) == split]
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
@lru_cache(maxsize=1)
|
| 107 |
+
def profiles() -> list[dict]:
|
| 108 |
+
with open(LINE_DIR / "disruption_profiles.csv", encoding="utf-8") as f:
|
| 109 |
+
rows = [r for r in csv.DictReader(f) if r["duration_minutes"]]
|
| 110 |
+
for r in rows:
|
| 111 |
+
r["effect"] = effect(r["cause_en"])
|
| 112 |
+
r["minutes"] = max(10, min(240, round(float(r["duration_minutes"]))))
|
| 113 |
+
return rows
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def real_disruptions(split: str | None = None) -> list[dict]:
|
| 117 |
+
"""The record's own disruptions on the line, as (day, start, end, cause, stations)."""
|
| 118 |
+
out = []
|
| 119 |
+
for d in _real_disruptions():
|
| 120 |
+
day = d["start_time"][:10]
|
| 121 |
+
if split and split_of(day) != split:
|
| 122 |
+
continue
|
| 123 |
+
hh, mm = int(d["start_time"][11:13]), int(d["start_time"][14:16])
|
| 124 |
+
start = hh * 60 + mm
|
| 125 |
+
dur = round(float(d["duration_minutes"] or 0))
|
| 126 |
+
if dur < 15 or not 5 * 60 <= start <= 22 * 60:
|
| 127 |
+
continue
|
| 128 |
+
out.append({"day": day, "start": start, "end": start + dur, "cause": d["cause_en"],
|
| 129 |
+
"stations": [c.strip() for c in d["rdt_station_codes"].split(",")]})
|
| 130 |
+
return out
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def draw(rng: random.Random, kind: str) -> dict:
|
| 134 |
+
pool = [p for p in profiles() if p["effect"] == kind] or profiles()
|
| 135 |
+
return rng.choice(pool)
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def seeded_closures(net: Network, tier: str, start: int, window: int, rng: random.Random) -> tuple[list, list[str]]:
|
| 139 |
+
"""Closures for a seeded tier, starting within the first half hour of the window."""
|
| 140 |
+
r = TIERS[tier]
|
| 141 |
+
out: list[Closure] = []
|
| 142 |
+
|
| 143 |
+
def count(key):
|
| 144 |
+
v = r.get(key, 0)
|
| 145 |
+
return rng.randint(*v) if isinstance(v, tuple) else v
|
| 146 |
+
|
| 147 |
+
for kind, key in (("closed", "closed"), ("track", "track"), ("slow", "slow")):
|
| 148 |
+
for _ in range(count(key)):
|
| 149 |
+
p = draw(rng, kind)
|
| 150 |
+
t0 = start + rng.choice((0, 0, 5, 10, 20, 30))
|
| 151 |
+
minutes = min(p["minutes"], window - 20) if kind == "closed" else p["minutes"]
|
| 152 |
+
codes = [c.strip() for c in p["rdt_station_codes"].split(",")]
|
| 153 |
+
where = span(net, codes) or _draw_span(net, kind, rng)
|
| 154 |
+
out += closures_for(net, p["cause_en"], where, t0, t0 + minutes, rng, real=False)
|
| 155 |
+
notes = []
|
| 156 |
+
return out, notes
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def candidate_id(day: str, start: int, window: int, tier: str, seed: int) -> str:
|
| 160 |
+
return f"gnlw-{day}-{hhmm(start).replace(':', '')}-{window}m-{tier}-{seed}"
|
core/rail_core/pack.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Task packs: directories of tasks (tasks.jsonl or tasks.jsonl.gz, with their reference plans) plus a manifest.
|
| 2 |
+
|
| 3 |
+
The server serves several packs at once; splits come from each task's `split` field. By default those are the
|
| 4 |
+
rail-v1 eval and train packs.
|
| 5 |
+
|
| 6 |
+
RAIL_TASKS_DIR=/path/a:/path/b colon-separated pack directories
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import gzip
|
| 12 |
+
import hashlib
|
| 13 |
+
import json
|
| 14 |
+
import os
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
|
| 17 |
+
from .model import Task
|
| 18 |
+
|
| 19 |
+
TASKS_ROOT = Path(__file__).resolve().parents[2] / "tasks"
|
| 20 |
+
DEFAULT_PACKS = ["rail-v1-eval", "rail-v1-train"]
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def default_roots() -> list[Path]:
|
| 24 |
+
env = os.environ.get("RAIL_TASKS_DIR", "").strip()
|
| 25 |
+
if env:
|
| 26 |
+
return [Path(p) for p in env.split(":") if p]
|
| 27 |
+
return [TASKS_ROOT / p for p in DEFAULT_PACKS if _tasks_file(TASKS_ROOT / p)]
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _tasks_file(root: Path) -> Path | None:
|
| 31 |
+
for name in ("tasks.jsonl", "tasks.jsonl.gz"):
|
| 32 |
+
if (root / name).is_file():
|
| 33 |
+
return root / name
|
| 34 |
+
return None
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _read(root: Path) -> tuple[list[Task], dict]:
|
| 38 |
+
path = _tasks_file(root)
|
| 39 |
+
if path is None:
|
| 40 |
+
raise FileNotFoundError(f"no tasks.jsonl(.gz) in {root}")
|
| 41 |
+
raw = path.read_bytes()
|
| 42 |
+
body = gzip.decompress(raw) if path.suffix == ".gz" else raw
|
| 43 |
+
manifest = json.loads((root / "manifest.json").read_text()) if (root / "manifest.json").is_file() else {}
|
| 44 |
+
digest = hashlib.sha256(body).hexdigest()
|
| 45 |
+
if manifest.get("sha256") and manifest["sha256"] != digest:
|
| 46 |
+
raise ValueError(f"{path} does not match its manifest.json (sha256 {digest[:12]} != {manifest['sha256'][:12]})")
|
| 47 |
+
return [Task.from_dict(json.loads(line)) for line in body.decode().splitlines() if line.strip()], manifest
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class TaskPack:
|
| 51 |
+
def __init__(self, roots: Path | str | list[Path | str] | None = None):
|
| 52 |
+
if roots is None:
|
| 53 |
+
roots = default_roots()
|
| 54 |
+
if isinstance(roots, (str, Path)):
|
| 55 |
+
roots = [roots]
|
| 56 |
+
self.roots = [Path(r) for r in roots]
|
| 57 |
+
if not self.roots:
|
| 58 |
+
raise FileNotFoundError(f"no task packs found under {TASKS_ROOT}; set RAIL_TASKS_DIR")
|
| 59 |
+
self.tasks: list[Task] = []
|
| 60 |
+
self.manifests: dict[str, dict] = {}
|
| 61 |
+
for r in self.roots:
|
| 62 |
+
tasks, manifest = _read(r)
|
| 63 |
+
self.tasks += tasks
|
| 64 |
+
self.manifests[r.name] = manifest
|
| 65 |
+
self._by_id: dict[str, Task] = {}
|
| 66 |
+
for t in self.tasks:
|
| 67 |
+
if t.task_id in self._by_id:
|
| 68 |
+
raise ValueError(f"task id {t.task_id} appears in more than one pack")
|
| 69 |
+
self._by_id[t.task_id] = t
|
| 70 |
+
self._by_split: dict[str, list[Task]] = {}
|
| 71 |
+
for t in self.tasks:
|
| 72 |
+
self._by_split.setdefault(t.split, []).append(t)
|
| 73 |
+
|
| 74 |
+
def splits(self) -> list[str]:
|
| 75 |
+
return sorted(self._by_split)
|
| 76 |
+
|
| 77 |
+
def count(self, split: str) -> int:
|
| 78 |
+
return len(self._by_split.get(split, []))
|
| 79 |
+
|
| 80 |
+
def at(self, split: str, index: int) -> Task:
|
| 81 |
+
return self._by_split[split][index]
|
| 82 |
+
|
| 83 |
+
def get(self, task_id: str) -> Task:
|
| 84 |
+
return self._by_id[task_id]
|
| 85 |
+
|
| 86 |
+
def public(self, task: Task) -> dict:
|
| 87 |
+
"""What anyone may see: everything but the reference plans and costs."""
|
| 88 |
+
return task.to_dict(public=True)
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
_PACK: TaskPack | None = None
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def load_pack(root: Path | str | list | None = None) -> TaskPack:
|
| 95 |
+
global _PACK
|
| 96 |
+
if root is not None:
|
| 97 |
+
return TaskPack(root)
|
| 98 |
+
if _PACK is None:
|
| 99 |
+
_PACK = TaskPack()
|
| 100 |
+
return _PACK
|
core/rail_core/prompts.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""What the agent reads: `rules(task)` is the system prompt, `situation(task)` what get_situation returns.
|
| 2 |
+
|
| 3 |
+
Both are rendered at runtime from the task, so changing them never needs a pack rebuild.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
from .check import network
|
| 9 |
+
from .model import Run, Task, hhmm
|
| 10 |
+
|
| 11 |
+
PROMPT_VERSION = "rail-v1.0"
|
| 12 |
+
KIND_NAME = {"stop": "Stoptrein (stopping)", "fast": "Sneltrein (fast)"}
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def rules(task: Task, max_checks: int = 10) -> str:
|
| 16 |
+
R = task.rules
|
| 17 |
+
net = network(task)
|
| 18 |
+
turns = ", ".join(p.code for p in net.points if p.turn)
|
| 19 |
+
return f"""You are the train dispatcher (treindienstleider) for Arriva's line between Leeuwarden and Groningen in the \
|
| 20 |
+
Netherlands, on a real day in 2025. Long stretches of the line are single track: trains going opposite ways can only \
|
| 21 |
+
pass each other at stations with two tracks or on the double-track parts. Something has gone wrong and you must re-plan \
|
| 22 |
+
the trains for the rest of the window. Everything you need is in the situation; it is the whole truth (no surprises \
|
| 23 |
+
later).
|
| 24 |
+
|
| 25 |
+
How trains move (the physics; deterministic):
|
| 26 |
+
- Every train set works its runs in the order listed. A run starts where its train set is, no earlier than when the set \
|
| 27 |
+
is ready there and never before its scheduled departure.
|
| 28 |
+
- Unless your plan says otherwise, a train runs at its minimum running times and leaves as soon as it can, but it never \
|
| 29 |
+
leaves a scheduled stop before its scheduled departure and never reaches any point before its timetable minute there \
|
| 30 |
+
(the passing minutes marked * included): arrival at the next point = max(departure + running time, timetable minute). \
|
| 31 |
+
A late train makes up time this way.
|
| 32 |
+
- A train held where it has no scheduled stop (a fast train at a station it normally passes) loses \
|
| 33 |
+
{R['stop_loss']} minute braking and {R['stop_loss']} minute accelerating. Trains cannot be held at junctions (J1, J2).
|
| 34 |
+
- After a run ends, its train set is ready {R['turn_min']} minutes after arriving, at the point where the run ended.
|
| 35 |
+
|
| 36 |
+
Rules (a plan that breaks any of them is infeasible):
|
| 37 |
+
1. Single track: two trains going opposite ways may never be in a single-track section at the same time: the second \
|
| 38 |
+
may enter only when the first has left it (arrived at the far end){' - in that same minute at the earliest' if not R['h_opp'] else f" - {R['h_opp']} minute(s) later at the earliest"}. \
|
| 39 |
+
A train is in a section from its departure at one end to its arrival at the other.
|
| 40 |
+
2. Headway: trains going the same way stay at least {R['h_follow']} minutes apart at both ends of every section, so a \
|
| 41 |
+
train can only overtake another at a station where both fit.
|
| 42 |
+
3. Platforms: never more trains standing at (or passing through) a station than it has tracks. A directional station \
|
| 43 |
+
(on double track) has one track each way. A train set waiting at a station between runs keeps a track there. \
|
| 44 |
+
Terminals (LW, GN) have room for everyone.
|
| 45 |
+
4. Closures: no train may be in a closed section while it is closed. A train stuck inside when it closed waits there.
|
| 46 |
+
5. Continuity: a run can only start where its train set is. You may short-turn trains only at the turn points \
|
| 47 |
+
({turns}). A train already running cannot be cancelled (short-turn it instead). At the end of the window every train \
|
| 48 |
+
set must be where its next run (listed) leaves from.
|
| 49 |
+
6. No train may reach a stop more than {R['max_delay']} minutes late.
|
| 50 |
+
|
| 51 |
+
What you decide, per train (run), in the plan:
|
| 52 |
+
- "hold": {{"HDG": "07:31"}} keeps it at that point until 07:31 (it leaves then, or later if it is not ready). Use \
|
| 53 |
+
holds to make trains wait for each other or for a closure to end.
|
| 54 |
+
- "to": "HDG" ends the run early at a turn point (short-turn); its train set is then at HDG, so its next run must \
|
| 55 |
+
start there.
|
| 56 |
+
- "from": "HDG" starts a run at a turn point instead of its origin (where its set was short-turned).
|
| 57 |
+
- "cancel": true does not run it at all; its train set stays where it is (so usually its next run is cancelled too, \
|
| 58 |
+
or starts from where the set is).
|
| 59 |
+
Trains you leave out of the plan run in full, as early as they can. An empty plan [] changes nothing.
|
| 60 |
+
|
| 61 |
+
Cost, in stop-minutes (lower is better): every minute a train reaches one of its scheduled stops late (arrival at each \
|
| 62 |
+
stop it serves after where it starts), plus {R['cancel_cost']} for every scheduled stop a train does not serve \
|
| 63 |
+
(cancelled or cut off by a short-turn: passengers wait for the next train), plus, for each train set, the minutes it is \
|
| 64 |
+
late for its next run after the window times that run's number of stops.
|
| 65 |
+
|
| 66 |
+
Tools:
|
| 67 |
+
- get_situation(): the line, the closures, the trains and the train sets.
|
| 68 |
+
- check_plan(plan): simulates your plan and returns every train's times, delay, the rule breaks and the cost. You \
|
| 69 |
+
have {max_checks} checks. It never grades the plan.
|
| 70 |
+
- submit_plan(plan): your final plan. It ends the episode and is graded once against the best possible plan (proved \
|
| 71 |
+
by a solver). A plan that breaks a rule scores below 0.2; a plan that breaks none scores 0.2-1.0 by how close its cost \
|
| 72 |
+
is to the best.
|
| 73 |
+
|
| 74 |
+
Plan format, a JSON list with one entry per train you change, for example:
|
| 75 |
+
[{{"train": "37413", "to": "HDG"}}, {{"train": "37424", "from": "HDG", "hold": {{"HDG": "07:31"}}}}, \
|
| 76 |
+
{{"train": "37326", "hold": {{"FWD": "07:55", "BP": "08:05"}}}}, {{"train": "37419", "cancel": true}}]
|
| 77 |
+
Times are "HH:MM" (after midnight: 24:10)."""
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _line_diagram(task: Task) -> list[str]:
|
| 81 |
+
net = network(task)
|
| 82 |
+
parts = []
|
| 83 |
+
for i, p in enumerate(net.points):
|
| 84 |
+
if p.kind == "terminal":
|
| 85 |
+
tag = f"{p.code}[terminal]"
|
| 86 |
+
elif p.kind == "junction":
|
| 87 |
+
tag = f"{p.code}"
|
| 88 |
+
elif p.directional:
|
| 89 |
+
tag = f"{p.code}[1 each way]"
|
| 90 |
+
else:
|
| 91 |
+
tag = f"{p.code}[{p.tracks}{' turn' if p.turn else ''}]"
|
| 92 |
+
if p.turn and p.kind != "terminal" and "turn" not in tag:
|
| 93 |
+
tag = tag[:-1] + " turn]"
|
| 94 |
+
parts.append(tag)
|
| 95 |
+
if i < len(net.sections):
|
| 96 |
+
parts.append(" ---single--- " if net.sections[i].single else " ===double=== ")
|
| 97 |
+
lines = ["West to east (km from Leeuwarden in brackets):"]
|
| 98 |
+
lines.append(" " + ", ".join(f"{p.code} {p.name} ({p.km:g})" for p in net.points))
|
| 99 |
+
lines.append(" " + "".join(parts))
|
| 100 |
+
lines.append(" [n] = tracks at a station; [1 each way] = one track per direction (no overtaking); turn = trains "
|
| 101 |
+
"can be short-turned there; J1, J2 = junctions where double track becomes single (no stopping).")
|
| 102 |
+
return lines
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def _pattern(r: Run) -> tuple:
|
| 106 |
+
return (r.kind, tuple(r.route), tuple(r.stops), tuple(r.seg))
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def situation(task: Task) -> str:
|
| 110 |
+
net = network(task)
|
| 111 |
+
out = [f"Groningen - Leeuwarden, {task.day}. It is now {hhmm(task.start)}. You plan every run that leaves "
|
| 112 |
+
f"before {hhmm(task.end)} (and those already running)."]
|
| 113 |
+
out.append("")
|
| 114 |
+
out.append("THE LINE")
|
| 115 |
+
out += _line_diagram(task)
|
| 116 |
+
out.append("")
|
| 117 |
+
out.append("WHAT HAS HAPPENED")
|
| 118 |
+
for n in task.notices:
|
| 119 |
+
out.append(f"- {n}")
|
| 120 |
+
for c in task.closures:
|
| 121 |
+
where = "-".join(c.where) if c.kind != "track" else c.where[0]
|
| 122 |
+
if c.kind == "closed":
|
| 123 |
+
out.append(f"- CLOSED: every section between {c.where[0]} and {c.where[-1]}, {hhmm(c.start)} to "
|
| 124 |
+
f"{hhmm(c.end)} ({c.cause}).")
|
| 125 |
+
elif c.kind == "slow":
|
| 126 |
+
out.append(f"- SLOW: trains entering a section between {c.where[0]} and {c.where[-1]} from "
|
| 127 |
+
f"{hhmm(c.start)} to {hhmm(c.end)} take {c.extra} minutes longer ({c.cause}).")
|
| 128 |
+
else:
|
| 129 |
+
out.append(f"- TRACK OUT OF USE: one track at {where} cannot be used {hhmm(c.start)} to {hhmm(c.end)} "
|
| 130 |
+
f"({c.cause}).")
|
| 131 |
+
if not task.notices and not task.closures:
|
| 132 |
+
out.append("- Nothing beyond the delays below.")
|
| 133 |
+
out.append("")
|
| 134 |
+
out.append("MINIMUM RUNNING TIMES (minutes from each point to the next, for each stopping pattern)")
|
| 135 |
+
patterns: dict[tuple, list[str]] = {}
|
| 136 |
+
for r in task.runs:
|
| 137 |
+
patterns.setdefault(_pattern(r), []).append(r.id)
|
| 138 |
+
for k, (pat, ids) in enumerate(patterns.items()):
|
| 139 |
+
kind, route, stops, seg = pat
|
| 140 |
+
legs = " ".join(f"{route[i]}{'' if stops[i] else '*'} {seg[i]}" for i in range(len(seg))) + f" {route[-1]}"
|
| 141 |
+
out.append(f"P{k + 1} {KIND_NAME[kind]} {route[0]}->{route[-1]}: {legs}")
|
| 142 |
+
out.append(" (* = no scheduled stop there: the train passes; held there it loses "
|
| 143 |
+
f"{task.rule('stop_loss')}+{task.rule('stop_loss')} min)")
|
| 144 |
+
pid = {rid: f"P{k + 1}" for k, ids in enumerate(patterns.values()) for rid in ids}
|
| 145 |
+
out.append("")
|
| 146 |
+
out.append("TRAINS (timetable: arrival/departure at stops, passing minute at points marked *; the pattern gives the "
|
| 147 |
+
"running times)")
|
| 148 |
+
for r in sorted(task.runs, key=lambda r: (r.fixed_arr if r.started else r.dep[0])):
|
| 149 |
+
stops = []
|
| 150 |
+
for i, code in enumerate(r.route):
|
| 151 |
+
if not r.stops[i]:
|
| 152 |
+
stops.append(f"{code}* {hhmm(r.arr[i])}")
|
| 153 |
+
continue
|
| 154 |
+
a, d = r.arr[i], r.dep[i]
|
| 155 |
+
if a is None:
|
| 156 |
+
stops.append(f"{code} {hhmm(d)}")
|
| 157 |
+
elif d is None:
|
| 158 |
+
stops.append(f"{code} {hhmm(a)}")
|
| 159 |
+
elif a == d:
|
| 160 |
+
stops.append(f"{code} {hhmm(a)}")
|
| 161 |
+
else:
|
| 162 |
+
stops.append(f"{code} {hhmm(a)}/{hhmm(d)}")
|
| 163 |
+
head = f"{r.id} {r.kind} {r.origin}->{r.dest} [{pid[r.id]}, set {r.unit}]"
|
| 164 |
+
if r.started:
|
| 165 |
+
k = r.fixed_to
|
| 166 |
+
code = r.route[k]
|
| 167 |
+
sched = r.arr[k] if r.arr[k] is not None else r.dep[k]
|
| 168 |
+
late = r.fixed_arr - sched if sched is not None else 0
|
| 169 |
+
if r.fixed_arr <= task.start and r.stops[k]:
|
| 170 |
+
pos = f"standing at {code} since {hhmm(r.fixed_arr)}"
|
| 171 |
+
elif r.stuck:
|
| 172 |
+
pos = f"STUCK in the closed section before {code}; reaches {code} at {hhmm(r.fixed_arr)} at the earliest"
|
| 173 |
+
else:
|
| 174 |
+
pos = f"running, reaches {code} at {hhmm(r.fixed_arr)}"
|
| 175 |
+
head += f" RUNNING ({pos}; {late:+d} min on its timetable there)"
|
| 176 |
+
out.append(f"{head}: " + ", ".join(stops))
|
| 177 |
+
out.append("")
|
| 178 |
+
out.append("TRAIN SETS (each works its runs in this order)")
|
| 179 |
+
for u in task.units:
|
| 180 |
+
first = task.run(u.runs[0])
|
| 181 |
+
if first.started:
|
| 182 |
+
where = f"running {first.id}"
|
| 183 |
+
else:
|
| 184 |
+
where = f"at {u.at}, ready {hhmm(u.ready)}"
|
| 185 |
+
nxt = (f"; must then be at {u.after['station']} for {u.after['run']} at {hhmm(u.after['dep'])} "
|
| 186 |
+
f"({u.after['stops']} stops)") if u.after else "; no further runs today"
|
| 187 |
+
out.append(f"{u.id}: {where}; runs " + " -> ".join(u.runs) + nxt)
|
| 188 |
+
return "\n".join(out)
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def brief(task: Task) -> str:
|
| 192 |
+
return f"{task.task_id}: {len(task.runs)} runs, {len(task.units)} train sets, {len(task.closures)} closures"
|
core/rail_core/reference.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""A task's reference: the proven optimum, the floor, the lazy policies' costs and rewards, and the real outcome.
|
| 2 |
+
|
| 3 |
+
Nothing here is shown to the agent.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
from .baselines import POLICIES
|
| 11 |
+
from .check import evaluate
|
| 12 |
+
from .model import Task, plan_to_list
|
| 13 |
+
from .reward import score
|
| 14 |
+
from .solve import floor_parts, optimal_plan
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def add_reference(task: Task, time_limit: float = 90.0, workers: int = 4, extend: int = 2) -> dict:
|
| 18 |
+
hints = {}
|
| 19 |
+
for name in ("greedy", "greedy_fast"):
|
| 20 |
+
try:
|
| 21 |
+
hints[name] = POLICIES[name](task)
|
| 22 |
+
except Exception as e: # a baseline that crashes is a bug, but must not stop a build
|
| 23 |
+
hints[name] = None
|
| 24 |
+
task.reference.setdefault("errors", []).append(f"{name}: {type(e).__name__}: {e}")
|
| 25 |
+
best_hint = None
|
| 26 |
+
for name, p in hints.items():
|
| 27 |
+
if p is None:
|
| 28 |
+
continue
|
| 29 |
+
r = evaluate(task, p)
|
| 30 |
+
if r.feasible and (best_hint is None or r.cost < best_hint[1]):
|
| 31 |
+
best_hint = (p, r.cost)
|
| 32 |
+
floor, parts = floor_parts(task)
|
| 33 |
+
sol = optimal_plan(task, time_limit=time_limit, workers=workers, hint=best_hint[0] if best_hint else None,
|
| 34 |
+
unit_floor=parts)
|
| 35 |
+
if sol.plan is None:
|
| 36 |
+
raise ValueError(f"{task.task_id}: no feasible plan found ({sol.status})")
|
| 37 |
+
floor = min(floor, sol.bound)
|
| 38 |
+
# large windows: warm-start from the best plan and give the proof more time, while the gap is closable
|
| 39 |
+
limit, spent = time_limit, sol.seconds
|
| 40 |
+
for _ in range(extend):
|
| 41 |
+
gap = (sol.cost - sol.bound) / (sol.cost - floor + task.rule("gap_k"))
|
| 42 |
+
if sol.optimal or gap <= 0.01 or gap > 0.3: # close enough to finish, or hopeless
|
| 43 |
+
break
|
| 44 |
+
limit *= 2
|
| 45 |
+
nxt = optimal_plan(task, time_limit=limit, workers=workers, hint=sol.plan, unit_floor=parts)
|
| 46 |
+
spent += nxt.seconds
|
| 47 |
+
if nxt.plan is not None and (nxt.cost, -nxt.bound) <= (sol.cost, -sol.bound):
|
| 48 |
+
sol = nxt
|
| 49 |
+
elif nxt.plan is not None:
|
| 50 |
+
sol = type(sol)(sol.plan, sol.cost, max(sol.bound, nxt.bound), sol.optimal, sol.status, sol.seconds)
|
| 51 |
+
floor = min(floor, sol.bound)
|
| 52 |
+
ref = task.reference
|
| 53 |
+
ref.update(optimal_cost=sol.cost, optimal_plan=plan_to_list(sol.plan), proven_optimal=sol.optimal,
|
| 54 |
+
lower_bound=sol.bound, floor_cost=floor, solver_seconds=round(spent, 1), solver=sol.status)
|
| 55 |
+
score_baselines(task, hints)
|
| 56 |
+
if "real" in ref:
|
| 57 |
+
rr = ref["real"]
|
| 58 |
+
rew, _ = score(rr["cost"], True, 1.0, sol.cost, floor, gap_k)
|
| 59 |
+
rr["reward_if_legal"] = round(rew, 4)
|
| 60 |
+
return ref
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def score_baselines(task: Task, hints: dict | None = None) -> None:
|
| 64 |
+
"""The lazy policies' feasibility, cost, reward and rule breaks, against the stored optimum and floor."""
|
| 65 |
+
ref = task.reference
|
| 66 |
+
if hints is None:
|
| 67 |
+
hints = {}
|
| 68 |
+
for name in ("greedy", "greedy_fast"):
|
| 69 |
+
try:
|
| 70 |
+
hints[name] = POLICIES[name](task)
|
| 71 |
+
except Exception as e:
|
| 72 |
+
hints[name] = None
|
| 73 |
+
ref.setdefault("errors", []).append(f"{name}: {type(e).__name__}: {e}")
|
| 74 |
+
gap_k = task.rule("gap_k")
|
| 75 |
+
for name, p in [("naive", POLICIES["naive"](task))] + list(hints.items()):
|
| 76 |
+
if p is None:
|
| 77 |
+
ref[f"{name}_feasible"] = False
|
| 78 |
+
ref[f"{name}_reward"] = 0.0
|
| 79 |
+
continue
|
| 80 |
+
r = evaluate(task, p)
|
| 81 |
+
clean = sum(1 for x in r.runs if not x.problems) / max(1, len(r.runs))
|
| 82 |
+
rew, _ = score(r.cost, r.feasible, clean, ref["optimal_cost"], ref["floor_cost"], gap_k)
|
| 83 |
+
ref[f"{name}_feasible"] = r.feasible
|
| 84 |
+
ref[f"{name}_cost"] = r.cost
|
| 85 |
+
ref[f"{name}_reward"] = round(rew, 4)
|
| 86 |
+
ref[f"{name}_violations"] = len(r.violations)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
BASELINE_KEYS = [f"{n}_{k}" for n in ("naive", "greedy", "greedy_fast") for k in ("feasible", "cost", "reward",
|
| 90 |
+
"violations")]
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def refresh_baselines(pack_dir: Path) -> list[str]:
|
| 94 |
+
"""Re-score the lazy policies of every task in a pack (after a baseline fix), without re-solving. Rewrites the
|
| 95 |
+
pack only if something changed; returns the ids of the tasks that changed."""
|
| 96 |
+
import json
|
| 97 |
+
|
| 98 |
+
from .build import write_pack
|
| 99 |
+
from .hardness import features
|
| 100 |
+
from .pack import _read
|
| 101 |
+
|
| 102 |
+
tasks, manifest = _read(pack_dir)
|
| 103 |
+
raw = [t.to_dict() for t in tasks]
|
| 104 |
+
changed = []
|
| 105 |
+
for t, d in zip(tasks, raw):
|
| 106 |
+
before = {k: t.reference.get(k) for k in BASELINE_KEYS}
|
| 107 |
+
t.reference.pop("errors", None)
|
| 108 |
+
score_baselines(t)
|
| 109 |
+
if {k: t.reference.get(k) for k in BASELINE_KEYS} != before or "errors" in d["reference"]:
|
| 110 |
+
t.reference["hardness"] = features(t)
|
| 111 |
+
d["reference"] = t.reference
|
| 112 |
+
changed.append(t.task_id)
|
| 113 |
+
if changed:
|
| 114 |
+
log = [json.loads(x) for x in (pack_dir / "build_log.jsonl").read_text().splitlines()]
|
| 115 |
+
for st in log:
|
| 116 |
+
for t in tasks:
|
| 117 |
+
if st.get("task_id") == t.task_id and "hardness" in st:
|
| 118 |
+
st["hardness"] = t.reference["hardness"]
|
| 119 |
+
std = {"name", "split", "tasks", "file", "tiers", "sha256", "candidates", "days", "proven_optimal", "runs",
|
| 120 |
+
"source"}
|
| 121 |
+
write_pack(pack_dir, raw, log, manifest["split"], {k: v for k, v in manifest.items() if k not in std})
|
| 122 |
+
return changed
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
if __name__ == "__main__":
|
| 126 |
+
import argparse
|
| 127 |
+
|
| 128 |
+
ap = argparse.ArgumentParser(description="re-score the lazy policies stored in task packs")
|
| 129 |
+
ap.add_argument("packs", nargs="+")
|
| 130 |
+
for pack in ap.parse_args().packs:
|
| 131 |
+
print(pack, refresh_baselines(Path(pack)))
|
core/rail_core/replay.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""What the map replay animates: every run's simulated times under a plan, the plan's orders, the cost as it accrues,
|
| 2 |
+
the rule breaks and the moments worth marking on the timeline. Built from the same `simulate` and `evaluate` the
|
| 3 |
+
grader uses, so the picture is the grade. For people (the `/twin/` viewer); no MCP tool returns it.
|
| 4 |
+
|
| 5 |
+
Cost events add up to the grader's cost: a stop's delay when the train arrives there, 30 for a stop not served at
|
| 6 |
+
the minute it was due, and each train set's lateness handed on to the next window at the window's end.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
from .check import evaluate, network, simulate
|
| 12 |
+
from .model import Order, Plan, Task, hhmm
|
| 13 |
+
from .reward import score
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _due(r, j: int) -> int | None:
|
| 17 |
+
return r.arr[j] if r.arr[j] is not None else r.dep[j]
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def replay(task: Task, plan: Plan, name: str = "", parse_problems: list[str] | None = None) -> dict:
|
| 21 |
+
net = network(task)
|
| 22 |
+
res = evaluate(task, plan)
|
| 23 |
+
_, _, _, _, carry = simulate(task, plan)
|
| 24 |
+
problems = list(parse_problems or [])
|
| 25 |
+
feasible = res.feasible and not problems
|
| 26 |
+
clean = sum(1 for x in res.runs if not x.problems) / max(1, len(res.runs) + len(problems))
|
| 27 |
+
optimum, floor = int(task.reference.get("optimal_cost", 0)), int(task.reference.get("floor_cost", 0))
|
| 28 |
+
reward, _ = score(res.cost if feasible else None, feasible, clean, optimum, floor, task.rule("gap_k"))
|
| 29 |
+
cancel_cost = task.rule("cancel_cost")
|
| 30 |
+
|
| 31 |
+
runs, costs, events = [], [], []
|
| 32 |
+
for x in res.runs:
|
| 33 |
+
r = task.run(x.train)
|
| 34 |
+
o = plan.get(x.train) or Order()
|
| 35 |
+
n = len(r.route)
|
| 36 |
+
runs.append({
|
| 37 |
+
"train": x.train, "unit": x.unit, "kind": r.kind, "dir": "E" if net.direction(r.origin, r.dest) > 0
|
| 38 |
+
else "W", "route": r.route, "stops": r.stops, "sched_arr": r.arr, "sched_dep": r.dep,
|
| 39 |
+
"cancelled": x.cancelled, "first": x.first, "last": x.last,
|
| 40 |
+
"arr": [x.arr.get(i) for i in range(n)], "dep": [x.dep.get(i) for i in range(n)],
|
| 41 |
+
"stopped": [bool(x.stopped.get(i)) for i in range(n)],
|
| 42 |
+
"started": r.started, "fixed_to": r.fixed_to, "fixed_from": r.fixed_from, "stuck": r.stuck,
|
| 43 |
+
"delay": x.delay, "unserved": x.unserved, "problems": x.problems,
|
| 44 |
+
"order": o.to_json(x.train) if (o.cancel or o.start or o.end or o.hold) else None})
|
| 45 |
+
# cost as it accrues (the same terms as check.evaluate)
|
| 46 |
+
if x.cancelled:
|
| 47 |
+
for j in range(n):
|
| 48 |
+
if r.stops[j] and _due(r, j) is not None:
|
| 49 |
+
costs.append([_due(r, j), cancel_cost, "unserved", x.train, r.route[j]])
|
| 50 |
+
events.append({"t": r.dep[0] if r.dep[0] is not None else task.start, "kind": "cancel",
|
| 51 |
+
"train": x.train, "text": f"{x.train} cancelled ({r.origin} {hhmm(r.dep[0])} to {r.dest})"})
|
| 52 |
+
continue
|
| 53 |
+
s, e = x.first, x.last
|
| 54 |
+
k = r.fixed_to if r.started else s
|
| 55 |
+
for j in range(k if r.started else s + 1, e + 1):
|
| 56 |
+
if r.stops[j] and r.arr[j] is not None and x.arr.get(j) is not None and x.arr[j] >= task.start:
|
| 57 |
+
late = max(0, x.arr[j] - r.arr[j])
|
| 58 |
+
if late:
|
| 59 |
+
costs.append([x.arr[j], late, "delay", x.train, r.route[j]])
|
| 60 |
+
for j in range(n):
|
| 61 |
+
if r.stops[j] and not (s <= j <= e) and _due(r, j) is not None:
|
| 62 |
+
costs.append([_due(r, j), cancel_cost, "unserved", x.train, r.route[j]])
|
| 63 |
+
if e < n - 1:
|
| 64 |
+
events.append({"t": x.arr.get(e), "kind": "turn", "train": x.train,
|
| 65 |
+
"text": f"{x.train} turns back at {r.route[e]} (not to {r.dest})"})
|
| 66 |
+
if s > 0 and not r.started:
|
| 67 |
+
events.append({"t": x.dep.get(s), "kind": "turn", "train": x.train,
|
| 68 |
+
"text": f"{x.train} starts at {r.route[s]} (not {r.origin})"})
|
| 69 |
+
for code, minute in sorted(o.hold.items(), key=lambda kv: kv[1]):
|
| 70 |
+
i = r.route.index(code) if code in r.route else None
|
| 71 |
+
if i is not None and x.dep.get(i) is not None:
|
| 72 |
+
events.append({"t": x.arr.get(i, minute), "kind": "hold", "train": x.train,
|
| 73 |
+
"text": f"{x.train} held at {code} until {hhmm(minute)}"})
|
| 74 |
+
for unit, c in carry.items():
|
| 75 |
+
if c:
|
| 76 |
+
costs.append([task.end, c, "handed_on", unit, ""])
|
| 77 |
+
for c in task.closures:
|
| 78 |
+
span = "-".join(dict.fromkeys([c.where[0], c.where[-1]]))
|
| 79 |
+
what = {"closed": "closed", "slow": f"slow (+{c.extra} min)", "track": "one platform track out of use"}
|
| 80 |
+
events.append({"t": c.start, "kind": "closure", "text": f"{span} {what.get(c.kind, c.kind)}: {c.cause} "
|
| 81 |
+
f"({hhmm(c.start)}-{hhmm(c.end)})"})
|
| 82 |
+
events.append({"t": c.end, "kind": "reopen", "text": f"{span} back to normal"})
|
| 83 |
+
for v in res.violations:
|
| 84 |
+
if v.get("time"):
|
| 85 |
+
h, m = v["time"].split(":")
|
| 86 |
+
events.append({"t": int(h) * 60 + int(m), "kind": "violation", "rule": v["rule"],
|
| 87 |
+
"trains": v["trains"], "where": v["where"], "text": v["detail"]})
|
| 88 |
+
events.sort(key=lambda ev: (ev["t"] if ev["t"] is not None else task.start, ev["kind"]))
|
| 89 |
+
costs.sort(key=lambda c: c[0])
|
| 90 |
+
total = sum(c[1] for c in costs)
|
| 91 |
+
assert total == res.delay_cost + res.cancel_cost + res.carry_cost, "cost events must add up to the grader's cost"
|
| 92 |
+
return {
|
| 93 |
+
"task_id": task.task_id, "day": task.day, "start": task.start, "end": task.end,
|
| 94 |
+
"difficulty": task.difficulty, "plan": name, "feasible": feasible, "reward": round(reward, 4),
|
| 95 |
+
"cost": res.cost if feasible else None, "cost_if_legal": total,
|
| 96 |
+
"cost_parts": {"delay": res.delay_cost, "unserved": res.cancel_cost, "handed_on": res.carry_cost},
|
| 97 |
+
"optimal_cost": optimum, "floor_cost": floor,
|
| 98 |
+
"cancelled": res.cancelled, "short_turned": res.short_turned,
|
| 99 |
+
"violations": res.violations[:80], "parse_problems": problems,
|
| 100 |
+
"points": [{"code": p.code, "name": p.name, "km": p.km, "kind": p.kind, "tracks": p.tracks,
|
| 101 |
+
"directional": p.directional, "turn": p.turn} for p in net.points],
|
| 102 |
+
"sections": [{"a": s.a, "b": s.b, "single": s.single} for s in net.sections],
|
| 103 |
+
"closures": [{"kind": c.kind, "where": c.where, "start": c.start, "end": c.end, "extra": c.extra,
|
| 104 |
+
"cause": c.cause, "real": c.real} for c in task.closures],
|
| 105 |
+
"units": [{"id": u.id, "at": u.at, "ready": u.ready, "runs": u.runs, "after": u.after} for u in task.units],
|
| 106 |
+
"runs": runs, "costs": costs, "events": events, "notices": task.notices,
|
| 107 |
+
"rules": {k: task.rule(k) for k in ("h_follow", "h_opp", "turn_min", "stop_loss", "cancel_cost",
|
| 108 |
+
"max_delay")},
|
| 109 |
+
}
|
core/rail_core/report.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""What check_plan shows the agent: every train's simulated times, its delay and lost stops, every rule broken, the
|
| 2 |
+
cost. Never a score and never anything from the reference."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
from .check import evaluate, network
|
| 7 |
+
from .model import Plan, Task, hhmm
|
| 8 |
+
|
| 9 |
+
MAX_VIOLATIONS = 60
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def _times(task: Task, res) -> str:
|
| 13 |
+
r = task.run(res.train)
|
| 14 |
+
net = network(task)
|
| 15 |
+
parts = []
|
| 16 |
+
first = r.fixed_to if r.started else res.first
|
| 17 |
+
for i in range(first, res.last + 1):
|
| 18 |
+
code = r.route[i]
|
| 19 |
+
if net.by_code[code].kind == "junction":
|
| 20 |
+
continue
|
| 21 |
+
a, d = res.arr.get(i), res.dep.get(i)
|
| 22 |
+
if a is None and d is None:
|
| 23 |
+
continue
|
| 24 |
+
if a is None: # where it starts
|
| 25 |
+
s = f"{code} dep {hhmm(d)}"
|
| 26 |
+
elif d is None or d == a:
|
| 27 |
+
s = f"{code} {hhmm(a)}"
|
| 28 |
+
else:
|
| 29 |
+
s = f"{code} {hhmm(a)}-{hhmm(d)}"
|
| 30 |
+
if r.stops[i] and r.arr[i] is not None and a is not None and i > res.first:
|
| 31 |
+
late = a - r.arr[i]
|
| 32 |
+
if late > 0:
|
| 33 |
+
s += f" (+{late})"
|
| 34 |
+
elif not r.stops[i] and res.stopped.get(i) and i not in (res.first, res.last):
|
| 35 |
+
s += " (unscheduled stop)"
|
| 36 |
+
parts.append(s)
|
| 37 |
+
return " | ".join(parts)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def plan_report(task: Task, plan: Plan, problems: list[str] | None = None) -> dict:
|
| 41 |
+
res = evaluate(task, plan)
|
| 42 |
+
problems = list(problems or [])
|
| 43 |
+
feasible = res.feasible and not problems
|
| 44 |
+
trains = []
|
| 45 |
+
for x in sorted(res.runs, key=lambda x: (task.run(x.train).fixed_arr if task.run(x.train).started
|
| 46 |
+
else task.run(x.train).dep[0])):
|
| 47 |
+
r = task.run(x.train)
|
| 48 |
+
if x.cancelled:
|
| 49 |
+
what = "cancelled"
|
| 50 |
+
else:
|
| 51 |
+
a, b = r.route[x.first], r.route[x.last]
|
| 52 |
+
what = f"{a}->{b}" + ("" if (x.first == 0 and x.last == len(r.route) - 1) else " (short)")
|
| 53 |
+
row = {"train": x.train, "set": x.unit, "runs": what, "delay": x.delay, "unserved_stops": x.unserved}
|
| 54 |
+
if not x.cancelled:
|
| 55 |
+
row["times"] = _times(task, x)
|
| 56 |
+
if x.problems:
|
| 57 |
+
row["breaks"] = x.problems
|
| 58 |
+
trains.append(row)
|
| 59 |
+
out = {
|
| 60 |
+
"feasible": feasible,
|
| 61 |
+
"cost": res.delay_cost + res.cancel_cost + res.carry_cost if feasible else None,
|
| 62 |
+
"cost_parts": {"delay": res.delay_cost, "unserved_stops": res.cancel_cost, "handed_on": res.carry_cost},
|
| 63 |
+
"violations_total": len(res.violations),
|
| 64 |
+
"violations": res.violations[:MAX_VIOLATIONS],
|
| 65 |
+
"trains": trains,
|
| 66 |
+
}
|
| 67 |
+
if not feasible:
|
| 68 |
+
out["note"] = ("infeasible: fix every violation (cost_parts are what the plan would cost if it were legal)"
|
| 69 |
+
if res.violations or problems else "")
|
| 70 |
+
if problems:
|
| 71 |
+
out["entry_problems"] = problems
|
| 72 |
+
return out
|
core/rail_core/reward.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""The terminal reward, from a submitted plan, the task's proven optimum and its floor.
|
| 2 |
+
|
| 3 |
+
unreadable, malformed or infeasible 0.2 x clean runs / (runs + entry problems) in [0, 0.2)
|
| 4 |
+
feasible 0.2 + 0.8 x exp(-g / 0.5) in (0.2, 1]
|
| 5 |
+
g = (cost - optimum) / (optimum - floor + gap_k)
|
| 6 |
+
|
| 7 |
+
`floor` is each train set's best cost alone with the closures (`solve.floor_cost`): what the disruption forces no
|
| 8 |
+
matter how well the trains are sequenced. The gap is measured against the part a dispatcher controls, so a plan that
|
| 9 |
+
wastes as much as the optimum spends above the floor (plus gap_k) scores 0.31. gap_k is 100 stop-minutes (about three
|
| 10 |
+
unserved stops), which keeps the gap finite when the optimum sits on the floor.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import math
|
| 16 |
+
from dataclasses import asdict, dataclass
|
| 17 |
+
|
| 18 |
+
from .check import evaluate
|
| 19 |
+
from .model import Plan, Task
|
| 20 |
+
|
| 21 |
+
INFEASIBLE_CAP = 0.2
|
| 22 |
+
GAP_TAU = 0.5
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@dataclass
|
| 26 |
+
class Grade:
|
| 27 |
+
reward: float
|
| 28 |
+
feasible: bool
|
| 29 |
+
clean_fraction: float
|
| 30 |
+
quality: float
|
| 31 |
+
cost: int | None
|
| 32 |
+
delay_cost: int | None
|
| 33 |
+
cancel_cost: int | None
|
| 34 |
+
carry_cost: int | None
|
| 35 |
+
cancelled: int | None
|
| 36 |
+
short_turned: int | None
|
| 37 |
+
optimal_cost: int
|
| 38 |
+
floor_cost: int
|
| 39 |
+
violations: list[dict]
|
| 40 |
+
parse_problems: list[str]
|
| 41 |
+
|
| 42 |
+
def as_dict(self) -> dict:
|
| 43 |
+
return asdict(self)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def score(cost: int | None, feasible: bool, clean_fraction: float, optimum: int, floor: int,
|
| 47 |
+
gap_k: int) -> tuple[float, float]:
|
| 48 |
+
"""(reward, quality)."""
|
| 49 |
+
if not feasible or cost is None:
|
| 50 |
+
return INFEASIBLE_CAP * clean_fraction, 0.0
|
| 51 |
+
g = max(0, cost - optimum) / (max(0, optimum - floor) + gap_k)
|
| 52 |
+
quality = max(math.exp(-g / GAP_TAU), 1e-6)
|
| 53 |
+
return INFEASIBLE_CAP + (1.0 - INFEASIBLE_CAP) * quality, quality
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def grade(task: Task, plan: Plan, parse_problems: list[str] | None = None) -> Grade:
|
| 57 |
+
optimum = int(task.reference["optimal_cost"])
|
| 58 |
+
floor = int(task.reference["floor_cost"])
|
| 59 |
+
res = evaluate(task, plan)
|
| 60 |
+
problems = list(parse_problems or [])
|
| 61 |
+
feasible = res.feasible and not problems
|
| 62 |
+
clean = sum(1 for r in res.runs if not r.problems) / max(1, len(res.runs) + len(problems))
|
| 63 |
+
reward, quality = score(res.cost, feasible, clean, optimum, floor, task.rule("gap_k"))
|
| 64 |
+
ok = feasible
|
| 65 |
+
return Grade(round(reward, 6), feasible, round(clean, 4), round(quality, 6),
|
| 66 |
+
res.cost if ok else None, res.delay_cost if ok else None, res.cancel_cost if ok else None,
|
| 67 |
+
res.carry_cost if ok else None, res.cancelled if ok else None, res.short_turned if ok else None,
|
| 68 |
+
optimum, floor, res.violations, problems)
|
core/rail_core/select.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Choose a hard eval set from a candidate pool, without looking at any model's results on the eval tasks.
|
| 2 |
+
|
| 3 |
+
python -m rail_core.select --pool ../tasks/pool-eval --calib ../../results/rollouts/<run>/index.json \
|
| 4 |
+
--n 45 --out ../tasks/rail-v1-eval
|
| 5 |
+
|
| 6 |
+
How:
|
| 7 |
+
1. Calibrate on the train split. A model is run on a few train tasks (eval/run_eval.py --split train); its rewards
|
| 8 |
+
and the tasks' hardness features (`hardness.features`, model-free) are the calibration set.
|
| 9 |
+
2. Fit a ridge regression from standardized features to reward on that set (pure Python, leave-one-out error
|
| 10 |
+
reported). Features whose coefficient sign is unstable under leave-one-out are dropped.
|
| 11 |
+
3. Predict every pool task's reward (the pool is built on held-out days only) and keep the `n` with the lowest
|
| 12 |
+
prediction, with at most `max_per_day` per day and every tier in the pool represented (`min_per_tier`).
|
| 13 |
+
The eval tasks themselves are never scored by a model before selection: the choice depends only on train-split
|
| 14 |
+
calibration and on features any solver-built task has, so the eval stays a fair test.
|
| 15 |
+
|
| 16 |
+
Writes the pack (tasks.jsonl, manifest.json with the selection) and selection.json: the fit, its leave-one-out
|
| 17 |
+
error, the weights, and every pool task's prediction.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
import argparse
|
| 23 |
+
import json
|
| 24 |
+
import math
|
| 25 |
+
from pathlib import Path
|
| 26 |
+
|
| 27 |
+
from .build import write_pack
|
| 28 |
+
from .pack import TaskPack
|
| 29 |
+
|
| 30 |
+
FEATURES = ["runs", "conflicts", "decisions", "holds", "short_turns", "cancels", "turn_points", "avoidable",
|
| 31 |
+
"greedy_gap", "closed_min", "slow_min", "track_min", "single_closed", "mid_line", "late_sets"]
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _vec(h: dict, feats: list[str]) -> list[float]:
|
| 35 |
+
out = []
|
| 36 |
+
for f in feats:
|
| 37 |
+
v = h.get(f, 0)
|
| 38 |
+
v = float(v) if not isinstance(v, bool) else float(int(v))
|
| 39 |
+
if f in ("avoidable",):
|
| 40 |
+
v = math.log1p(max(0.0, v))
|
| 41 |
+
out.append(v)
|
| 42 |
+
return out
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def _solve(A: list[list[float]], b: list[float]) -> list[float]:
|
| 46 |
+
"""Gaussian elimination with partial pivoting."""
|
| 47 |
+
n = len(A)
|
| 48 |
+
M = [row[:] + [b[i]] for i, row in enumerate(A)]
|
| 49 |
+
for c in range(n):
|
| 50 |
+
p = max(range(c, n), key=lambda r: abs(M[r][c]))
|
| 51 |
+
M[c], M[p] = M[p], M[c]
|
| 52 |
+
if abs(M[c][c]) < 1e-12:
|
| 53 |
+
continue
|
| 54 |
+
for r in range(n):
|
| 55 |
+
if r != c:
|
| 56 |
+
f = M[r][c] / M[c][c]
|
| 57 |
+
for k in range(c, n + 1):
|
| 58 |
+
M[r][k] -= f * M[c][k]
|
| 59 |
+
return [M[i][n] / M[i][i] if abs(M[i][i]) > 1e-12 else 0.0 for i in range(n)]
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def ridge(X: list[list[float]], y: list[float], lam: float) -> list[float]:
|
| 63 |
+
"""w (intercept first) minimizing |y - [1 X] w|^2 + lam |w[1:]|^2."""
|
| 64 |
+
Z = [[1.0] + row for row in X]
|
| 65 |
+
d = len(Z[0])
|
| 66 |
+
A = [[sum(z[i] * z[j] for z in Z) + (lam if i == j and i > 0 else 0.0) for j in range(d)] for i in range(d)]
|
| 67 |
+
b = [sum(z[i] * yy for z, yy in zip(Z, y)) for i in range(d)]
|
| 68 |
+
return _solve(A, b)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def predict(w: list[float], x: list[float]) -> float:
|
| 72 |
+
return w[0] + sum(a * b for a, b in zip(w[1:], x))
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
class Scaler:
|
| 76 |
+
def __init__(self, rows: list[list[float]]):
|
| 77 |
+
n = len(rows)
|
| 78 |
+
self.mu = [sum(r[j] for r in rows) / n for j in range(len(rows[0]))]
|
| 79 |
+
self.sd = [math.sqrt(sum((r[j] - self.mu[j]) ** 2 for r in rows) / n) or 1.0 for j in range(len(rows[0]))]
|
| 80 |
+
|
| 81 |
+
def __call__(self, r: list[float]) -> list[float]:
|
| 82 |
+
return [(v - m) / s for v, m, s in zip(r, self.mu, self.sd)]
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def fit(calib: list[tuple[dict, float]], pool_feats: list[dict], feats: list[str], lam: float):
|
| 86 |
+
X_raw = [_vec(h, feats) for h, _ in calib]
|
| 87 |
+
scale = Scaler(X_raw + [_vec(h, feats) for h in pool_feats])
|
| 88 |
+
X = [scale(r) for r in X_raw]
|
| 89 |
+
y = [r for _, r in calib]
|
| 90 |
+
w = ridge(X, y, lam)
|
| 91 |
+
loo, signs = [], []
|
| 92 |
+
for i in range(len(X)):
|
| 93 |
+
wi = ridge(X[:i] + X[i + 1:], y[:i] + y[i + 1:], lam)
|
| 94 |
+
loo.append(predict(wi, X[i]) - y[i])
|
| 95 |
+
signs.append([math.copysign(1, v) if abs(v) > 1e-9 else 0 for v in wi[1:]])
|
| 96 |
+
stable = [all(s[j] == signs[0][j] for s in signs) for j in range(len(feats))]
|
| 97 |
+
return w, scale, loo, stable
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def _spearman(a: list[float], b: list[float]) -> float:
|
| 101 |
+
def ranks(v):
|
| 102 |
+
o = sorted(range(len(v)), key=lambda i: v[i])
|
| 103 |
+
r = [0.0] * len(v)
|
| 104 |
+
for k, i in enumerate(o):
|
| 105 |
+
r[i] = k
|
| 106 |
+
return r
|
| 107 |
+
ra, rb = ranks(a), ranks(b)
|
| 108 |
+
n = len(a)
|
| 109 |
+
ma, mb = sum(ra) / n, sum(rb) / n
|
| 110 |
+
num = sum((x - ma) * (y - mb) for x, y in zip(ra, rb))
|
| 111 |
+
den = math.sqrt(sum((x - ma) ** 2 for x in ra) * sum((y - mb) ** 2 for y in rb)) or 1.0
|
| 112 |
+
return num / den
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def main():
|
| 116 |
+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 117 |
+
ap.add_argument("--pool", required=True)
|
| 118 |
+
ap.add_argument("--calib", nargs="+", required=True, help="index.json of runs on train tasks (with hardness)")
|
| 119 |
+
ap.add_argument("--model", default=None, help="only this model's calibration episodes")
|
| 120 |
+
ap.add_argument("--n", type=int, default=45)
|
| 121 |
+
ap.add_argument("--out", required=True)
|
| 122 |
+
ap.add_argument("--max-per-day", type=int, default=3)
|
| 123 |
+
ap.add_argument("--min-per-tier", type=int, default=4)
|
| 124 |
+
ap.add_argument("--lam", type=float, default=3.0)
|
| 125 |
+
args = ap.parse_args()
|
| 126 |
+
calib = []
|
| 127 |
+
for path in args.calib:
|
| 128 |
+
for e in json.loads(Path(path).read_text())["episodes"]:
|
| 129 |
+
if e.get("split") == "train" and e.get("hardness") and (not args.model or e["model"] == args.model):
|
| 130 |
+
calib.append((e["hardness"], float(e["reward"])))
|
| 131 |
+
pool = TaskPack(args.pool)
|
| 132 |
+
tasks = [t for t in pool.tasks if t.split == "eval"]
|
| 133 |
+
feats = list(FEATURES)
|
| 134 |
+
w, scale, loo, stable = fit(calib, [t.reference["hardness"] for t in tasks], feats, args.lam)
|
| 135 |
+
keep = [f for f, s in zip(feats, stable) if s]
|
| 136 |
+
if keep and len(keep) < len(feats):
|
| 137 |
+
feats = keep
|
| 138 |
+
w, scale, loo, stable = fit(calib, [t.reference["hardness"] for t in tasks], feats, args.lam)
|
| 139 |
+
preds = {t.task_id: predict(w, scale(_vec(t.reference["hardness"], feats))) for t in tasks}
|
| 140 |
+
y = [r for _, r in calib]
|
| 141 |
+
loo_pred = [e + yy for e, yy in zip(loo, y)]
|
| 142 |
+
# pick: lowest predicted reward, every tier represented, at most max_per_day per day
|
| 143 |
+
order = sorted(tasks, key=lambda t: preds[t.task_id])
|
| 144 |
+
chosen, per_day, per_tier = [], {}, {}
|
| 145 |
+
tiers = sorted({t.difficulty for t in tasks})
|
| 146 |
+
for tier in tiers: # the hardest few of each tier first
|
| 147 |
+
for t in [x for x in order if x.difficulty == tier]:
|
| 148 |
+
if per_tier.get(tier, 0) >= args.min_per_tier or len(chosen) >= args.n:
|
| 149 |
+
break
|
| 150 |
+
if per_day.get(t.day, 0) < args.max_per_day:
|
| 151 |
+
chosen.append(t)
|
| 152 |
+
per_day[t.day] = per_day.get(t.day, 0) + 1
|
| 153 |
+
per_tier[tier] = per_tier.get(tier, 0) + 1
|
| 154 |
+
for t in order:
|
| 155 |
+
if len(chosen) >= args.n:
|
| 156 |
+
break
|
| 157 |
+
if t in chosen or per_day.get(t.day, 0) >= args.max_per_day:
|
| 158 |
+
continue
|
| 159 |
+
chosen.append(t)
|
| 160 |
+
per_day[t.day] = per_day.get(t.day, 0) + 1
|
| 161 |
+
per_tier[t.difficulty] = per_tier.get(t.difficulty, 0) + 1
|
| 162 |
+
report = {
|
| 163 |
+
"calibration_episodes": len(calib), "features": feats, "lambda": args.lam,
|
| 164 |
+
"weights": dict(zip(["intercept"] + feats, [round(v, 4) for v in w])),
|
| 165 |
+
"loo_mae": round(sum(abs(e) for e in loo) / max(1, len(loo)), 4),
|
| 166 |
+
"loo_spearman": round(_spearman(loo_pred, y), 4) if len(y) > 2 else None,
|
| 167 |
+
"calibration_mean_reward": round(sum(y) / max(1, len(y)), 4),
|
| 168 |
+
"pool": len(tasks), "chosen": len(chosen), "per_tier": per_tier,
|
| 169 |
+
"predicted_mean_chosen": round(sum(preds[t.task_id] for t in chosen) / max(1, len(chosen)), 4),
|
| 170 |
+
"predicted_mean_pool": round(sum(preds.values()) / max(1, len(preds)), 4),
|
| 171 |
+
"predictions": {k: round(v, 4) for k, v in sorted(preds.items(), key=lambda kv: kv[1])},
|
| 172 |
+
}
|
| 173 |
+
out = Path(args.out)
|
| 174 |
+
out.mkdir(parents=True, exist_ok=True)
|
| 175 |
+
for t in chosen:
|
| 176 |
+
t.reference["predicted_reward"] = round(preds[t.task_id], 4)
|
| 177 |
+
picked = sorted((t.to_dict() for t in chosen), key=lambda d: (d["difficulty"], d["day"], d["start"]))
|
| 178 |
+
log = [json.loads(line) for line in (Path(args.pool) / "build_log.jsonl").read_text().splitlines()] \
|
| 179 |
+
if (Path(args.pool) / "build_log.jsonl").is_file() else []
|
| 180 |
+
manifest = write_pack(out, picked, log, "eval", extra={"selected_from": Path(args.pool).name,
|
| 181 |
+
"selection": {k: report[k] for k in report
|
| 182 |
+
if k != "predictions"}})
|
| 183 |
+
(out / "selection.json").write_text(json.dumps(report, indent=1) + "\n")
|
| 184 |
+
print(json.dumps({k: v for k, v in report.items() if k != "predictions"}, indent=1))
|
| 185 |
+
print(json.dumps({k: manifest[k] for k in ("tasks", "tiers", "runs")}))
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
if __name__ == "__main__":
|
| 189 |
+
main()
|
core/rail_core/solve.py
ADDED
|
@@ -0,0 +1,608 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""The reference plan: CP-SAT over the same physics and rules as `check`, proven optimal (or within a stated gap).
|
| 2 |
+
|
| 3 |
+
The model mirrors `check.simulate` exactly:
|
| 4 |
+
one option per run (run in full, short-turned / started at a turn point, or cancelled)
|
| 5 |
+
arrival and departure minutes at every point it may serve, with the same recurrences: minimum running times,
|
| 6 |
+
never ahead of the timetable, no early departures at scheduled stops, `stop_loss` for an unscheduled stop (a Boolean per passing point)
|
| 7 |
+
each train set's position and readiness carried run to run (a cancelled run leaves both unchanged)
|
| 8 |
+
single-track and headway rules as pairwise disjunctions (pruned where the time windows cannot meet), platform
|
| 9 |
+
tracks as cumulatives, closures as disjunctions, max delay as a domain bound
|
| 10 |
+
The solution is turned into a plan of holds, short-turns and cancellations and re-checked with `check.evaluate`: its
|
| 11 |
+
cost must equal the solver's objective.
|
| 12 |
+
|
| 13 |
+
`floor`: the same model without any interaction between train sets (no single-track, headway or platform rules).
|
| 14 |
+
Every real plan is feasible there, so its optimum is a lower bound: the cost the closures force on each train set
|
| 15 |
+
alone. The reward measures the gap against optimum - floor.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
import time
|
| 21 |
+
from dataclasses import dataclass
|
| 22 |
+
|
| 23 |
+
from ortools.sat.python import cp_model
|
| 24 |
+
|
| 25 |
+
from .check import evaluate, network, run_dir
|
| 26 |
+
from .model import Order, Plan, Run, Task
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@dataclass
|
| 30 |
+
class Solution:
|
| 31 |
+
plan: Plan | None
|
| 32 |
+
cost: int | None
|
| 33 |
+
bound: int
|
| 34 |
+
optimal: bool
|
| 35 |
+
status: str
|
| 36 |
+
seconds: float
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def _options(task: Task, r: Run) -> list[tuple[int, int] | None]:
|
| 40 |
+
net = network(task)
|
| 41 |
+
n = len(r.route)
|
| 42 |
+
turn = [i for i in range(1, n - 1) if net.by_code[r.route[i]].turn]
|
| 43 |
+
if r.started:
|
| 44 |
+
ends = [n - 1] + [i for i in turn if i >= r.fixed_to]
|
| 45 |
+
return [(0, e) for e in ends]
|
| 46 |
+
starts = [0] + turn
|
| 47 |
+
opts: list[tuple[int, int] | None] = [None]
|
| 48 |
+
for s in starts:
|
| 49 |
+
for e in [n - 1] + turn:
|
| 50 |
+
if e > s:
|
| 51 |
+
opts.append((s, e))
|
| 52 |
+
return opts
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class _Model:
|
| 56 |
+
def __init__(self, task: Task, interactions: bool = True, horizon_pad: int = 300,
|
| 57 |
+
unit_floor: dict[str, int] | None = None):
|
| 58 |
+
self.task = task
|
| 59 |
+
self.net = network(task)
|
| 60 |
+
self.m = cp_model.CpModel()
|
| 61 |
+
self.H = task.end + horizon_pad
|
| 62 |
+
self.interactions = interactions
|
| 63 |
+
self.L = task.rule("stop_loss")
|
| 64 |
+
self.turn = task.rule("turn_min")
|
| 65 |
+
self.cost_terms: list = []
|
| 66 |
+
self.const_cost = 0
|
| 67 |
+
self.unit_terms: dict[str, list] = {u.id: [] for u in task.units}
|
| 68 |
+
self.unit_const: dict[str, int] = {u.id: 0 for u in task.units}
|
| 69 |
+
self.unit_floor = unit_floor or {}
|
| 70 |
+
self.rv: dict[str, dict] = {}
|
| 71 |
+
self._build()
|
| 72 |
+
|
| 73 |
+
# ---------------------------------------------------------------- per run
|
| 74 |
+
def _bounds(self, r: Run):
|
| 75 |
+
"""(lo, hi_arr, hi_dep): earliest minute at each route point, and the latest arrival and departure there in
|
| 76 |
+
any plan that obeys max_delay (a run that may end before its next stop is bounded only by the horizon)."""
|
| 77 |
+
n = len(r.route)
|
| 78 |
+
lo = [0] * n
|
| 79 |
+
k = r.fixed_to if r.started else 0
|
| 80 |
+
t = max(r.fixed_arr, self.task.start) if r.started else r.dep[0]
|
| 81 |
+
lo[k] = r.fixed_arr if r.started else t
|
| 82 |
+
for i in range(k, n - 1):
|
| 83 |
+
if i > k and r.stops[i]:
|
| 84 |
+
t = max(t, r.dep[i])
|
| 85 |
+
t += r.seg[i]
|
| 86 |
+
if r.arr[i + 1] is not None:
|
| 87 |
+
t = max(t, r.arr[i + 1])
|
| 88 |
+
lo[i + 1] = t
|
| 89 |
+
maxd = self.task.rule("max_delay")
|
| 90 |
+
H = self.H
|
| 91 |
+
hi_arr, hi_dep = [H] * n, [H] * n
|
| 92 |
+
cap, travel, open_end = None, 0, False # the next stop's latest arrival, minutes to it, could end before it
|
| 93 |
+
for i in range(n - 1, k - 1, -1):
|
| 94 |
+
p = self.net.by_code[r.route[i]]
|
| 95 |
+
if i < n - 1:
|
| 96 |
+
travel += r.seg[i]
|
| 97 |
+
if cap is not None and not open_end:
|
| 98 |
+
hi_dep[i] = cap - travel
|
| 99 |
+
if r.stops[i] and r.arr[i] is not None and i > 0:
|
| 100 |
+
hi_arr[i] = r.arr[i] + maxd
|
| 101 |
+
cap, travel, open_end = r.arr[i] + maxd, 0, False
|
| 102 |
+
elif i < n - 1:
|
| 103 |
+
hi_arr[i] = hi_dep[i] if not p.turn else H
|
| 104 |
+
if p.turn:
|
| 105 |
+
open_end = True
|
| 106 |
+
for i in range(n):
|
| 107 |
+
hi_dep[i] = max(hi_dep[i], lo[i])
|
| 108 |
+
hi_arr[i] = max(hi_arr[i], lo[i])
|
| 109 |
+
return lo, hi_arr, hi_dep
|
| 110 |
+
|
| 111 |
+
def _run_vars(self, r: Run):
|
| 112 |
+
m, net = self.m, self.net
|
| 113 |
+
n = len(r.route)
|
| 114 |
+
opts = _options(self.task, r)
|
| 115 |
+
o = [m.NewBoolVar(f"o_{r.id}_{a}") for a in range(len(opts))]
|
| 116 |
+
m.AddExactlyOne(o)
|
| 117 |
+
lo, hi_arr, hi_dep = self._bounds(r)
|
| 118 |
+
f0 = r.fixed_to if r.started else 0
|
| 119 |
+
arr, dep = {}, {}
|
| 120 |
+
for i in range(f0, n):
|
| 121 |
+
arr[i] = m.NewIntVar(lo[i], hi_arr[i] + (self.L if not r.stops[i] else 0), f"a_{r.id}_{i}")
|
| 122 |
+
if i < n - 1:
|
| 123 |
+
dep[i] = m.NewIntVar(lo[i], hi_dep[i] + self.L, f"d_{r.id}_{i}")
|
| 124 |
+
|
| 125 |
+
def bsum(pred, name):
|
| 126 |
+
terms = [o[a] for a, op in enumerate(opts) if op is not None and pred(op)]
|
| 127 |
+
if not terms:
|
| 128 |
+
return 0
|
| 129 |
+
if len(terms) == 1:
|
| 130 |
+
return terms[0]
|
| 131 |
+
b = m.NewBoolVar(name)
|
| 132 |
+
m.Add(b == sum(terms))
|
| 133 |
+
return b
|
| 134 |
+
|
| 135 |
+
v = {i: bsum(lambda op, i=i: op[0] <= i <= op[1], f"v_{r.id}_{i}") for i in range(f0, n)}
|
| 136 |
+
u = {i: bsum(lambda op, i=i: op[0] <= i and op[1] >= i + 1, f"u_{r.id}_{i}") for i in range(f0, n - 1)}
|
| 137 |
+
st = {} if r.started else {i: bsum(lambda op, i=i: op[0] == i, f"s_{r.id}_{i}") for i in range(n)}
|
| 138 |
+
en = {i: bsum(lambda op, i=i: op[1] == i, f"e_{r.id}_{i}") for i in range(f0, n)}
|
| 139 |
+
cancel = o[0] if opts[0] is None else 0
|
| 140 |
+
w = {}
|
| 141 |
+
inner = {}
|
| 142 |
+
for i in range(f0, n):
|
| 143 |
+
code = r.route[i]
|
| 144 |
+
vi, si, ei = v[i], st.get(i, 0), en[i]
|
| 145 |
+
if isinstance(vi, int) and vi == 0:
|
| 146 |
+
inner[i] = 0
|
| 147 |
+
continue
|
| 148 |
+
if r.started and i == f0:
|
| 149 |
+
inner[i] = m.NewBoolVar(f"in_{r.id}_{i}")
|
| 150 |
+
m.Add(inner[i] == 1 - ei)
|
| 151 |
+
else:
|
| 152 |
+
inner[i] = m.NewBoolVar(f"in_{r.id}_{i}")
|
| 153 |
+
m.Add(inner[i] == vi - si - ei)
|
| 154 |
+
if not r.stops[i] and net.by_code[code].kind != "junction" and i < n - 1:
|
| 155 |
+
w[i] = m.NewBoolVar(f"w_{r.id}_{i}")
|
| 156 |
+
m.AddImplication(w[i], inner[i])
|
| 157 |
+
# departures and dwell
|
| 158 |
+
for i in range(f0, n - 1):
|
| 159 |
+
code = r.route[i]
|
| 160 |
+
pt = net.by_code[code]
|
| 161 |
+
if not isinstance(inner[i], int):
|
| 162 |
+
lit = inner[i]
|
| 163 |
+
if r.started and i == f0:
|
| 164 |
+
if r.stops[i]:
|
| 165 |
+
m.Add(dep[i] >= max(r.dep[i], self.task.start)).OnlyEnforceIf(lit)
|
| 166 |
+
m.Add(dep[i] >= self.task.start).OnlyEnforceIf(lit)
|
| 167 |
+
if r.stops[i]:
|
| 168 |
+
m.Add(dep[i] >= arr[i]).OnlyEnforceIf(lit)
|
| 169 |
+
m.Add(dep[i] >= r.dep[i]).OnlyEnforceIf(lit)
|
| 170 |
+
elif pt.kind == "junction":
|
| 171 |
+
m.Add(dep[i] == arr[i]).OnlyEnforceIf(lit)
|
| 172 |
+
else:
|
| 173 |
+
m.Add(dep[i] == arr[i]).OnlyEnforceIf([lit, w[i].Not()])
|
| 174 |
+
m.Add(dep[i] >= arr[i]).OnlyEnforceIf(w[i])
|
| 175 |
+
if not r.started and not isinstance(st[i], int):
|
| 176 |
+
m.Add(dep[i] >= r.dep[i]).OnlyEnforceIf(st[i])
|
| 177 |
+
if r.started:
|
| 178 |
+
k = f0
|
| 179 |
+
if k in w:
|
| 180 |
+
m.Add(arr[k] == r.fixed_arr + self.L * w[k])
|
| 181 |
+
else:
|
| 182 |
+
m.Add(arr[k] == r.fixed_arr)
|
| 183 |
+
# running
|
| 184 |
+
slow = [c for c in self.task.closures if c.kind == "slow"]
|
| 185 |
+
for i in range(f0, n - 1):
|
| 186 |
+
if isinstance(u[i], int):
|
| 187 |
+
continue
|
| 188 |
+
acc_terms = []
|
| 189 |
+
if i in w:
|
| 190 |
+
acc_terms.append(w[i])
|
| 191 |
+
if not r.started and not r.stops[i] and not isinstance(st.get(i, 0), int):
|
| 192 |
+
acc_terms.append(st[i])
|
| 193 |
+
brk_terms = []
|
| 194 |
+
if (i + 1) in w:
|
| 195 |
+
brk_terms.append(w[i + 1])
|
| 196 |
+
if not r.stops[i + 1] and not isinstance(en[i + 1], int):
|
| 197 |
+
brk_terms.append(en[i + 1])
|
| 198 |
+
extra = 0
|
| 199 |
+
sec = self.net.section(r.route[i], r.route[i + 1])
|
| 200 |
+
for c in slow:
|
| 201 |
+
lo_i, hi_i = sorted((self.net.index[c.where[0]], self.net.index[c.where[-1]]))
|
| 202 |
+
if not lo_i <= sec < hi_i:
|
| 203 |
+
continue
|
| 204 |
+
y = m.NewBoolVar(f"slow_{r.id}_{i}_{c.start}")
|
| 205 |
+
b1 = m.NewBoolVar("")
|
| 206 |
+
b2 = m.NewBoolVar("")
|
| 207 |
+
m.Add(dep[i] >= c.start).OnlyEnforceIf(b1)
|
| 208 |
+
m.Add(dep[i] < c.start).OnlyEnforceIf(b1.Not())
|
| 209 |
+
m.Add(dep[i] < c.end).OnlyEnforceIf(b2)
|
| 210 |
+
m.Add(dep[i] >= c.end).OnlyEnforceIf(b2.Not())
|
| 211 |
+
m.AddBoolAnd([b1, b2]).OnlyEnforceIf(y)
|
| 212 |
+
m.AddBoolOr([b1.Not(), b2.Not()]).OnlyEnforceIf(y.Not())
|
| 213 |
+
extra = extra + c.extra * y
|
| 214 |
+
run_t = m.NewIntVar(0, 2 * self.H, f"rt_{r.id}_{i}")
|
| 215 |
+
m.Add(run_t == dep[i] + r.seg[i] + self.L * sum(acc_terms) + extra)
|
| 216 |
+
if r.arr[i + 1] is not None: # never ahead of the timetable
|
| 217 |
+
reach = m.NewIntVar(0, 2 * self.H, f"rc_{r.id}_{i}")
|
| 218 |
+
m.AddMaxEquality(reach, [run_t, r.arr[i + 1]])
|
| 219 |
+
else:
|
| 220 |
+
reach = run_t
|
| 221 |
+
m.Add(arr[i + 1] == reach + self.L * sum(brk_terms)).OnlyEnforceIf(u[i])
|
| 222 |
+
# cost: delays at served stops (not where it starts), cancelled stops
|
| 223 |
+
C = self.task.rule("cancel_cost")
|
| 224 |
+
for i in range(f0, n):
|
| 225 |
+
if not r.stops[i] or r.arr[i] is None or i == 0:
|
| 226 |
+
continue
|
| 227 |
+
if r.started and i == f0:
|
| 228 |
+
if r.fixed_arr >= self.task.start:
|
| 229 |
+
self.const_cost += max(0, r.fixed_arr - r.arr[i])
|
| 230 |
+
self.unit_const[r.unit] += max(0, r.fixed_arr - r.arr[i])
|
| 231 |
+
continue
|
| 232 |
+
lit = m.NewBoolVar(f"late_{r.id}_{i}")
|
| 233 |
+
m.Add(lit == v[i] - st.get(i, 0)) if not isinstance(v[i], int) else m.Add(lit == 0)
|
| 234 |
+
dl = m.NewIntVar(0, self.H, f"dl_{r.id}_{i}")
|
| 235 |
+
m.Add(dl >= arr[i] - r.arr[i]).OnlyEnforceIf(lit)
|
| 236 |
+
self.cost_terms.append(dl)
|
| 237 |
+
self.unit_terms[r.unit].append(dl)
|
| 238 |
+
for a, op in enumerate(opts):
|
| 239 |
+
if op is None:
|
| 240 |
+
self.cost_terms.append(C * r.n_stops * o[a])
|
| 241 |
+
self.unit_terms[r.unit].append(C * r.n_stops * o[a])
|
| 242 |
+
else:
|
| 243 |
+
s, e = op
|
| 244 |
+
lost = sum(1 for j, x in enumerate(r.stops) if x and not (s <= j <= e))
|
| 245 |
+
if lost:
|
| 246 |
+
self.cost_terms.append(C * lost * o[a])
|
| 247 |
+
self.unit_terms[r.unit].append(C * lost * o[a])
|
| 248 |
+
self.rv[r.id] = dict(run=r, opts=opts, o=o, arr=arr, dep=dep, v=v, u=u, st=st, en=en, w=w, inner=inner,
|
| 249 |
+
lo=lo, hi_arr=hi_arr, hi_dep=hi_dep, cancel=cancel, f0=f0)
|
| 250 |
+
|
| 251 |
+
# ---------------------------------------------------------------- train sets
|
| 252 |
+
def _units(self):
|
| 253 |
+
m, net = self.m, self.net
|
| 254 |
+
self.waits = [] # (station, start expr/var, end var, literal)
|
| 255 |
+
for u in self.task.units:
|
| 256 |
+
pos = net.index[u.at] if u.at else None
|
| 257 |
+
rdy = u.ready
|
| 258 |
+
arrived = self.task.start if u.at else None
|
| 259 |
+
for k, rid in enumerate(u.runs):
|
| 260 |
+
R = self.rv[rid]
|
| 261 |
+
r = R["run"]
|
| 262 |
+
npos = m.NewIntVar(0, len(net.points) - 1, f"pos_{rid}")
|
| 263 |
+
nrdy = m.NewIntVar(0, self.H + self.turn, f"rdy_{rid}")
|
| 264 |
+
narr = m.NewIntVar(0, self.H, f"arrd_{rid}")
|
| 265 |
+
c = R["cancel"]
|
| 266 |
+
if not isinstance(c, int):
|
| 267 |
+
if pos is None:
|
| 268 |
+
m.Add(c == 0)
|
| 269 |
+
else:
|
| 270 |
+
m.Add(npos == pos).OnlyEnforceIf(c)
|
| 271 |
+
m.Add(nrdy == (rdy if rdy is not None else 0)).OnlyEnforceIf(c)
|
| 272 |
+
m.Add(narr == (arrived if arrived is not None else 0)).OnlyEnforceIf(c)
|
| 273 |
+
if not r.started:
|
| 274 |
+
for i, sv in R["st"].items():
|
| 275 |
+
if isinstance(sv, int):
|
| 276 |
+
continue
|
| 277 |
+
want = net.index[r.route[i]]
|
| 278 |
+
if pos is None or (isinstance(pos, int) and pos != want):
|
| 279 |
+
m.Add(sv == 0)
|
| 280 |
+
elif not isinstance(pos, int):
|
| 281 |
+
m.Add(pos == want).OnlyEnforceIf(sv)
|
| 282 |
+
if rdy is not None:
|
| 283 |
+
m.Add(R["dep"][i] >= rdy).OnlyEnforceIf(sv)
|
| 284 |
+
code = r.route[i]
|
| 285 |
+
p = net.by_code[code]
|
| 286 |
+
if p.tracks is not None and p.kind != "junction" and self.interactions:
|
| 287 |
+
self.waits.append((code, arrived, R["dep"][i], sv))
|
| 288 |
+
for i, ev in R["en"].items():
|
| 289 |
+
if isinstance(ev, int):
|
| 290 |
+
continue
|
| 291 |
+
m.Add(npos == net.index[r.route[i]]).OnlyEnforceIf(ev)
|
| 292 |
+
m.Add(nrdy == R["arr"][i] + self.turn).OnlyEnforceIf(ev)
|
| 293 |
+
m.Add(narr == R["arr"][i]).OnlyEnforceIf(ev)
|
| 294 |
+
pos, rdy, arrived = npos, nrdy, narr
|
| 295 |
+
if u.after:
|
| 296 |
+
m.Add(pos == net.index[u.after["station"]])
|
| 297 |
+
cr = m.NewIntVar(0, (self.H + self.turn) * int(u.after["stops"]), f"carry_{u.id}")
|
| 298 |
+
m.Add(cr >= int(u.after["stops"]) * (rdy - u.after["dep"]))
|
| 299 |
+
self.cost_terms.append(cr)
|
| 300 |
+
self.unit_terms[u.id].append(cr)
|
| 301 |
+
# a set that ends the window at a station with few tracks keeps one there
|
| 302 |
+
if self.interactions and pos is not None and not isinstance(pos, int):
|
| 303 |
+
for p in net.points:
|
| 304 |
+
if p.tracks is not None and p.kind != "junction" and p.turn:
|
| 305 |
+
b = m.NewBoolVar(f"park_{u.id}_{p.code}")
|
| 306 |
+
m.Add(pos == net.index[p.code]).OnlyEnforceIf(b)
|
| 307 |
+
m.Add(pos != net.index[p.code]).OnlyEnforceIf(b.Not())
|
| 308 |
+
self.waits.append((p.code, arrived, self.H, b))
|
| 309 |
+
|
| 310 |
+
# ---------------------------------------------------------------- interactions
|
| 311 |
+
def _sections(self):
|
| 312 |
+
m, net = self.m, self.net
|
| 313 |
+
hf, ho = self.task.rule("h_follow"), self.task.rule("h_opp")
|
| 314 |
+
occ: dict[int, list] = {}
|
| 315 |
+
for rid, R in self.rv.items():
|
| 316 |
+
r = R["run"]
|
| 317 |
+
d = run_dir(net, r)
|
| 318 |
+
for i, ui in R["u"].items():
|
| 319 |
+
if isinstance(ui, int) and ui == 0:
|
| 320 |
+
continue
|
| 321 |
+
sec = net.section(r.route[i], r.route[i + 1])
|
| 322 |
+
occ.setdefault(sec, []).append((rid, d, R["dep"][i], R["arr"][i + 1], ui,
|
| 323 |
+
R["lo"][i], R["hi_dep"][i] + self.L, R["lo"][i + 1],
|
| 324 |
+
R["hi_arr"][i + 1] + self.L))
|
| 325 |
+
if r.started and r.fixed_from is not None:
|
| 326 |
+
k = r.fixed_to
|
| 327 |
+
sec = net.section(r.route[k - 1], r.route[k])
|
| 328 |
+
occ.setdefault(sec, []).append((rid, d, r.fixed_from, R["arr"][k], 1, r.fixed_from, r.fixed_from,
|
| 329 |
+
r.fixed_arr, r.fixed_arr + self.L))
|
| 330 |
+
self.n_pairs = 0
|
| 331 |
+
order: dict[tuple, object] = {} # (section, first run id, second run id) -> "first goes first"
|
| 332 |
+
for sec, lst in occ.items():
|
| 333 |
+
single = net.sections[sec].single
|
| 334 |
+
for x in range(len(lst)):
|
| 335 |
+
for y in range(x + 1, len(lst)):
|
| 336 |
+
a, b = lst[x], lst[y]
|
| 337 |
+
if a[0] == b[0]:
|
| 338 |
+
continue
|
| 339 |
+
same = a[1] == b[1]
|
| 340 |
+
if not same and not single:
|
| 341 |
+
continue
|
| 342 |
+
lits = [l for l in (a[4], b[4]) if not isinstance(l, int)]
|
| 343 |
+
if same:
|
| 344 |
+
a_first = b[5] >= a[6] + hf and b[7] >= a[8] + hf
|
| 345 |
+
b_first = a[5] >= b[6] + hf and a[7] >= b[8] + hf
|
| 346 |
+
else:
|
| 347 |
+
a_first = b[5] >= a[8] + ho
|
| 348 |
+
b_first = a[5] >= b[8] + ho
|
| 349 |
+
if a_first or b_first:
|
| 350 |
+
order[(sec, a[0], b[0])] = bool(a_first)
|
| 351 |
+
continue
|
| 352 |
+
self.n_pairs += 1
|
| 353 |
+
z = m.NewBoolVar("")
|
| 354 |
+
order[(sec, a[0], b[0])] = z
|
| 355 |
+
if same:
|
| 356 |
+
m.Add(b[2] >= a[2] + hf).OnlyEnforceIf([z] + lits)
|
| 357 |
+
m.Add(b[3] >= a[3] + hf).OnlyEnforceIf([z] + lits)
|
| 358 |
+
m.Add(a[2] >= b[2] + hf).OnlyEnforceIf([z.Not()] + lits)
|
| 359 |
+
m.Add(a[3] >= b[3] + hf).OnlyEnforceIf([z.Not()] + lits)
|
| 360 |
+
else:
|
| 361 |
+
m.Add(b[2] >= a[3] + ho).OnlyEnforceIf([z] + lits)
|
| 362 |
+
m.Add(a[2] >= b[3] + ho).OnlyEnforceIf([z.Not()] + lits)
|
| 363 |
+
self._tie_orders(order, occ)
|
| 364 |
+
|
| 365 |
+
def _tie_orders(self, order: dict, occ: dict):
|
| 366 |
+
"""Redundant but exact: where two trains cannot pass each other at the point between two sections (a junction,
|
| 367 |
+
a one-track halt, a station with one track each way), their order is the same in both sections. Tying the
|
| 368 |
+
order variables removes symmetric search the solver would otherwise do."""
|
| 369 |
+
m, net = self.m, self.net
|
| 370 |
+
self.n_ties = 0
|
| 371 |
+
dirs = {e[0]: e[1] for lst in occ.values() for e in lst}
|
| 372 |
+
|
| 373 |
+
def lit(sec, a, b):
|
| 374 |
+
if (sec, a, b) in order:
|
| 375 |
+
return order[(sec, a, b)]
|
| 376 |
+
if (sec, b, a) in order:
|
| 377 |
+
v = order[(sec, b, a)]
|
| 378 |
+
return (not v) if isinstance(v, bool) else v.Not()
|
| 379 |
+
return None
|
| 380 |
+
|
| 381 |
+
for sec in range(len(net.sections) - 1):
|
| 382 |
+
p = net.points[sec + 1]
|
| 383 |
+
no_pass = p.kind == "junction" or p.directional or p.tracks == 1
|
| 384 |
+
one_track = p.tracks == 1 and net.sections[sec].single and net.sections[sec + 1].single
|
| 385 |
+
if not (no_pass or one_track):
|
| 386 |
+
continue
|
| 387 |
+
for (s, a, b) in list(order):
|
| 388 |
+
if s != sec:
|
| 389 |
+
continue
|
| 390 |
+
same = dirs.get(a) == dirs.get(b)
|
| 391 |
+
if (same and not no_pass) or (not same and not one_track):
|
| 392 |
+
continue
|
| 393 |
+
x, y = lit(sec, a, b), lit(sec + 1, a, b)
|
| 394 |
+
if x is None or y is None or (isinstance(x, bool) and isinstance(y, bool)):
|
| 395 |
+
continue
|
| 396 |
+
if isinstance(x, bool):
|
| 397 |
+
m.AddBoolOr([y if x else y.Not()])
|
| 398 |
+
elif isinstance(y, bool):
|
| 399 |
+
m.AddBoolOr([x if y else x.Not()])
|
| 400 |
+
else:
|
| 401 |
+
m.AddImplication(x, y)
|
| 402 |
+
m.AddImplication(y, x)
|
| 403 |
+
self.n_ties += 1
|
| 404 |
+
|
| 405 |
+
def _closures(self):
|
| 406 |
+
m, net = self.m, self.net
|
| 407 |
+
for c in self.task.closures:
|
| 408 |
+
if c.kind != "closed":
|
| 409 |
+
continue
|
| 410 |
+
lo_s, hi_s = sorted((net.index[c.where[0]], net.index[c.where[-1]]))
|
| 411 |
+
for rid, R in self.rv.items():
|
| 412 |
+
r = R["run"]
|
| 413 |
+
for i, ui in R["u"].items():
|
| 414 |
+
if isinstance(ui, int) and ui == 0:
|
| 415 |
+
continue
|
| 416 |
+
sec = net.section(r.route[i], r.route[i + 1])
|
| 417 |
+
if not lo_s <= sec < hi_s:
|
| 418 |
+
continue
|
| 419 |
+
if R["lo"][i] >= c.end or R["hi_arr"][i + 1] + self.L <= c.start:
|
| 420 |
+
continue
|
| 421 |
+
lits = [ui] if not isinstance(ui, int) else []
|
| 422 |
+
z = m.NewBoolVar("")
|
| 423 |
+
m.Add(R["arr"][i + 1] <= c.start).OnlyEnforceIf([z] + lits)
|
| 424 |
+
m.Add(R["dep"][i] >= c.end).OnlyEnforceIf([z.Not()] + lits)
|
| 425 |
+
|
| 426 |
+
def _stations(self):
|
| 427 |
+
m, net = self.m, self.net
|
| 428 |
+
by_pt: dict[str, list] = {}
|
| 429 |
+
for rid, R in self.rv.items():
|
| 430 |
+
r = R["run"]
|
| 431 |
+
d = run_dir(net, r)
|
| 432 |
+
for i, lit in R["inner"].items():
|
| 433 |
+
if isinstance(lit, int) and lit == 0:
|
| 434 |
+
continue
|
| 435 |
+
code = r.route[i]
|
| 436 |
+
p = net.by_code[code]
|
| 437 |
+
if p.tracks is None or p.kind == "junction" or i == len(r.route) - 1:
|
| 438 |
+
continue
|
| 439 |
+
end = m.NewIntVar(0, self.H + 1, "")
|
| 440 |
+
m.Add(end == R["dep"][i] + 1)
|
| 441 |
+
size = m.NewIntVar(1, self.H + 1, "")
|
| 442 |
+
iv = m.NewOptionalIntervalVar(R["arr"][i], size, end, lit, f"p_{rid}_{i}")
|
| 443 |
+
by_pt.setdefault(code, []).append((iv, d))
|
| 444 |
+
for code, start, depv, lit in self.waits:
|
| 445 |
+
s0 = start if start is not None else 0
|
| 446 |
+
if isinstance(s0, int):
|
| 447 |
+
sv = m.NewConstant(s0)
|
| 448 |
+
else:
|
| 449 |
+
sv = s0
|
| 450 |
+
end = m.NewIntVar(0, self.H + 2, "")
|
| 451 |
+
m.Add(end == depv + 1)
|
| 452 |
+
size = m.NewIntVar(0, self.H + 2, "")
|
| 453 |
+
iv = m.NewOptionalIntervalVar(sv, size, end, lit, f"wait_{code}")
|
| 454 |
+
by_pt.setdefault(code, []).append((iv, 0))
|
| 455 |
+
for code, lst in by_pt.items():
|
| 456 |
+
p = net.by_code[code]
|
| 457 |
+
outs = [c for c in self.task.closures if c.kind == "track" and c.where[0] == code]
|
| 458 |
+
fixed = [m.NewFixedSizeIntervalVar(c.start, c.end - c.start, "") for c in outs]
|
| 459 |
+
if p.directional:
|
| 460 |
+
for d in (1, -1):
|
| 461 |
+
ivs = [iv for iv, dd in lst if dd in (d, 0)]
|
| 462 |
+
if len(ivs) + len(fixed) > 1:
|
| 463 |
+
m.AddNoOverlap(ivs + fixed)
|
| 464 |
+
else:
|
| 465 |
+
ivs = [iv for iv, _ in lst] + fixed
|
| 466 |
+
if len(ivs) > p.tracks:
|
| 467 |
+
m.AddCumulative(ivs, [1] * len(ivs), p.tracks)
|
| 468 |
+
|
| 469 |
+
def _build(self):
|
| 470 |
+
for r in self.task.runs:
|
| 471 |
+
self._run_vars(r)
|
| 472 |
+
self._units()
|
| 473 |
+
self._closures()
|
| 474 |
+
self.n_pairs = 0
|
| 475 |
+
if self.interactions:
|
| 476 |
+
self._sections()
|
| 477 |
+
self._stations()
|
| 478 |
+
self.obj = sum(self.cost_terms) + self.const_cost
|
| 479 |
+
# each train set costs at least what it costs alone with the closures (the floor's part): exact, and it
|
| 480 |
+
# lifts the solver's bound to the floor at once, so only the interactions are left to prove
|
| 481 |
+
for uid, f in self.unit_floor.items():
|
| 482 |
+
if uid in self.unit_terms:
|
| 483 |
+
self.m.Add(sum(self.unit_terms[uid]) + self.unit_const[uid] >= f)
|
| 484 |
+
if self.unit_floor: # CP-SAT's bound does not add the parts up by itself
|
| 485 |
+
self.m.Add(self.obj >= sum(self.unit_floor.values()))
|
| 486 |
+
self.m.Minimize(self.obj)
|
| 487 |
+
|
| 488 |
+
def unit_costs(self, solver) -> dict[str, int]:
|
| 489 |
+
return {uid: int(sum(solver.Value(t) for t in terms)) + self.unit_const[uid]
|
| 490 |
+
for uid, terms in self.unit_terms.items()}
|
| 491 |
+
|
| 492 |
+
# ---------------------------------------------------------------- solution
|
| 493 |
+
def plan(self, solver) -> Plan:
|
| 494 |
+
plan: Plan = {}
|
| 495 |
+
for rid, R in self.rv.items():
|
| 496 |
+
r = R["run"]
|
| 497 |
+
a = next(a for a, ov in enumerate(R["o"]) if solver.Value(ov))
|
| 498 |
+
op = R["opts"][a]
|
| 499 |
+
if op is None:
|
| 500 |
+
plan[rid] = Order(cancel=True)
|
| 501 |
+
continue
|
| 502 |
+
s, e = op
|
| 503 |
+
o = Order(start=r.route[s] if s > 0 else None, end=r.route[e] if e < len(r.route) - 1 else None)
|
| 504 |
+
first = R["f0"] if r.started else s
|
| 505 |
+
for i in range(first, e):
|
| 506 |
+
if self.net.by_code[r.route[i]].kind == "junction":
|
| 507 |
+
continue
|
| 508 |
+
o.hold[r.route[i]] = solver.Value(R["dep"][i])
|
| 509 |
+
plan[rid] = o
|
| 510 |
+
return _tidy(self.task, plan)
|
| 511 |
+
|
| 512 |
+
def hint(self, plan: Plan):
|
| 513 |
+
"""Hint the solver with a plan's times (from `check.simulate`)."""
|
| 514 |
+
from .check import simulate
|
| 515 |
+
res, *_ = simulate(self.task, plan)
|
| 516 |
+
for rid, R in self.rv.items():
|
| 517 |
+
x = res.get(rid)
|
| 518 |
+
o = plan.get(rid) or Order()
|
| 519 |
+
r = R["run"]
|
| 520 |
+
if x is None:
|
| 521 |
+
continue
|
| 522 |
+
if x.cancelled:
|
| 523 |
+
key = None
|
| 524 |
+
else:
|
| 525 |
+
key = (0 if r.started else x.first, x.last)
|
| 526 |
+
for a, op in enumerate(R["opts"]):
|
| 527 |
+
self.m.AddHint(R["o"][a], int(op == key))
|
| 528 |
+
if x.cancelled:
|
| 529 |
+
continue
|
| 530 |
+
for i, t in x.arr.items():
|
| 531 |
+
if i in R["arr"]:
|
| 532 |
+
self.m.AddHint(R["arr"][i], t)
|
| 533 |
+
for i, t in x.dep.items():
|
| 534 |
+
if i in R["dep"]:
|
| 535 |
+
self.m.AddHint(R["dep"][i], t)
|
| 536 |
+
for i, wv in R["w"].items():
|
| 537 |
+
self.m.AddHint(wv, int(bool(x.stopped.get(i)) and not r.stops[i] and i != x.first and i != x.last))
|
| 538 |
+
|
| 539 |
+
|
| 540 |
+
def _tidy(task: Task, plan: Plan) -> Plan:
|
| 541 |
+
"""Drop holds that change nothing (the train would leave then anyway), so reference plans read cleanly."""
|
| 542 |
+
from .check import simulate
|
| 543 |
+
out: Plan = {}
|
| 544 |
+
for rid, o in plan.items():
|
| 545 |
+
if o.cancel:
|
| 546 |
+
out[rid] = o
|
| 547 |
+
continue
|
| 548 |
+
out[rid] = Order(start=o.start, end=o.end, hold=dict(o.hold))
|
| 549 |
+
base, *_ = simulate(task, out)
|
| 550 |
+
for rid, o in list(out.items()):
|
| 551 |
+
if o.cancel:
|
| 552 |
+
continue
|
| 553 |
+
for code in list(o.hold):
|
| 554 |
+
trial = Order(start=o.start, end=o.end, hold={k: v for k, v in o.hold.items() if k != code})
|
| 555 |
+
res, *_ = simulate(task, {**out, rid: trial})
|
| 556 |
+
if res[rid].arr == base[rid].arr and res[rid].dep == base[rid].dep:
|
| 557 |
+
out[rid] = trial
|
| 558 |
+
o = trial
|
| 559 |
+
return {rid: o for rid, o in out.items() if o.cancel or o.start or o.end or o.hold}
|
| 560 |
+
|
| 561 |
+
|
| 562 |
+
def solve(task: Task, time_limit: float = 60.0, workers: int = 4, hint: Plan | None = None,
|
| 563 |
+
interactions: bool = True, log: bool = False, unit_floor: dict[str, int] | None = None) -> Solution:
|
| 564 |
+
t0 = time.time()
|
| 565 |
+
M = _Model(task, interactions=interactions, unit_floor=unit_floor)
|
| 566 |
+
if hint is not None:
|
| 567 |
+
M.hint(hint)
|
| 568 |
+
solver = cp_model.CpSolver()
|
| 569 |
+
solver.parameters.max_time_in_seconds = time_limit
|
| 570 |
+
solver.parameters.num_workers = workers
|
| 571 |
+
solver.parameters.log_search_progress = log
|
| 572 |
+
st = solver.Solve(M.m)
|
| 573 |
+
name = solver.StatusName(st)
|
| 574 |
+
if st not in (cp_model.OPTIMAL, cp_model.FEASIBLE):
|
| 575 |
+
return Solution(None, None, int(solver.BestObjectiveBound()) if st != cp_model.INFEASIBLE else 0, False, name,
|
| 576 |
+
time.time() - t0)
|
| 577 |
+
plan = M.plan(solver) if interactions else None
|
| 578 |
+
return Solution(plan, int(round(solver.ObjectiveValue())), int(round(solver.BestObjectiveBound())),
|
| 579 |
+
st == cp_model.OPTIMAL, name, time.time() - t0)
|
| 580 |
+
|
| 581 |
+
|
| 582 |
+
def floor_cost(task: Task, time_limit: float = 30.0) -> int:
|
| 583 |
+
"""Lower bound: each train set alone with the closures (no interactions)."""
|
| 584 |
+
return floor_parts(task, time_limit)[0]
|
| 585 |
+
|
| 586 |
+
|
| 587 |
+
def floor_parts(task: Task, time_limit: float = 30.0) -> tuple[int, dict[str, int]]:
|
| 588 |
+
"""(floor, each train set's own part). Without interactions the sets are independent, so a proven optimum is
|
| 589 |
+
the sum of each set's optimum alone; the parts are only returned when that is proven."""
|
| 590 |
+
M = _Model(task, interactions=False)
|
| 591 |
+
solver = cp_model.CpSolver()
|
| 592 |
+
solver.parameters.max_time_in_seconds = time_limit
|
| 593 |
+
solver.parameters.num_workers = 4
|
| 594 |
+
st = solver.Solve(M.m)
|
| 595 |
+
if st == cp_model.OPTIMAL:
|
| 596 |
+
return int(round(solver.ObjectiveValue())), M.unit_costs(solver)
|
| 597 |
+
return int(solver.BestObjectiveBound()) if st == cp_model.FEASIBLE else 0, {}
|
| 598 |
+
|
| 599 |
+
|
| 600 |
+
def optimal_plan(task: Task, time_limit: float = 60.0, workers: int = 4, hint: Plan | None = None,
|
| 601 |
+
unit_floor: dict[str, int] | None = None) -> Solution:
|
| 602 |
+
sol = solve(task, time_limit=time_limit, workers=workers, hint=hint, unit_floor=unit_floor)
|
| 603 |
+
if sol.plan is not None:
|
| 604 |
+
res = evaluate(task, sol.plan)
|
| 605 |
+
if not res.feasible or res.cost != sol.cost:
|
| 606 |
+
raise AssertionError(f"{task.task_id}: solver plan re-checks to feasible={res.feasible} cost={res.cost}, "
|
| 607 |
+
f"solver said {sol.cost}: {res.violations[:3]}")
|
| 608 |
+
return sol
|
openenv.yaml
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
spec_version: 1
|
| 2 |
+
name: rail_ops
|
| 3 |
+
type: space
|
| 4 |
+
runtime: fastapi
|
| 5 |
+
app: rail_openenv.server:app
|
| 6 |
+
port: 7860
|
| 7 |
+
description: Rail dispatch on Arriva's single-track Groningen - Leeuwarden line (Netherlands) on real January-June 2025 days - hold, short-turn and cancel trains so opposing trains cross safely after real breakdowns, collisions, signal and points failures (and seeded ones drawn from the 2024-2025 disruption record); graded once on submit against the CP-SAT optimum. 45 eval tasks on held-out weeks, chosen for hardness, and a train pack on the other days.
|
| 8 |
+
version: "0.1.0"
|
| 9 |
+
variables:
|
| 10 |
+
RAIL_TASKS_DIR: "" # colon-separated pack directories; defaults to tasks/rail-v1-eval:tasks/rail-v1-train
|
| 11 |
+
RAIL_MAX_CHECKS: "10"
|
| 12 |
+
RAIL_MAX_TOOL_CALLS: "24"
|
| 13 |
+
MAX_CONCURRENT_ENVS: "64"
|
| 14 |
+
validation:
|
| 15 |
+
reward:
|
| 16 |
+
range: [0.0, 1.0]
|
| 17 |
+
# oracle/solve.py submits the CP-SAT optimum and scores exactly 1.0; no plan scores 0, any rule broken < 0.2
|
| 18 |
+
oracle_tolerance: 0.001
|
| 19 |
+
floor_margin: 0.5
|
| 20 |
+
resources:
|
| 21 |
+
# pure Python: the grader re-simulates a plan in milliseconds, CP-SAT never runs on the server
|
| 22 |
+
cpu: 2.0
|
| 23 |
+
memory_mb: 2048
|
| 24 |
+
disk_mb: 1024
|
| 25 |
+
episode_timeout_s: 3600.0
|
| 26 |
+
network:
|
| 27 |
+
mode: no-network
|
| 28 |
+
capabilities:
|
| 29 |
+
verifier:
|
| 30 |
+
kind: reward_channel
|
| 31 |
+
oracle:
|
| 32 |
+
form: script
|
| 33 |
+
location: oracle/solve.py
|
| 34 |
+
rubric_tree: true
|
| 35 |
+
task_api: true
|
| 36 |
+
declared_tools: [get_situation, check_plan, submit_plan]
|
| 37 |
+
types:
|
| 38 |
+
tags: [simulation, railway, dispatching, scheduling, operations-research, mcp, real-world-data]
|
openenv/rail_openenv/__init__.py
ADDED
|
File without changes
|
openenv/rail_openenv/environment.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""The OpenEnv environment: one dispatching episode per session, three MCP tools.
|
| 2 |
+
|
| 3 |
+
get_situation() the line, closures, trains (timetables, positions, delays) and train sets
|
| 4 |
+
check_plan(plan) simulates a draft: every train's times, the rules it breaks and the cost (budgeted; never a
|
| 5 |
+
score or a reference)
|
| 6 |
+
submit_plan(plan) the final answer: ends the episode; the rubric grades it once
|
| 7 |
+
|
| 8 |
+
`reset(task_id=...)` or `reset(split=, index=)` (or `seed=` for a random task) picks a task. Tasks are served through
|
| 9 |
+
OpenEnv's Task API (`list_splits`, `num_tasks`, `get_task`, `get_task_range`). The reward is `RailPlanRubric` applied
|
| 10 |
+
to the `submit_plan` action; every other step has reward 0.0. An episode that runs out of tool calls ends with 0.0.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import json
|
| 16 |
+
import os
|
| 17 |
+
import random
|
| 18 |
+
import threading
|
| 19 |
+
import time
|
| 20 |
+
import uuid
|
| 21 |
+
from collections import OrderedDict
|
| 22 |
+
from typing import Annotated, Any
|
| 23 |
+
|
| 24 |
+
from fastmcp import FastMCP
|
| 25 |
+
from openenv.core.env_server.mcp_environment import MCPEnvironment
|
| 26 |
+
from openenv.core.env_server.mcp_types import CallToolAction
|
| 27 |
+
from openenv.core.env_server.types import Action, EnvironmentMetadata, Observation, State
|
| 28 |
+
from pydantic import Field
|
| 29 |
+
|
| 30 |
+
from rail_core import (PROMPT_VERSION, PlanError, Task, TaskPack, load_pack, parse_plan, plan_report, plan_to_list,
|
| 31 |
+
rules, situation)
|
| 32 |
+
|
| 33 |
+
try:
|
| 34 |
+
from .rubric import SUBMIT_TOOL, RailPlanRubric
|
| 35 |
+
except ImportError: # flat layout
|
| 36 |
+
from rubric import SUBMIT_TOOL, RailPlanRubric
|
| 37 |
+
|
| 38 |
+
MAX_CHECKS = int(os.environ.get("RAIL_MAX_CHECKS", "10"))
|
| 39 |
+
MAX_TOOL_CALLS = int(os.environ.get("RAIL_MAX_TOOL_CALLS", "24"))
|
| 40 |
+
|
| 41 |
+
PlanArg = Annotated[list[dict[str, Any]] | str, Field(
|
| 42 |
+
description='one entry per train you change, e.g. [{"train": "37413", "to": "HDG"}, '
|
| 43 |
+
'{"train": "37424", "from": "HDG", "hold": {"HDG": "07:31"}}, {"train": "37419", "cancel": true}]; '
|
| 44 |
+
'trains not listed run in full, as early as they can')]
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class RailState(State):
|
| 48 |
+
task_id: str | None = None
|
| 49 |
+
split: str | None = None
|
| 50 |
+
checks_used: int = 0
|
| 51 |
+
tool_calls: int = 0
|
| 52 |
+
done: bool = False
|
| 53 |
+
reward: float | None = None
|
| 54 |
+
grade: dict | None = None
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class Episode:
|
| 58 |
+
"""Everything one episode did (bounded store, newest first), for /api/episodes."""
|
| 59 |
+
|
| 60 |
+
def __init__(self, task: Task, episode_id: str):
|
| 61 |
+
self.task = task
|
| 62 |
+
self.episode_id = episode_id
|
| 63 |
+
self.started = time.time()
|
| 64 |
+
self.steps: list[dict] = []
|
| 65 |
+
self.done = False
|
| 66 |
+
self.grade: dict | None = None
|
| 67 |
+
self.end_reason: str | None = None
|
| 68 |
+
|
| 69 |
+
def summary(self) -> dict:
|
| 70 |
+
return {"episode_id": self.episode_id, "task_id": self.task.task_id, "started": self.started,
|
| 71 |
+
"done": self.done, "end_reason": self.end_reason, "steps": self.steps, "grade": self.grade}
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class _Store:
|
| 75 |
+
def __init__(self, cap: int = 200):
|
| 76 |
+
self.cap = cap
|
| 77 |
+
self._d: OrderedDict[str, Episode] = OrderedDict()
|
| 78 |
+
self._lock = threading.Lock()
|
| 79 |
+
|
| 80 |
+
def add(self, ep: Episode):
|
| 81 |
+
with self._lock:
|
| 82 |
+
self._d[ep.episode_id] = ep
|
| 83 |
+
while len(self._d) > self.cap:
|
| 84 |
+
self._d.popitem(last=False)
|
| 85 |
+
|
| 86 |
+
def get(self, eid: str) -> Episode | None:
|
| 87 |
+
return self._d.get(eid)
|
| 88 |
+
|
| 89 |
+
def recent(self, n: int = 50) -> list[Episode]:
|
| 90 |
+
return list(reversed(self._d.values()))[:n]
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
STORE = _Store()
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def _json(obj: Any) -> str:
|
| 97 |
+
return json.dumps(obj, ensure_ascii=False, separators=(",", ":"))
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
class RailOpsEnvironment(MCPEnvironment):
|
| 101 |
+
"""Re-plan Arriva's trains on the single-track Groningen - Leeuwarden line on a real 2025 day."""
|
| 102 |
+
|
| 103 |
+
SUPPORTS_CONCURRENT_SESSIONS = True
|
| 104 |
+
|
| 105 |
+
def get_metadata(self) -> EnvironmentMetadata:
|
| 106 |
+
return EnvironmentMetadata(
|
| 107 |
+
name="RailSimEnv v1",
|
| 108 |
+
description="Dispatch Arriva's trains on the single-track Groningen - Leeuwarden line (Netherlands) on a "
|
| 109 |
+
"real 2025 day: real timetables, delays and disruptions from the Rijden de Treinen record. "
|
| 110 |
+
"Hold, short-turn or cancel trains so they cross safely. Tools: get_situation, check_plan "
|
| 111 |
+
"(10 per episode), submit_plan (graded once, deterministically, against the CP-SAT optimum).",
|
| 112 |
+
version="1.0.0",
|
| 113 |
+
author="RLSimEnv",
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
def __init__(self, pack: TaskPack | None = None):
|
| 117 |
+
self.pack = pack or load_pack()
|
| 118 |
+
self._episode: Episode | None = None
|
| 119 |
+
self._state = RailState()
|
| 120 |
+
mcp = FastMCP("rail_ops")
|
| 121 |
+
|
| 122 |
+
@mcp.tool
|
| 123 |
+
def get_situation() -> str:
|
| 124 |
+
"""The line, what has happened (closures), every train to plan and every train set."""
|
| 125 |
+
ep = self._require()
|
| 126 |
+
return situation(ep.task)
|
| 127 |
+
|
| 128 |
+
@mcp.tool
|
| 129 |
+
def check_plan(plan: PlanArg) -> str:
|
| 130 |
+
"""Simulate your plan: every train's times, its delay and the stops it loses, every rule it breaks, and
|
| 131 |
+
the cost. Uses one of your checks. It does not grade the plan."""
|
| 132 |
+
ep = self._require(open_only=True)
|
| 133 |
+
if self._state.checks_used >= MAX_CHECKS:
|
| 134 |
+
return _json({"error": f"no checks left (you had {MAX_CHECKS}); submit_plan when ready"})
|
| 135 |
+
self._state.checks_used += 1
|
| 136 |
+
try:
|
| 137 |
+
parsed, problems = parse_plan(ep.task, plan)
|
| 138 |
+
except PlanError as e:
|
| 139 |
+
out = {"error": str(e), "checks_left": MAX_CHECKS - self._state.checks_used}
|
| 140 |
+
ep.steps.append({"turn": self._state.tool_calls, "tool": "check_plan", "plan": None, "result": out})
|
| 141 |
+
return _json(out)
|
| 142 |
+
out = plan_report(ep.task, parsed, problems)
|
| 143 |
+
out["checks_left"] = MAX_CHECKS - self._state.checks_used
|
| 144 |
+
ep.steps.append({"turn": self._state.tool_calls, "tool": "check_plan", "plan": plan_to_list(parsed),
|
| 145 |
+
"result": {k: v for k, v in out.items() if k != "trains"}})
|
| 146 |
+
return _json(out)
|
| 147 |
+
|
| 148 |
+
@mcp.tool
|
| 149 |
+
def submit_plan(plan: PlanArg) -> str:
|
| 150 |
+
"""Submit your final plan. Ends the episode; the plan is graded once."""
|
| 151 |
+
ep = self._require(open_only=True)
|
| 152 |
+
try:
|
| 153 |
+
parsed, problems = parse_plan(ep.task, plan)
|
| 154 |
+
except PlanError as e:
|
| 155 |
+
parsed, problems = {}, [str(e)]
|
| 156 |
+
# The rubric grades the same arguments again in step(); this result is what the agent reads.
|
| 157 |
+
g = self.rubric.graded(CallToolAction(tool_name=SUBMIT_TOOL, arguments={"plan": plan}))
|
| 158 |
+
ep.done = True
|
| 159 |
+
ep.end_reason = "submitted"
|
| 160 |
+
ep.grade = g.as_dict() if g else None
|
| 161 |
+
ep.steps.append({"turn": self._state.tool_calls, "tool": "submit_plan", "plan": plan_to_list(parsed),
|
| 162 |
+
"result": {"grade": ep.grade}})
|
| 163 |
+
out = {"submitted": True, "feasible": g.feasible if g else False, "reward": g.reward if g else 0.0}
|
| 164 |
+
if g and g.feasible:
|
| 165 |
+
out.update(cost=g.cost, delay_cost=g.delay_cost, unserved_stops_cost=g.cancel_cost,
|
| 166 |
+
handed_on_cost=g.carry_cost, cancelled=g.cancelled, short_turned=g.short_turned)
|
| 167 |
+
elif g:
|
| 168 |
+
out.update(violations=g.violations[:20], entry_problems=g.parse_problems)
|
| 169 |
+
return _json(out)
|
| 170 |
+
|
| 171 |
+
super().__init__(mcp)
|
| 172 |
+
self.rubric = RailPlanRubric()
|
| 173 |
+
|
| 174 |
+
# ------------------------------------------------------------ Task API
|
| 175 |
+
|
| 176 |
+
def list_splits(self):
|
| 177 |
+
return [{"name": s, "type": s, "num_tasks": self.pack.count(s)} for s in self.pack.splits()]
|
| 178 |
+
|
| 179 |
+
def num_tasks(self, split):
|
| 180 |
+
return self.pack.count(split)
|
| 181 |
+
|
| 182 |
+
def get_task(self, split, index):
|
| 183 |
+
return self.pack.public(self.pack.at(split, int(index)))
|
| 184 |
+
|
| 185 |
+
def get_task_range(self, split, start=None, stop=None):
|
| 186 |
+
n = self.pack.count(split)
|
| 187 |
+
start, stop = int(start or 0), int(n if stop is None else stop)
|
| 188 |
+
return [self.pack.public(self.pack.at(split, i)) for i in range(max(0, start), min(n, stop))]
|
| 189 |
+
|
| 190 |
+
def list_tasks(self, split):
|
| 191 |
+
return self.get_task_range(split)
|
| 192 |
+
|
| 193 |
+
# ------------------------------------------------------------ lifecycle
|
| 194 |
+
|
| 195 |
+
def reset(self, seed: int | None = None, episode_id: str | None = None, split: str | None = None,
|
| 196 |
+
index: int | None = None, task_id: str | None = None, **kwargs: Any) -> Observation:
|
| 197 |
+
if task_id is not None:
|
| 198 |
+
task = self.pack.get(task_id)
|
| 199 |
+
else:
|
| 200 |
+
split = split or ("train" if "train" in self.pack.splits() else self.pack.splits()[0])
|
| 201 |
+
n = self.pack.count(split)
|
| 202 |
+
index = random.Random(seed).randrange(n) if index is None else int(index)
|
| 203 |
+
task = self.pack.at(split, index)
|
| 204 |
+
ep = Episode(task, str(episode_id)[:64] if episode_id else uuid.uuid4().hex[:12])
|
| 205 |
+
STORE.add(ep)
|
| 206 |
+
self._episode = ep
|
| 207 |
+
self._state = RailState(episode_id=ep.episode_id, step_count=0, task_id=task.task_id, split=task.split)
|
| 208 |
+
self._reset_rubric()
|
| 209 |
+
self.rubric.set_task(task)
|
| 210 |
+
return Observation(done=False, reward=None, metadata={
|
| 211 |
+
"episode_id": ep.episode_id,
|
| 212 |
+
"task_id": task.task_id,
|
| 213 |
+
"split": task.split,
|
| 214 |
+
"difficulty": task.difficulty,
|
| 215 |
+
"instructions": rules(task, MAX_CHECKS),
|
| 216 |
+
"situation": situation(task),
|
| 217 |
+
"max_checks": MAX_CHECKS,
|
| 218 |
+
"max_tool_calls": MAX_TOOL_CALLS,
|
| 219 |
+
"tools": ["get_situation", "check_plan", "submit_plan"],
|
| 220 |
+
"prompt_version": PROMPT_VERSION,
|
| 221 |
+
})
|
| 222 |
+
|
| 223 |
+
def _require(self, open_only: bool = False) -> Episode:
|
| 224 |
+
if self._episode is None:
|
| 225 |
+
raise ValueError("no episode: call reset first")
|
| 226 |
+
if open_only and self._episode.done:
|
| 227 |
+
raise ValueError("the episode is over: the plan was already submitted")
|
| 228 |
+
return self._episode
|
| 229 |
+
|
| 230 |
+
@property
|
| 231 |
+
def state(self) -> RailState:
|
| 232 |
+
return self._state
|
| 233 |
+
|
| 234 |
+
# ------------------------------------------------------------ stepping
|
| 235 |
+
|
| 236 |
+
def _after_step(self, action: Action, obs: Observation) -> Observation:
|
| 237 |
+
ep = self._episode
|
| 238 |
+
if ep is None or not isinstance(action, CallToolAction):
|
| 239 |
+
return obs
|
| 240 |
+
self._state.tool_calls += 1
|
| 241 |
+
self._state.step_count += 1
|
| 242 |
+
obs.reward = self._apply_rubric(action, obs) # 0.0 unless this was submit_plan
|
| 243 |
+
if ep.done:
|
| 244 |
+
g = self.rubric.last_grade
|
| 245 |
+
obs.done = True
|
| 246 |
+
obs.metadata = {**(obs.metadata or {}), "grade": g.as_dict() if g else ep.grade,
|
| 247 |
+
"rubric": {name: r.last_score for name, r in self.rubric.named_rubrics()},
|
| 248 |
+
"episode_id": ep.episode_id}
|
| 249 |
+
self._state.done, self._state.reward = True, obs.reward
|
| 250 |
+
self._state.grade = obs.metadata["grade"]
|
| 251 |
+
elif self._state.tool_calls >= MAX_TOOL_CALLS:
|
| 252 |
+
ep.done, ep.end_reason = True, "tool_call_limit"
|
| 253 |
+
obs.done, obs.reward = True, 0.0
|
| 254 |
+
obs.metadata = {**(obs.metadata or {}), "end_reason": "tool_call_limit", "episode_id": ep.episode_id}
|
| 255 |
+
self._state.done, self._state.reward = True, 0.0
|
| 256 |
+
return obs
|
| 257 |
+
|
| 258 |
+
def step(self, action: Action, timeout_s: float | None = None, **kwargs: Any) -> Observation:
|
| 259 |
+
return self._after_step(action, super().step(action, timeout_s=timeout_s, **kwargs))
|
| 260 |
+
|
| 261 |
+
async def step_async(self, action: Action, timeout_s: float | None = None, **kwargs: Any) -> Observation:
|
| 262 |
+
return self._after_step(action, await super().step_async(action, timeout_s=timeout_s, **kwargs))
|
| 263 |
+
|
| 264 |
+
def _step_impl(self, action: Action, timeout_s: float | None = None, **kwargs) -> Observation:
|
| 265 |
+
return Observation(done=False, reward=0.0,
|
| 266 |
+
metadata={"error": "use the MCP tools: get_situation, check_plan, submit_plan"})
|
openenv/rail_openenv/gradio_ui.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""The "Explore" tab of OpenEnv's web UI (/web): the eval explorer, the 3D replay and the train graph, each the
|
| 2 |
+
server's own page in a frame, one tab each. OpenEnv's Playground tab (reset, step and the MCP tools) stays next to it.
|
| 3 |
+
|
| 4 |
+
A frame is only created when its tab is first opened, so the 3D replay does not start WebGL in a hidden tab.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import html
|
| 10 |
+
|
| 11 |
+
import gradio as gr
|
| 12 |
+
|
| 13 |
+
PAGES = [("Eval explorer", "/explorer/?embed=1"), ("3D replay", "/twin/"), ("Train graph", "/viewer/?embed=1")]
|
| 14 |
+
# OpenEnv's page caps the container width; the pages want the full window.
|
| 15 |
+
STYLE = ("<style>.gradio-container .main.fillable{max-width:none!important;padding-left:12px!important;"
|
| 16 |
+
"padding-right:12px!important}.gradio-container .html-container:has(iframe){padding:0!important}</style>")
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def frame(src: str, title: str) -> str:
|
| 20 |
+
return (f'{STYLE}<iframe src="{html.escape(src)}" title="RailSimEnv {html.escape(title)}" allow="fullscreen" '
|
| 21 |
+
'style="width:100%;height:calc(100vh - 190px);min-height:720px;border:0;display:block"></iframe>')
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def build_ui(*_args, **_kwargs) -> gr.Blocks:
|
| 25 |
+
"""OpenEnv's `gradio_builder` hook; its arguments (web manager, action fields, metadata, ...) are not needed."""
|
| 26 |
+
with gr.Blocks(title="RailSimEnv v1") as ui:
|
| 27 |
+
with gr.Tabs():
|
| 28 |
+
for i, (name, src) in enumerate(PAGES):
|
| 29 |
+
with gr.Tab(name) as tab:
|
| 30 |
+
out = gr.HTML(frame(src, name) if i == 0 else "")
|
| 31 |
+
loaded = gr.State(i == 0)
|
| 32 |
+
|
| 33 |
+
def show(done: bool, src: str = src, name: str = name):
|
| 34 |
+
return (gr.update(), True) if done else (frame(src, name), True)
|
| 35 |
+
|
| 36 |
+
tab.select(show, inputs=loaded, outputs=[out, loaded])
|
| 37 |
+
return ui
|
openenv/rail_openenv/rubric.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""The reward as OpenEnv rubrics (RFC 004).
|
| 2 |
+
|
| 3 |
+
`RailPlanRubric` scores the `submit_plan` call and returns 0.0 for every other action, so the grader runs once per
|
| 4 |
+
episode. Its two children are the inputs of the score and show up in `named_rubrics()` with their `last_score`:
|
| 5 |
+
|
| 6 |
+
runs_clean fraction of runs with no rule broken (entry problems count as unclean)
|
| 7 |
+
cost_quality exp(-g / 0.5), g the gap to the proven optimum over the avoidable cost; 0 when infeasible
|
| 8 |
+
|
| 9 |
+
The combination is `rail_core.reward.score` (infeasible: 0.2 x runs_clean; feasible: 0.2 + 0.8 x cost_quality),
|
| 10 |
+
shared with the offline grader so the two can never disagree.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
from typing import Any
|
| 16 |
+
|
| 17 |
+
from openenv.core.env_server.mcp_types import CallToolAction
|
| 18 |
+
from openenv.core.rubrics import Rubric
|
| 19 |
+
|
| 20 |
+
from rail_core import Grade, PlanError, Task, grade, parse_plan
|
| 21 |
+
|
| 22 |
+
SUBMIT_TOOL = "submit_plan"
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def submitted_plan(action: Any):
|
| 26 |
+
if isinstance(action, CallToolAction) and action.tool_name == SUBMIT_TOOL:
|
| 27 |
+
return (action.arguments or {}).get("plan")
|
| 28 |
+
return None
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class _Graded(Rubric):
|
| 32 |
+
"""Grades the submitted plan once per action and shares the result with sibling rubrics."""
|
| 33 |
+
|
| 34 |
+
def __init__(self):
|
| 35 |
+
super().__init__()
|
| 36 |
+
self.task: Task | None = None
|
| 37 |
+
self._cache: tuple[int, Grade | None] | None = None
|
| 38 |
+
|
| 39 |
+
def graded(self, action: Any) -> Grade | None:
|
| 40 |
+
if self._cache is not None and self._cache[0] == id(action):
|
| 41 |
+
return self._cache[1]
|
| 42 |
+
raw = submitted_plan(action)
|
| 43 |
+
g = None
|
| 44 |
+
if raw is not None and self.task is not None:
|
| 45 |
+
try:
|
| 46 |
+
plan, problems = parse_plan(self.task, raw)
|
| 47 |
+
except PlanError as e:
|
| 48 |
+
plan, problems = {}, [str(e)]
|
| 49 |
+
g = grade(self.task, plan, problems)
|
| 50 |
+
self._cache = (id(action), g)
|
| 51 |
+
return g
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class RunsClean(Rubric):
|
| 55 |
+
def __init__(self, source: _Graded):
|
| 56 |
+
super().__init__()
|
| 57 |
+
object.__setattr__(self, "_source", source) # not a child: avoid a cycle in named_rubrics()
|
| 58 |
+
|
| 59 |
+
def forward(self, action, observation) -> float:
|
| 60 |
+
g = self._source.graded(action)
|
| 61 |
+
return 0.0 if g is None else g.clean_fraction
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
class CostQuality(Rubric):
|
| 65 |
+
def __init__(self, source: _Graded):
|
| 66 |
+
super().__init__()
|
| 67 |
+
object.__setattr__(self, "_source", source)
|
| 68 |
+
|
| 69 |
+
def forward(self, action, observation) -> float:
|
| 70 |
+
g = self._source.graded(action)
|
| 71 |
+
return 0.0 if g is None else g.quality
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class RailPlanRubric(_Graded):
|
| 75 |
+
"""Terminal reward for a dispatch plan: 0.0 until `submit_plan`, then the score in [0, 1]."""
|
| 76 |
+
|
| 77 |
+
def __init__(self):
|
| 78 |
+
super().__init__()
|
| 79 |
+
self.runs_clean = RunsClean(self)
|
| 80 |
+
self.cost_quality = CostQuality(self)
|
| 81 |
+
self.last_grade: Grade | None = None
|
| 82 |
+
|
| 83 |
+
def set_task(self, task: Task) -> None:
|
| 84 |
+
self.task = task
|
| 85 |
+
self._cache = None
|
| 86 |
+
self.last_grade = None
|
| 87 |
+
|
| 88 |
+
def forward(self, action, observation) -> float:
|
| 89 |
+
g = self.graded(action)
|
| 90 |
+
self.runs_clean(action, observation)
|
| 91 |
+
self.cost_quality(action, observation)
|
| 92 |
+
if g is None:
|
| 93 |
+
return 0.0
|
| 94 |
+
self.last_grade = g
|
| 95 |
+
return g.reward
|
| 96 |
+
|
| 97 |
+
def reset(self) -> None:
|
| 98 |
+
self._cache = None
|
| 99 |
+
self.last_grade = None
|
openenv/rail_openenv/server.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""The server: OpenEnv's app plus the map replay, a train-graph viewer and their read-only JSON on the same port.
|
| 2 |
+
|
| 3 |
+
OpenEnv provides `/reset`, `/step`, `/state`, `/schema`, `/ws`, `/mcp`, `/health`, the Task API under `/rail_ops/...`
|
| 4 |
+
and its web UI at `/web`, where an "Explore" tab (`gradio_ui.py`) frames the pages below next to OpenEnv's Playground.
|
| 5 |
+
We add:
|
| 6 |
+
|
| 7 |
+
/twin/ the map replay: trains moving on the line's OpenStreetMap track, any plan
|
| 8 |
+
/viewer/ a train graph (time against distance) of any plan, live episodes too
|
| 9 |
+
/explorer/ the eval explorer: the board and every model's plan on every eval task
|
| 10 |
+
/api/tasks task summaries, with the lazy baselines' rewards
|
| 11 |
+
/api/runs the committed model runs: each episode's reward and outcome, and a board
|
| 12 |
+
/api/tasks/{id} a public task (what the agent sees, structured)
|
| 13 |
+
/api/tasks/{id}/reference optimum, floor, baselines and the real outcome - for people; no tool shows them
|
| 14 |
+
/api/tasks/{id}/timeline?plan=... paths for the graph: plan = optimal | naive | greedy | greedy_fast
|
| 15 |
+
/api/episodes live episodes on this server (newest first)
|
| 16 |
+
/api/episodes/{episode_id}/timeline the latest plan an episode checked or submitted, as paths
|
| 17 |
+
/api/tasks/{id}/plans the plans the replay can show: optimum, baselines, models' submitted plans
|
| 18 |
+
(from results/rollouts/*/index.json, or RAIL_RUNS_DIR), live episodes
|
| 19 |
+
/api/tasks/{id}/replay?plan=... everything the replay animates (rail_core.replay)
|
| 20 |
+
/api/episodes/{episode_id}/replay the same for the latest plan of a live episode
|
| 21 |
+
|
| 22 |
+
uvicorn rail_openenv.server:app --port 8000
|
| 23 |
+
|
| 24 |
+
`/` is OpenEnv's: it redirects to the web UI (`/web/`), or to `/explorer/` when ENABLE_WEB_INTERFACE is false.
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
from __future__ import annotations
|
| 28 |
+
|
| 29 |
+
import json
|
| 30 |
+
import os
|
| 31 |
+
import random
|
| 32 |
+
import statistics
|
| 33 |
+
from functools import lru_cache
|
| 34 |
+
from pathlib import Path
|
| 35 |
+
|
| 36 |
+
from fastapi import HTTPException
|
| 37 |
+
from fastapi.responses import RedirectResponse
|
| 38 |
+
from fastapi.staticfiles import StaticFiles
|
| 39 |
+
from openenv.core.env_server.http_server import create_app
|
| 40 |
+
from openenv.core.env_server.mcp_types import CallToolAction, CallToolObservation
|
| 41 |
+
|
| 42 |
+
from rail_core import TaskPack, evaluate, hhmm, load_pack, parse_plan
|
| 43 |
+
from rail_core.baselines import POLICIES
|
| 44 |
+
from rail_core.check import network
|
| 45 |
+
from rail_core.replay import replay
|
| 46 |
+
|
| 47 |
+
try:
|
| 48 |
+
from .environment import STORE, RailOpsEnvironment, RailState
|
| 49 |
+
from .gradio_ui import build_ui
|
| 50 |
+
except ImportError:
|
| 51 |
+
from environment import STORE, RailOpsEnvironment, RailState
|
| 52 |
+
from gradio_ui import build_ui
|
| 53 |
+
|
| 54 |
+
ENV_NAME = "rail_ops"
|
| 55 |
+
WEB = Path(__file__).resolve().parent / "web"
|
| 56 |
+
RUNS_DIR = Path(os.environ.get("RAIL_RUNS_DIR", Path(__file__).resolve().parents[4] / "results" / "rollouts"))
|
| 57 |
+
BASELINES = {"greedy": "greedy dispatcher", "greedy_fast": "greedy dispatcher (turns early)",
|
| 58 |
+
"naive": "change nothing"}
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
TIERS = ("real", "blockage", "loop", "compound")
|
| 62 |
+
PROVIDERS = {"together": "Together", "baseten": "Baseten", "novita": "Novita", "deepinfra": "DeepInfra",
|
| 63 |
+
"fireworks": "Fireworks"}
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def model_name(spec: str) -> str:
|
| 67 |
+
"""A person's name for a run's model spec: "hf:Qwen/Qwen3.8-2.4T-A95B:together@default" -> "Qwen3.8-2.4T-A95B
|
| 68 |
+
(Together)", "claude-sonnet-5-5@high" -> "Claude Sonnet 5.5 (effort high)"."""
|
| 69 |
+
model, _, effort = spec.partition("@")
|
| 70 |
+
if model.startswith("hf:"):
|
| 71 |
+
repo, _, provider = model[3:].partition(":")
|
| 72 |
+
name = repo.split("/")[-1]
|
| 73 |
+
return f"{name} ({PROVIDERS.get(provider, provider)})" if provider else name
|
| 74 |
+
if model.startswith("claude-"):
|
| 75 |
+
parts = model.split("-")[1:]
|
| 76 |
+
family = " ".join(p.capitalize() for p in parts if not p.isdigit())
|
| 77 |
+
version = ".".join(p for p in parts if p.isdigit())
|
| 78 |
+
name = f"Claude {family} {version}".strip()
|
| 79 |
+
return f"{name} (effort {effort})" if effort and effort != "default" else name
|
| 80 |
+
return model
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def load_runs(runs_dir: Path = RUNS_DIR) -> list[dict]:
|
| 84 |
+
"""The committed rollout runs (results/rollouts/*/index.json): [{run, episodes}]."""
|
| 85 |
+
out = []
|
| 86 |
+
for idx in sorted(runs_dir.glob("*/index.json")) if runs_dir.is_dir() else []:
|
| 87 |
+
try:
|
| 88 |
+
body = json.loads(idx.read_text())
|
| 89 |
+
except (OSError, ValueError):
|
| 90 |
+
continue
|
| 91 |
+
out.append({"run": body["run"], "episodes": body.get("episodes", [])})
|
| 92 |
+
return out
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def run_plans(runs_dir: Path = RUNS_DIR) -> dict[str, list[dict]]:
|
| 96 |
+
"""Submitted plans in the committed rollout runs, by task: [{id, label, model, run, reward, plan}]."""
|
| 97 |
+
out: dict[str, list[dict]] = {}
|
| 98 |
+
for body in load_runs(runs_dir):
|
| 99 |
+
for e in body["episodes"]:
|
| 100 |
+
if e.get("plan") is None:
|
| 101 |
+
continue
|
| 102 |
+
out.setdefault(e["task_id"], []).append(
|
| 103 |
+
{"id": f"run:{body['run']}:{e['model']}", "label": model_name(e["model"]),
|
| 104 |
+
"model": e["model"], "run": body["run"], "reward": e.get("reward"), "plan": e["plan"]})
|
| 105 |
+
for entries in out.values(): # two runs of one model on a task: name the run too
|
| 106 |
+
for e in entries:
|
| 107 |
+
if sum(x["model"] == e["model"] for x in entries) > 1:
|
| 108 |
+
e["label"] = f"{e['label']} ({e['run']})"
|
| 109 |
+
return out
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def ci(xs, n=2000, seed=0):
|
| 113 |
+
"""95 % bootstrap interval of the mean, the same as eval/report.py's (so the numbers match the README)."""
|
| 114 |
+
xs = sorted(xs)
|
| 115 |
+
rng = random.Random(seed)
|
| 116 |
+
means = sorted(statistics.mean(rng.choices(xs, k=len(xs))) for _ in range(n))
|
| 117 |
+
return means[int(0.025 * n)], means[int(0.975 * n)]
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def runs_summary(runs: list[dict]) -> dict:
|
| 121 |
+
"""What the eval explorer shows: one row per (run, model) with the board's columns, and every episode's outcome."""
|
| 122 |
+
rows, episodes = [], []
|
| 123 |
+
for body in runs:
|
| 124 |
+
by: dict[str, list[dict]] = {}
|
| 125 |
+
for e in body["episodes"]:
|
| 126 |
+
by.setdefault(e["model"], []).append(e)
|
| 127 |
+
for model, es in by.items():
|
| 128 |
+
r = [e["reward"] for e in es]
|
| 129 |
+
lo, hi = ci(r)
|
| 130 |
+
real = [e for e in es if e["difficulty"] == "real" and e.get("real_cost") is not None]
|
| 131 |
+
rows.append({
|
| 132 |
+
"run": body["run"], "model": model, "name": model_name(model), "split": es[0]["split"],
|
| 133 |
+
"n": len(es), "mean": statistics.mean(r), "ci": [lo, hi],
|
| 134 |
+
"tiers": {t: statistics.mean(x["reward"] for x in es if x["difficulty"] == t)
|
| 135 |
+
for t in TIERS if any(x["difficulty"] == t for x in es)},
|
| 136 |
+
"feasible": sum(bool(e["feasible"]) for e in es),
|
| 137 |
+
"optimal": sum(1 for e in es if e["feasible"] and e["cost"] == e["optimal_cost"]),
|
| 138 |
+
"no_submit": sum(not e["submitted"] for e in es),
|
| 139 |
+
"beat_real": sum(1 for e in real if e["feasible"] and e["cost"] < e["real_cost"]), "real": len(real),
|
| 140 |
+
"median_turns": statistics.median(e["agent_turns"] for e in es)})
|
| 141 |
+
for e in es:
|
| 142 |
+
episodes.append({k: e.get(k) for k in ("model", "task_id", "reward", "feasible", "submitted", "cost",
|
| 143 |
+
"optimal_cost", "end_reason", "agent_turns", "checks",
|
| 144 |
+
"violations")}
|
| 145 |
+
| {"run": body["run"], "plan_id": f"run:{body['run']}:{model}"
|
| 146 |
+
if e.get("plan") is not None else None})
|
| 147 |
+
rows.sort(key=lambda x: (x["split"], -x["mean"]))
|
| 148 |
+
return {"models": rows, "episodes": episodes}
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def _summary(t, plans: dict | None = None) -> dict:
|
| 152 |
+
ref = t.reference
|
| 153 |
+
return {"task_id": t.task_id, "split": t.split, "day": t.day, "start": hhmm(t.start), "end": hhmm(t.end),
|
| 154 |
+
"difficulty": t.difficulty, "runs": len(t.runs), "closures": [c.kind for c in t.closures],
|
| 155 |
+
"causes": sorted({c.cause for c in t.closures if c.cause}),
|
| 156 |
+
"model_plans": len((plans or {}).get(t.task_id, [])), "optimal_cost": ref.get("optimal_cost"),
|
| 157 |
+
"rewards": {"greedy": ref.get("greedy_reward"), "greedy_fast": ref.get("greedy_fast_reward"),
|
| 158 |
+
"naive": ref.get("naive_reward"), "real": (ref.get("real") or {}).get("reward_if_legal")}}
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def timeline(task, plan) -> dict:
|
| 162 |
+
"""Every served run as a path of (km, minute) points, for a time-distance graph."""
|
| 163 |
+
net = network(task)
|
| 164 |
+
res = evaluate(task, plan)
|
| 165 |
+
paths = []
|
| 166 |
+
for x in res.runs:
|
| 167 |
+
if x.cancelled:
|
| 168 |
+
paths.append({"train": x.train, "set": x.unit, "cancelled": True, "points": []})
|
| 169 |
+
continue
|
| 170 |
+
r = task.run(x.train)
|
| 171 |
+
pts = []
|
| 172 |
+
for i in sorted(set(x.arr) | set(x.dep)):
|
| 173 |
+
km = net.by_code[r.route[i]].km
|
| 174 |
+
if i in x.arr:
|
| 175 |
+
pts.append([km, x.arr[i]])
|
| 176 |
+
if i in x.dep and x.dep[i] != x.arr.get(i):
|
| 177 |
+
pts.append([km, x.dep[i]])
|
| 178 |
+
paths.append({"train": x.train, "set": x.unit, "kind": r.kind, "points": pts, "delay": x.delay,
|
| 179 |
+
"unserved": x.unserved, "problems": x.problems})
|
| 180 |
+
return {"task_id": task.task_id, "start": task.start, "end": task.end,
|
| 181 |
+
"points": [{"code": p.code, "km": p.km, "kind": p.kind, "tracks": p.tracks} for p in net.points],
|
| 182 |
+
"sections": [{"a": s.a, "b": s.b, "single": s.single} for s in net.sections],
|
| 183 |
+
"closures": [{"kind": c.kind, "where": c.where, "start": c.start, "end": c.end, "cause": c.cause}
|
| 184 |
+
for c in task.closures],
|
| 185 |
+
"paths": paths, "feasible": res.feasible, "cost": res.cost, "violations": res.violations[:50],
|
| 186 |
+
"optimal_cost": task.reference.get("optimal_cost")}
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def mount_api(app, pack: TaskPack) -> None:
|
| 190 |
+
plans_by_task = run_plans()
|
| 191 |
+
runs = runs_summary(load_runs())
|
| 192 |
+
|
| 193 |
+
@app.get("/api/tasks", include_in_schema=False)
|
| 194 |
+
def tasks():
|
| 195 |
+
return [_summary(t, plans_by_task) for t in pack.tasks]
|
| 196 |
+
|
| 197 |
+
@app.get("/api/runs", include_in_schema=False)
|
| 198 |
+
def model_runs():
|
| 199 |
+
return runs
|
| 200 |
+
|
| 201 |
+
def _task(task_id):
|
| 202 |
+
try:
|
| 203 |
+
return pack.get(task_id)
|
| 204 |
+
except KeyError:
|
| 205 |
+
raise HTTPException(404, f"no task {task_id}") from None
|
| 206 |
+
|
| 207 |
+
@app.get("/api/tasks/{task_id}", include_in_schema=False)
|
| 208 |
+
def task(task_id: str):
|
| 209 |
+
return pack.public(_task(task_id))
|
| 210 |
+
|
| 211 |
+
@app.get("/api/tasks/{task_id}/reference", include_in_schema=False)
|
| 212 |
+
def reference(task_id: str):
|
| 213 |
+
return _task(task_id).reference
|
| 214 |
+
|
| 215 |
+
@app.get("/api/tasks/{task_id}/timeline", include_in_schema=False)
|
| 216 |
+
def task_timeline(task_id: str, plan: str = "optimal"):
|
| 217 |
+
t = _task(task_id)
|
| 218 |
+
if plan == "optimal":
|
| 219 |
+
p, _ = parse_plan(t, t.reference["optimal_plan"])
|
| 220 |
+
elif plan in POLICIES:
|
| 221 |
+
p = POLICIES[plan](t)
|
| 222 |
+
else:
|
| 223 |
+
raise HTTPException(400, "plan must be optimal, naive, greedy or greedy_fast")
|
| 224 |
+
return timeline(t, p)
|
| 225 |
+
|
| 226 |
+
def _plan(t, plan_id: str):
|
| 227 |
+
"""(plan, parse problems, label) for a plan id."""
|
| 228 |
+
if plan_id == "optimal":
|
| 229 |
+
p, probs = parse_plan(t, t.reference["optimal_plan"])
|
| 230 |
+
return p, probs, "CP-SAT optimum"
|
| 231 |
+
if plan_id in POLICIES:
|
| 232 |
+
return POLICIES[plan_id](t), [], BASELINES.get(plan_id, plan_id)
|
| 233 |
+
for e in plans_by_task.get(t.task_id, []):
|
| 234 |
+
if e["id"] == plan_id:
|
| 235 |
+
p, probs = parse_plan(t, e["plan"])
|
| 236 |
+
return p, probs, e["label"]
|
| 237 |
+
raise HTTPException(400, f"unknown plan {plan_id!r}: see /api/tasks/{t.task_id}/plans")
|
| 238 |
+
|
| 239 |
+
@lru_cache(maxsize=256)
|
| 240 |
+
def _replay(task_id: str, plan_id: str) -> dict:
|
| 241 |
+
t = _task(task_id)
|
| 242 |
+
p, probs, label = _plan(t, plan_id)
|
| 243 |
+
return replay(t, p, label, probs) | {"plan_id": plan_id}
|
| 244 |
+
|
| 245 |
+
@app.get("/api/tasks/{task_id}/plans", include_in_schema=False)
|
| 246 |
+
def task_plans(task_id: str):
|
| 247 |
+
t = _task(task_id)
|
| 248 |
+
ref = t.reference
|
| 249 |
+
out = [{"id": "optimal", "label": "CP-SAT optimum", "reward": 1.0, "cost": ref.get("optimal_cost")}]
|
| 250 |
+
out += [{"id": k, "label": v, "reward": ref.get(f"{k}_reward"),
|
| 251 |
+
"cost": ref.get(f"{k}_cost") if ref.get(f"{k}_feasible") else None} for k, v in BASELINES.items()]
|
| 252 |
+
out += [{k: e[k] for k in ("id", "label", "reward")} for e in plans_by_task.get(task_id, [])]
|
| 253 |
+
out += [{"id": f"episode:{ep.episode_id}", "label": f"live episode {ep.episode_id}", "reward": None}
|
| 254 |
+
for ep in STORE.recent() if ep.task.task_id == task_id]
|
| 255 |
+
return out
|
| 256 |
+
|
| 257 |
+
@app.get("/api/tasks/{task_id}/replay", include_in_schema=False)
|
| 258 |
+
def task_replay(task_id: str, plan: str = "optimal"):
|
| 259 |
+
if plan.startswith("episode:"):
|
| 260 |
+
return episode_replay(plan.split(":", 1)[1])
|
| 261 |
+
return _replay(task_id, plan)
|
| 262 |
+
|
| 263 |
+
@app.get("/api/episodes/{episode_id}/replay", include_in_schema=False)
|
| 264 |
+
def episode_replay(episode_id: str):
|
| 265 |
+
ep = STORE.get(episode_id)
|
| 266 |
+
if ep is None:
|
| 267 |
+
raise HTTPException(404, f"no episode {episode_id}")
|
| 268 |
+
last = next((s for s in reversed(ep.steps) if s.get("plan") is not None), None)
|
| 269 |
+
p, probs = parse_plan(ep.task, last["plan"]) if last else ({}, [])
|
| 270 |
+
return replay(ep.task, p, f"live episode {episode_id}", probs) | {"plan_id": f"episode:{episode_id}"}
|
| 271 |
+
|
| 272 |
+
@app.get("/api/episodes", include_in_schema=False)
|
| 273 |
+
def episodes():
|
| 274 |
+
return [{k: v for k, v in ep.summary().items() if k != "steps"} | {"steps": len(ep.steps)}
|
| 275 |
+
for ep in STORE.recent()]
|
| 276 |
+
|
| 277 |
+
@app.get("/api/episodes/{episode_id}/timeline", include_in_schema=False)
|
| 278 |
+
def episode_timeline(episode_id: str):
|
| 279 |
+
ep = STORE.get(episode_id)
|
| 280 |
+
if ep is None:
|
| 281 |
+
raise HTTPException(404, f"no episode {episode_id}")
|
| 282 |
+
last = next((s for s in reversed(ep.steps) if s.get("plan") is not None), None)
|
| 283 |
+
p, _ = parse_plan(ep.task, last["plan"]) if last else ({}, [])
|
| 284 |
+
return timeline(ep.task, p) | {"episode": {k: v for k, v in ep.summary().items() if k != "steps"}}
|
| 285 |
+
|
| 286 |
+
if (WEB / "twin").is_dir():
|
| 287 |
+
app.mount("/twin", StaticFiles(directory=WEB / "twin", html=True), name="twin")
|
| 288 |
+
if (WEB / "explorer").is_dir():
|
| 289 |
+
app.mount("/explorer", StaticFiles(directory=WEB / "explorer", html=True), name="explorer")
|
| 290 |
+
if WEB.is_dir():
|
| 291 |
+
app.mount("/viewer", StaticFiles(directory=WEB, html=True), name="viewer")
|
| 292 |
+
|
| 293 |
+
@app.get("/", include_in_schema=False)
|
| 294 |
+
def root():
|
| 295 |
+
return RedirectResponse("/explorer/" if (WEB / "explorer" / "index.html").is_file() else "/twin/")
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
def create_server(pack: TaskPack | None = None):
|
| 299 |
+
pack = pack or load_pack()
|
| 300 |
+
|
| 301 |
+
def factory():
|
| 302 |
+
return RailOpsEnvironment(pack)
|
| 303 |
+
|
| 304 |
+
os.environ.setdefault("ENABLE_WEB_INTERFACE", "true")
|
| 305 |
+
app = create_app(factory, CallToolAction, CallToolObservation, env_name=ENV_NAME,
|
| 306 |
+
max_concurrent_envs=int(os.environ.get("MAX_CONCURRENT_ENVS", "64")),
|
| 307 |
+
gradio_builder=build_ui, custom_tab_name="Explore", custom_tab_primary=True,
|
| 308 |
+
show_default_tab=True, title_override="RailSimEnv v1 Environment", state_cls=RailState)
|
| 309 |
+
mount_api(app, pack)
|
| 310 |
+
|
| 311 |
+
@app.get("/healthz", include_in_schema=False)
|
| 312 |
+
def healthz():
|
| 313 |
+
return {"status": "ok", "tasks": {s: pack.count(s) for s in pack.splits()}}
|
| 314 |
+
|
| 315 |
+
return app
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
_app = None
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
def __getattr__(name: str):
|
| 322 |
+
global _app
|
| 323 |
+
if name == "app":
|
| 324 |
+
if _app is None:
|
| 325 |
+
_app = create_server()
|
| 326 |
+
return _app
|
| 327 |
+
raise AttributeError(name)
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
def main(host: str = "0.0.0.0", port: int | None = None) -> None:
|
| 331 |
+
import uvicorn
|
| 332 |
+
|
| 333 |
+
port = port or int(os.environ.get("PORT", "8000"))
|
| 334 |
+
uvicorn.run(create_server(), host=host, port=port, ws_ping_interval=60, ws_ping_timeout=300)
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
if __name__ == "__main__":
|
| 338 |
+
main()
|
openenv/rail_openenv/session.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""A client session on a RailSimEnv server (OpenEnv's MCP client over its WebSocket), for scripts and agents.
|
| 2 |
+
|
| 3 |
+
env = EnvSession("http://127.0.0.1:8000") # or LocalSession(pack): the same tools, in this process
|
| 4 |
+
meta = env.reset("gnlw-2025-03-13-0703-real-0") # metadata["instructions"] is the system prompt
|
| 5 |
+
text, done, reward, meta = env.call("submit_plan", {"plan": [...]})
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import json
|
| 11 |
+
|
| 12 |
+
from openenv.core.env_server.mcp_types import CallToolAction
|
| 13 |
+
from openenv.core.mcp_client import MCPToolClient
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class EnvSession:
|
| 17 |
+
"""One WebSocket session (one episode at a time)."""
|
| 18 |
+
|
| 19 |
+
def __init__(self, base_url: str, timeout_s: float = 300.0):
|
| 20 |
+
self.client = MCPToolClient(base_url.rstrip("/"), message_timeout_s=timeout_s).sync()
|
| 21 |
+
self._tools = None
|
| 22 |
+
|
| 23 |
+
def reset(self, task_id: str | None = None, **kw) -> dict:
|
| 24 |
+
if task_id is not None:
|
| 25 |
+
kw["task_id"] = task_id
|
| 26 |
+
return dict(self.client.reset(**kw).observation.metadata or {})
|
| 27 |
+
|
| 28 |
+
@property
|
| 29 |
+
def tools(self):
|
| 30 |
+
if self._tools is None:
|
| 31 |
+
self._tools = self.client.list_tools()
|
| 32 |
+
return self._tools
|
| 33 |
+
|
| 34 |
+
def call(self, name: str, arguments: dict) -> tuple[str, bool, float | None, dict]:
|
| 35 |
+
"""(tool result text, done, reward, observation metadata)."""
|
| 36 |
+
step = self.client.step(CallToolAction(tool_name=name, arguments=arguments))
|
| 37 |
+
obs = step.observation
|
| 38 |
+
err = getattr(obs, "error", None)
|
| 39 |
+
res = getattr(obs, "result", None)
|
| 40 |
+
text = None
|
| 41 |
+
if isinstance(res, dict):
|
| 42 |
+
blocks = res.get("content") or []
|
| 43 |
+
text = "\n".join(b.get("text", "") for b in blocks if b.get("type") == "text") or None
|
| 44 |
+
if text is None and res.get("data") is not None:
|
| 45 |
+
text = res["data"] if isinstance(res["data"], str) else json.dumps(res["data"])
|
| 46 |
+
elif res is not None:
|
| 47 |
+
data = getattr(res, "data", None)
|
| 48 |
+
text = data if isinstance(data, str) else json.dumps(data) if data is not None else str(res)
|
| 49 |
+
if err:
|
| 50 |
+
msg = err.get("message") if isinstance(err, dict) else getattr(err, "message", str(err))
|
| 51 |
+
text = json.dumps({"error": str(msg)})
|
| 52 |
+
reward = step.reward if step.reward is not None else obs.reward
|
| 53 |
+
return text or "", bool(step.done or obs.done), reward, dict(obs.metadata or {})
|
| 54 |
+
|
| 55 |
+
def close(self):
|
| 56 |
+
try:
|
| 57 |
+
self.client.close()
|
| 58 |
+
except Exception:
|
| 59 |
+
pass
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _result_text(res) -> str | None:
|
| 63 |
+
if res is None:
|
| 64 |
+
return None
|
| 65 |
+
if isinstance(res, dict):
|
| 66 |
+
blocks = res.get("content") or []
|
| 67 |
+
text = "\n".join(b.get("text", "") for b in blocks if b.get("type") == "text")
|
| 68 |
+
if text:
|
| 69 |
+
return text
|
| 70 |
+
data = res.get("data")
|
| 71 |
+
return data if isinstance(data, str) else (json.dumps(data) if data is not None else None)
|
| 72 |
+
content = getattr(res, "content", None)
|
| 73 |
+
if content:
|
| 74 |
+
text = "\n".join(getattr(b, "text", "") for b in content)
|
| 75 |
+
if text:
|
| 76 |
+
return text
|
| 77 |
+
data = getattr(res, "data", None)
|
| 78 |
+
return data if isinstance(data, str) else (json.dumps(data) if data is not None else str(res))
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
class LocalSession:
|
| 82 |
+
"""The environment in this process (no server, no WebSocket): the same tools, results and rubric. For tests,
|
| 83 |
+
pilots and quick experiments."""
|
| 84 |
+
|
| 85 |
+
def __init__(self, pack=None):
|
| 86 |
+
from openenv.core.env_server.mcp_types import ListToolsAction
|
| 87 |
+
|
| 88 |
+
from .environment import RailOpsEnvironment
|
| 89 |
+
|
| 90 |
+
self.env = RailOpsEnvironment(pack)
|
| 91 |
+
self._list = ListToolsAction
|
| 92 |
+
self._tools = None
|
| 93 |
+
|
| 94 |
+
def reset(self, task_id: str | None = None, **kw) -> dict:
|
| 95 |
+
if task_id is not None:
|
| 96 |
+
kw["task_id"] = task_id
|
| 97 |
+
return dict(self.env.reset(**kw).metadata or {})
|
| 98 |
+
|
| 99 |
+
@property
|
| 100 |
+
def tools(self):
|
| 101 |
+
if self._tools is None:
|
| 102 |
+
self._tools = self.env.step(self._list()).tools
|
| 103 |
+
return self._tools
|
| 104 |
+
|
| 105 |
+
def call(self, name: str, arguments: dict) -> tuple[str, bool, float | None, dict]:
|
| 106 |
+
obs = self.env.step(CallToolAction(tool_name=name, arguments=arguments))
|
| 107 |
+
err = getattr(obs, "error", None)
|
| 108 |
+
text = _result_text(getattr(obs, "result", None))
|
| 109 |
+
if err:
|
| 110 |
+
msg = err.get("message") if isinstance(err, dict) else getattr(err, "message", str(err))
|
| 111 |
+
text = json.dumps({"error": str(msg)})
|
| 112 |
+
return text or "", bool(obs.done), obs.reward, dict(obs.metadata or {})
|
| 113 |
+
|
| 114 |
+
def close(self):
|
| 115 |
+
pass
|
openenv/rail_openenv/web/explorer/index.html
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
+
<title>RailSimEnv eval explorer</title>
|
| 7 |
+
<link rel="icon" href="data:,">
|
| 8 |
+
<style>
|
| 9 |
+
:root {
|
| 10 |
+
color-scheme: light;
|
| 11 |
+
--page: #f9f9f7; --surface: #fcfcfb; --ink: #0b0b0b; --ink-2: #52514e; --muted: #898781;
|
| 12 |
+
--grid: #e1e0d9; --axis: #c3c2b7; --ring: rgba(11, 11, 11, 0.10); --wash: #f0efec;
|
| 13 |
+
--accent: #2a78d6; --critical: #d03b3b; --good: #0ca30c; --bar: #2a78d6; --ref: #898781;
|
| 14 |
+
}
|
| 15 |
+
@media (prefers-color-scheme: dark) {
|
| 16 |
+
:root:where(:not([data-theme="light"])) {
|
| 17 |
+
color-scheme: dark;
|
| 18 |
+
--page: #0d0d0d; --surface: #1a1a19; --ink: #ffffff; --ink-2: #c3c2b7; --muted: #898781;
|
| 19 |
+
--grid: #2c2c2a; --axis: #383835; --ring: rgba(255, 255, 255, 0.10); --wash: #232321;
|
| 20 |
+
--accent: #3987e5; --bar: #3987e5;
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
:root[data-theme="dark"] {
|
| 24 |
+
color-scheme: dark;
|
| 25 |
+
--page: #0d0d0d; --surface: #1a1a19; --ink: #ffffff; --ink-2: #c3c2b7; --muted: #898781;
|
| 26 |
+
--grid: #2c2c2a; --axis: #383835; --ring: rgba(255, 255, 255, 0.10); --wash: #232321;
|
| 27 |
+
--accent: #3987e5; --bar: #3987e5;
|
| 28 |
+
}
|
| 29 |
+
* { box-sizing: border-box; }
|
| 30 |
+
[hidden] { display: none !important; }
|
| 31 |
+
body { margin: 0; background: var(--page); color: var(--ink); font: 14px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif; }
|
| 32 |
+
a { color: var(--accent); }
|
| 33 |
+
nav.site { display: flex; gap: 4px 18px; flex-wrap: wrap; align-items: baseline; padding: 12px 16px 0; }
|
| 34 |
+
nav.site b { font-size: 15px; margin-right: 6px; }
|
| 35 |
+
nav.site a { color: var(--ink-2); text-decoration: none; padding: 2px 0; border-bottom: 2px solid transparent; }
|
| 36 |
+
nav.site a:hover { color: var(--ink); }
|
| 37 |
+
nav.site a[aria-current] { color: var(--ink); border-bottom-color: var(--accent); }
|
| 38 |
+
main { padding: 16px 16px 32px; max-width: 1320px; margin: 0 auto; }
|
| 39 |
+
h1 { font-size: 24px; line-height: 1.2; margin: 0 0 6px; font-weight: 650; }
|
| 40 |
+
h2 { font-size: 17px; margin: 0 0 4px; font-weight: 650; }
|
| 41 |
+
.lede { color: var(--ink-2); max-width: 88ch; margin: 0 0 16px; }
|
| 42 |
+
.card { background: var(--surface); border: 1px solid var(--ring); border-radius: 10px; padding: 16px; margin-bottom: 16px; }
|
| 43 |
+
.note { color: var(--ink-2); font-size: 13px; margin: 0 0 10px; }
|
| 44 |
+
.seg { display: inline-flex; border: 1px solid var(--axis); border-radius: 8px; overflow: hidden; margin-bottom: 16px; }
|
| 45 |
+
.seg button { font: inherit; border: 0; background: var(--surface); color: var(--ink-2); padding: 6px 14px; cursor: pointer; }
|
| 46 |
+
.seg button + button { border-left: 1px solid var(--axis); }
|
| 47 |
+
.seg button[aria-pressed="true"] { background: var(--accent); color: #fff; }
|
| 48 |
+
.tablewrap { overflow-x: auto; }
|
| 49 |
+
table { border-collapse: collapse; width: 100%; font-size: 13px; }
|
| 50 |
+
th, td { padding: 7px 8px; border-bottom: 1px solid var(--grid); text-align: left; vertical-align: middle; white-space: nowrap; }
|
| 51 |
+
th { color: var(--ink-2); font-weight: 600; font-size: 12px; }
|
| 52 |
+
td.n, th.n { text-align: right; font-variant-numeric: tabular-nums; }
|
| 53 |
+
tr.ref td { color: var(--ink-2); }
|
| 54 |
+
tr.ref:first-of-type td { border-top: 1px solid var(--axis); }
|
| 55 |
+
.meanbar { display: inline-flex; align-items: center; gap: 10px; }
|
| 56 |
+
.meanbar b { font-variant-numeric: tabular-nums; min-width: 40px; display: inline-block; }
|
| 57 |
+
.meanbar small { color: var(--ink-2); font-variant-numeric: tabular-nums; }
|
| 58 |
+
.bar { width: 160px; height: 14px; position: relative; flex: none; }
|
| 59 |
+
.bar .track { position: absolute; inset: 6px 0; background: var(--wash); border-radius: 2px; }
|
| 60 |
+
.bar .fill { position: absolute; left: 0; top: 3px; height: 8px; background: var(--bar); border-radius: 0 4px 4px 0; }
|
| 61 |
+
.bar .ci { position: absolute; top: 6px; height: 2px; background: var(--ink); opacity: 0.65; }
|
| 62 |
+
.bar .ci::before, .bar .ci::after { content: ""; position: absolute; top: -4px; width: 1.5px; height: 10px; background: var(--ink); }
|
| 63 |
+
.bar .ci::before { left: 0; } .bar .ci::after { right: 0; }
|
| 64 |
+
tr.ref .bar .fill { background: var(--ref); }
|
| 65 |
+
.filters { display: flex; flex-wrap: wrap; gap: 8px 16px; align-items: center; margin: 4px 0 12px; }
|
| 66 |
+
.chips { display: inline-flex; flex-wrap: wrap; gap: 6px; }
|
| 67 |
+
.chip { font: inherit; font-size: 13px; border: 1px solid var(--axis); background: var(--surface); color: var(--ink-2); border-radius: 999px; padding: 3px 11px; cursor: pointer; }
|
| 68 |
+
.chip[aria-pressed="true"] { border-color: var(--accent); color: var(--ink); box-shadow: inset 0 0 0 1px var(--accent); }
|
| 69 |
+
.ramp { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--ink-2); }
|
| 70 |
+
.ramp i { display: inline-block; width: 22px; height: 12px; }
|
| 71 |
+
.ramp i:first-of-type { border-radius: 3px 0 0 3px; } .ramp i:last-of-type { border-radius: 0 3px 3px 0; }
|
| 72 |
+
.key { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; color: var(--ink-2); }
|
| 73 |
+
#grid th.sort { cursor: pointer; user-select: none; }
|
| 74 |
+
#grid th.sort:hover { color: var(--ink); }
|
| 75 |
+
#grid th.sorted::after { content: " ↓"; }
|
| 76 |
+
#grid th.sorted.asc::after { content: " ↑"; }
|
| 77 |
+
#grid td.cell { padding: 3px; }
|
| 78 |
+
#grid td.cell button { width: 100%; min-width: 64px; font: inherit; font-size: 13px; font-variant-numeric: tabular-nums; border: 0; border-radius: 5px;
|
| 79 |
+
padding: 5px 6px; cursor: pointer; text-align: right; outline-offset: 1px; }
|
| 80 |
+
#grid td.cell button:hover, #grid td.cell button:focus-visible { outline: 2px solid var(--ink); }
|
| 81 |
+
#grid td.cell button.sel { outline: 2px solid var(--accent); }
|
| 82 |
+
#grid td.cell button.illegal { background: var(--wash); color: var(--ink); }
|
| 83 |
+
#grid td.cell button.illegal::before { content: "✕ "; color: var(--critical); font-weight: 700; }
|
| 84 |
+
#grid td.cell button.none { background: transparent; color: var(--muted); border: 1px dashed var(--axis); }
|
| 85 |
+
.tier { display: inline-block; font-size: 11px; padding: 1px 7px; border-radius: 999px; border: 1px solid var(--ring); color: var(--ink-2); }
|
| 86 |
+
.task small { color: var(--ink-2); }
|
| 87 |
+
#detail { position: sticky; bottom: 12px; z-index: 3; margin-top: 12px; box-shadow: 0 6px 24px rgba(0, 0, 0, 0.14); }
|
| 88 |
+
#detail .row { display: flex; flex-wrap: wrap; gap: 8px 22px; align-items: baseline; }
|
| 89 |
+
#detail .k { font-size: 12px; color: var(--ink-2); display: block; }
|
| 90 |
+
#detail .v { font-size: 17px; font-weight: 600; font-variant-numeric: tabular-nums; }
|
| 91 |
+
#detail .links { margin-left: auto; display: flex; gap: 8px; }
|
| 92 |
+
#detail .links a { padding: 6px 12px; border: 1px solid var(--axis); border-radius: 6px; text-decoration: none; color: var(--ink); background: var(--surface); }
|
| 93 |
+
#detail .links a.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
|
| 94 |
+
#detail button.close { font: inherit; background: none; border: 0; color: var(--ink-2); cursor: pointer; font-size: 18px; }
|
| 95 |
+
footer { color: var(--muted); font-size: 12px; margin-top: 18px; }
|
| 96 |
+
.err { color: var(--critical); }
|
| 97 |
+
</style>
|
| 98 |
+
</head>
|
| 99 |
+
<body>
|
| 100 |
+
<nav class="site" id="nav">
|
| 101 |
+
<b>RailSimEnv</b>
|
| 102 |
+
<a href="/explorer/" aria-current="page">Eval explorer</a>
|
| 103 |
+
<a href="/twin/">3D replay</a>
|
| 104 |
+
<a href="/viewer/">Train graph</a>
|
| 105 |
+
<a href="/web">Environment</a>
|
| 106 |
+
</nav>
|
| 107 |
+
<main>
|
| 108 |
+
<h1>Eval explorer</h1>
|
| 109 |
+
<p class="lede">Each task is a real 2025 disruption on the single-track Groningen – Leeuwarden line: hold, short-turn and
|
| 110 |
+
cancel trains so opposing trains cross safely. Every plan is graded against a proven CP-SAT optimum (reward 1.0); a plan
|
| 111 |
+
that breaks a rule scores under 0.2. Pick any cell to watch that plan run in 3D or read it on the train graph.</p>
|
| 112 |
+
<div class="seg" role="group" aria-label="Task split" id="splits"></div>
|
| 113 |
+
|
| 114 |
+
<section class="card">
|
| 115 |
+
<h2>Leaderboard</h2>
|
| 116 |
+
<p class="note" id="boardnote"></p>
|
| 117 |
+
<div class="tablewrap"><table id="board"></table></div>
|
| 118 |
+
</section>
|
| 119 |
+
|
| 120 |
+
<section class="card">
|
| 121 |
+
<h2>Every task, every model</h2>
|
| 122 |
+
<div class="filters">
|
| 123 |
+
<div class="chips" id="tiers" role="group" aria-label="Tier"></div>
|
| 124 |
+
<span class="ramp" id="ramp"></span>
|
| 125 |
+
<span class="key"><b style="color:var(--critical)">✕</b> breaks a rule</span>
|
| 126 |
+
<span class="key"><i style="display:inline-block;width:22px;height:12px;border:1px dashed var(--axis);border-radius:3px"></i> no plan submitted</span>
|
| 127 |
+
</div>
|
| 128 |
+
<div class="tablewrap"><table id="grid"></table></div>
|
| 129 |
+
<div class="card" id="detail" hidden></div>
|
| 130 |
+
</section>
|
| 131 |
+
<footer>Data: Rijden de Treinen open data (CC BY 4.0, "Rijden de Treinen"); track and map © OpenStreetMap contributors (ODbL 1.0).
|
| 132 |
+
Rewards are one rollout per model per task.</footer>
|
| 133 |
+
</main>
|
| 134 |
+
<script>
|
| 135 |
+
"use strict";
|
| 136 |
+
const $ = (s) => document.querySelector(s);
|
| 137 |
+
const params = new URLSearchParams(location.search);
|
| 138 |
+
const TIERS = ["real", "blockage", "loop", "compound"];
|
| 139 |
+
const TIER = { real: "real disruption", blockage: "blockage", loop: "lost passing track", compound: "compound" };
|
| 140 |
+
// sequential blue ramp (light to dark) for rewards from 0.2 (legal) to 1.0 (the optimum)
|
| 141 |
+
const RAMP = [["#cde2fb", 0.2], ["#9ec5f4", 0.3], ["#6da7ec", 0.4], ["#3987e5", 0.55], ["#256abf", 0.7], ["#184f95", 0.85], ["#0d366b", 0.999]];
|
| 142 |
+
const S = { runs: null, tasks: [], byTask: {}, split: params.get("split") || "eval", tier: "all", sort: { key: "mean", asc: true }, sel: null };
|
| 143 |
+
|
| 144 |
+
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
| 145 |
+
const f3 = (x) => (x === null || x === undefined ? "–" : Number(x).toFixed(3));
|
| 146 |
+
const mean = (xs) => xs.reduce((a, b) => a + b, 0) / Math.max(1, xs.length);
|
| 147 |
+
const shortName = (n) => n.replace(/ \(.*\)$/, "");
|
| 148 |
+
const day = (d) => new Date(`${d}T12:00:00Z`).toLocaleDateString("en-GB", { weekday: "short", day: "numeric", month: "short", timeZone: "UTC" });
|
| 149 |
+
async function json(url) {
|
| 150 |
+
const r = await fetch(url);
|
| 151 |
+
if (!r.ok) throw new Error(`${url}: HTTP ${r.status}`);
|
| 152 |
+
return r.json();
|
| 153 |
+
}
|
| 154 |
+
function rampColor(r) {
|
| 155 |
+
let c = RAMP[0][0];
|
| 156 |
+
for (const [hex, lo] of RAMP) if (r >= lo) c = hex;
|
| 157 |
+
return c;
|
| 158 |
+
}
|
| 159 |
+
function textOn(hex) {
|
| 160 |
+
const n = parseInt(hex.slice(1), 16), rgb = [n >> 16, (n >> 8) & 255, n & 255].map((v) => { v /= 255; return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; });
|
| 161 |
+
return 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2] > 0.3 ? "#0b0b0b" : "#ffffff";
|
| 162 |
+
}
|
| 163 |
+
const bar = (m, ci) => `<span class="bar"><span class="track"></span><span class="fill" style="width:${(m * 100).toFixed(1)}%"></span>`
|
| 164 |
+
+ (ci ? `<span class="ci" style="left:${(ci[0] * 100).toFixed(1)}%;width:${((ci[1] - ci[0]) * 100).toFixed(1)}%"></span>` : "") + `</span>`;
|
| 165 |
+
|
| 166 |
+
function models() {
|
| 167 |
+
return S.runs.models.filter((m) => m.split === S.split);
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
function renderSplits() {
|
| 171 |
+
const splits = [...new Set(S.runs.models.map((m) => m.split))].sort((a, b) => (a === "eval" ? -1 : b === "eval" ? 1 : 0));
|
| 172 |
+
if (!splits.includes(S.split)) S.split = splits[0];
|
| 173 |
+
$("#splits").innerHTML = splits.map((sp) => {
|
| 174 |
+
const n = new Set(S.runs.episodes.filter((e) => S.runs.models.some((m) => m.run === e.run && m.split === sp)).map((e) => e.task_id)).size;
|
| 175 |
+
return `<button type="button" data-split="${esc(sp)}" aria-pressed="${sp === S.split}">${sp === "eval" ? "Eval" : "Train (calibration)"} · ${n} tasks</button>`;
|
| 176 |
+
}).join("");
|
| 177 |
+
for (const b of $("#splits").querySelectorAll("button")) b.onclick = () => { S.split = b.dataset.split; S.sel = null; render(); };
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
function taskIds() {
|
| 181 |
+
const runs = new Set(models().map((m) => m.run));
|
| 182 |
+
return [...new Set(S.runs.episodes.filter((e) => runs.has(e.run)).map((e) => e.task_id))];
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
function renderBoard() {
|
| 186 |
+
const ms = models();
|
| 187 |
+
const tiers = TIERS.filter((t) => ms.some((m) => t in m.tiers));
|
| 188 |
+
const ids = taskIds(), tasks = ids.map((id) => S.byTask[id]).filter(Boolean);
|
| 189 |
+
$("#boardnote").textContent = `${tasks.length} tasks, one rollout per model, the same rules for every model: 12 turns, 64k output tokens per turn, `
|
| 190 |
+
+ `10 checks of a draft, one graded submit. 95% bootstrap intervals.`;
|
| 191 |
+
const head = `<thead><tr><th>#</th><th>Model</th><th>Mean reward (95% CI)</th>${tiers.map((t) => `<th class="n">${esc(t)}</th>`).join("")}`
|
| 192 |
+
+ `<th class="n">Legal plans</th><th class="n">Optimal</th><th class="n">No plan</th><th class="n">Median turns</th></tr></thead>`;
|
| 193 |
+
const rows = ms.map((m, i) => `<tr><td class="n">${i + 1}</td><td><b>${esc(m.name)}</b></td>`
|
| 194 |
+
+ `<td><span class="meanbar"><b>${f3(m.mean)}</b>${bar(m.mean, m.ci)}<small>${f3(m.ci[0])}–${f3(m.ci[1])}</small></span></td>`
|
| 195 |
+
+ tiers.map((t) => `<td class="n">${f3(m.tiers[t])}</td>`).join("")
|
| 196 |
+
+ `<td class="n">${m.feasible} / ${m.n}</td><td class="n">${m.optimal}</td><td class="n">${m.no_submit}</td><td class="n">${m.median_turns}</td></tr>`);
|
| 197 |
+
const refRow = (name, f, legal) => {
|
| 198 |
+
const xs = tasks.map(f).filter((x) => x !== null && x !== undefined);
|
| 199 |
+
if (!xs.length) return "";
|
| 200 |
+
const per = tiers.map((t) => { const ys = tasks.filter((x) => x.difficulty === t).map(f).filter((x) => x !== null && x !== undefined); return `<td class="n">${ys.length ? f3(mean(ys)) : "–"}</td>`; });
|
| 201 |
+
return `<tr class="ref"><td></td><td>${esc(name)}</td><td><span class="meanbar"><b>${f3(mean(xs))}</b>${bar(mean(xs))}<small>${xs.length < tasks.length ? `${xs.length} tasks` : ""}</small></span></td>`
|
| 202 |
+
+ per.join("") + `<td class="n">${legal}</td><td class="n"></td><td class="n"></td><td class="n"></td></tr>`;
|
| 203 |
+
};
|
| 204 |
+
const refs = [refRow("CP-SAT optimum (the reference)", () => 1.0, `${tasks.length} / ${tasks.length}`),
|
| 205 |
+
refRow("the real dispatchers (the record, real tier only, scored as if legal)", (t) => (t.difficulty === "real" ? t.rewards.real : null), ""),
|
| 206 |
+
refRow("greedy dispatcher", (t) => t.rewards.greedy, ""),
|
| 207 |
+
refRow("change nothing", (t) => t.rewards.naive, "")];
|
| 208 |
+
$("#board").innerHTML = head + `<tbody>${rows.join("")}${refs.join("")}</tbody>`;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
function renderGrid() {
|
| 212 |
+
const ms = models();
|
| 213 |
+
const ids = taskIds();
|
| 214 |
+
const ep = {};
|
| 215 |
+
for (const e of S.runs.episodes) ep[`${e.run}|${e.model}|${e.task_id}`] = e;
|
| 216 |
+
const tiers = TIERS.filter((t) => ids.some((id) => S.byTask[id] && S.byTask[id].difficulty === t));
|
| 217 |
+
$("#tiers").innerHTML = ["all", ...tiers].map((t) => {
|
| 218 |
+
const n = t === "all" ? ids.length : ids.filter((id) => S.byTask[id] && S.byTask[id].difficulty === t).length;
|
| 219 |
+
return `<button type="button" class="chip" data-tier="${t}" aria-pressed="${S.tier === t}">${t === "all" ? "All" : esc(TIER[t])} · ${n}</button>`;
|
| 220 |
+
}).join("");
|
| 221 |
+
for (const b of $("#tiers").querySelectorAll("button")) b.onclick = () => { S.tier = b.dataset.tier; renderGrid(); };
|
| 222 |
+
$("#ramp").innerHTML = `0.2 ${RAMP.map(([c]) => `<i style="background:${c}"></i>`).join("")} 1.0 (the optimum)`;
|
| 223 |
+
|
| 224 |
+
let rows = ids.map((id) => {
|
| 225 |
+
const t = S.byTask[id];
|
| 226 |
+
const cells = ms.map((m) => ep[`${m.run}|${m.model}|${id}`] || null);
|
| 227 |
+
const rs = cells.filter(Boolean).map((e) => e.reward);
|
| 228 |
+
return { id, t, cells, mean: rs.length ? mean(rs) : null };
|
| 229 |
+
}).filter((r) => r.t && (S.tier === "all" || r.t.difficulty === S.tier));
|
| 230 |
+
const k = S.sort.key, dir = S.sort.asc ? 1 : -1;
|
| 231 |
+
const val = (r) => (k === "mean" ? r.mean : k === "task" ? `${r.t.day} ${r.t.start}` : k === "greedy" ? r.t.rewards.greedy
|
| 232 |
+
: k === "runs" ? r.t.runs : r.cells[+k] ? r.cells[+k].reward : -1);
|
| 233 |
+
rows.sort((a, b) => { const x = val(a), y = val(b); return (x < y ? -1 : x > y ? 1 : 0) * dir || a.id.localeCompare(b.id); });
|
| 234 |
+
|
| 235 |
+
const th = (key, label, cls = "") => `<th class="sort ${cls}${S.sort.key === key ? ` sorted${S.sort.asc ? " asc" : ""}` : ""}" data-key="${key}" scope="col">${label}</th>`;
|
| 236 |
+
const head = `<thead><tr>${th("task", "Task")}${th("runs", "Trains", "n")}${ms.map((m, i) => th(String(i), esc(shortName(m.name)), "n")).join("")}`
|
| 237 |
+
+ `${th("mean", "Mean of models", "n")}${th("greedy", "Greedy", "n")}<th>Watch</th></tr></thead>`;
|
| 238 |
+
const body = rows.map((r) => {
|
| 239 |
+
const cause = (r.t.causes || []).join(", ");
|
| 240 |
+
const cells = r.cells.map((e, i) => {
|
| 241 |
+
if (!e) return `<td class="cell"><button type="button" class="none" disabled>–</button></td>`;
|
| 242 |
+
const sel = S.sel && S.sel.task === r.id && S.sel.i === i ? " sel" : "";
|
| 243 |
+
const title = `${shortName(ms[i].name)}: reward ${f3(e.reward)}, ${e.submitted ? (e.feasible ? "legal plan" : `${e.violations} rule breaks`) : "no plan submitted"}`;
|
| 244 |
+
if (!e.submitted || !e.plan_id) return `<td class="cell"><button type="button" class="none${sel}" data-task="${esc(r.id)}" data-i="${i}" title="${esc(title)}">no plan</button></td>`;
|
| 245 |
+
if (!e.feasible) return `<td class="cell"><button type="button" class="illegal${sel}" data-task="${esc(r.id)}" data-i="${i}" title="${esc(title)}">${f3(e.reward)}</button></td>`;
|
| 246 |
+
const c = rampColor(e.reward);
|
| 247 |
+
return `<td class="cell"><button type="button" class="${sel.trim()}" style="background:${c};color:${textOn(c)}" data-task="${esc(r.id)}" data-i="${i}" title="${esc(title)}">${e.reward >= 0.999 ? "★ " : ""}${f3(e.reward)}</button></td>`;
|
| 248 |
+
}).join("");
|
| 249 |
+
return `<tr><td class="task"><b>${esc(day(r.t.day))}</b> ${esc(r.t.start)}–${esc(r.t.end)} <span class="tier">${esc(TIER[r.t.difficulty] || r.t.difficulty)}</span><br><small>${esc(cause)}</small></td>`
|
| 250 |
+
+ `<td class="n">${r.t.runs}</td>${cells}<td class="n">${f3(r.mean)}</td><td class="n">${f3(r.t.rewards.greedy)}</td>`
|
| 251 |
+
+ `<td><a href="/twin/?task=${encodeURIComponent(r.id)}&plan=optimal">3D</a> · <a href="/viewer/?task=${encodeURIComponent(r.id)}&plan=optimal">graph</a></td></tr>`;
|
| 252 |
+
}).join("");
|
| 253 |
+
$("#grid").innerHTML = head + `<tbody>${body}</tbody>`;
|
| 254 |
+
for (const h of $("#grid").querySelectorAll("th.sort")) h.onclick = () => {
|
| 255 |
+
const key = h.dataset.key;
|
| 256 |
+
S.sort = { key, asc: S.sort.key === key ? !S.sort.asc : key === "task" || key === "runs" ? true : key === "mean" };
|
| 257 |
+
renderGrid();
|
| 258 |
+
};
|
| 259 |
+
for (const b of $("#grid").querySelectorAll("td.cell button[data-task]")) b.onclick = () => { S.sel = { task: b.dataset.task, i: +b.dataset.i }; renderGrid(); renderDetail(); };
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
function renderDetail() {
|
| 263 |
+
const box = $("#detail");
|
| 264 |
+
if (!S.sel) { box.hidden = true; return; }
|
| 265 |
+
const m = models()[S.sel.i], t = S.byTask[S.sel.task];
|
| 266 |
+
const e = S.runs.episodes.find((x) => x.run === m.run && x.model === m.model && x.task_id === S.sel.task);
|
| 267 |
+
if (!e || !t) { box.hidden = true; return; }
|
| 268 |
+
const over = e.feasible && e.cost !== null && e.optimal_cost ? Math.round((e.cost / e.optimal_cost - 1) * 100) : null;
|
| 269 |
+
const outcome = !e.submitted ? "no plan submitted" : e.feasible ? (over === 0 ? "the proven optimum" : `legal, ${over}% over the optimum`) : `${e.violations} rule break${e.violations === 1 ? "" : "s"}`;
|
| 270 |
+
const plan = e.plan_id ? encodeURIComponent(e.plan_id) : "optimal";
|
| 271 |
+
const task = encodeURIComponent(t.task_id);
|
| 272 |
+
box.innerHTML = `<div class="row">
|
| 273 |
+
<div><span class="k">${esc(day(t.day))} ${esc(t.start)}–${esc(t.end)} · ${esc(TIER[t.difficulty] || t.difficulty)}</span><span class="v">${esc(m.name)}</span></div>
|
| 274 |
+
<div><span class="k">Reward</span><span class="v">${f3(e.reward)}</span></div>
|
| 275 |
+
<div><span class="k">Outcome</span><span class="v">${esc(outcome)}</span></div>
|
| 276 |
+
<div><span class="k">Cost / optimum</span><span class="v">${e.cost ?? "–"} / ${e.optimal_cost}</span></div>
|
| 277 |
+
<div><span class="k">Turns · checks</span><span class="v">${e.agent_turns} · ${e.checks}</span></div>
|
| 278 |
+
<div class="links">${e.plan_id ? `<a class="primary" href="/twin/?task=${task}&plan=${plan}">Watch in 3D</a><a href="/viewer/?task=${task}&plan=${plan}">Train graph</a>` : ""}
|
| 279 |
+
<a href="/viewer/?task=${task}&plan=optimal">The optimum</a><button class="close" type="button" aria-label="Close">×</button></div>
|
| 280 |
+
</div>`;
|
| 281 |
+
box.hidden = false;
|
| 282 |
+
box.querySelector(".close").onclick = () => { S.sel = null; renderGrid(); renderDetail(); };
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
function render() {
|
| 286 |
+
renderSplits();
|
| 287 |
+
renderBoard();
|
| 288 |
+
renderGrid();
|
| 289 |
+
renderDetail();
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
(async () => {
|
| 293 |
+
if (params.has("embed")) $("#nav").hidden = true;
|
| 294 |
+
if (params.get("theme")) document.documentElement.dataset.theme = params.get("theme");
|
| 295 |
+
try {
|
| 296 |
+
const [runs, tasks] = await Promise.all([json("/api/runs"), json("/api/tasks")]);
|
| 297 |
+
S.runs = runs;
|
| 298 |
+
S.tasks = tasks;
|
| 299 |
+
S.byTask = Object.fromEntries(tasks.map((t) => [t.task_id, t]));
|
| 300 |
+
} catch (err) {
|
| 301 |
+
$("main").insertAdjacentHTML("afterbegin", `<p class="err">${esc(err.message)}</p>`);
|
| 302 |
+
return;
|
| 303 |
+
}
|
| 304 |
+
if (!S.runs.models.length) {
|
| 305 |
+
$("main").insertAdjacentHTML("afterbegin", `<p class="note">No model runs on this server (results/rollouts/*/index.json, or RAIL_RUNS_DIR).</p>`);
|
| 306 |
+
return;
|
| 307 |
+
}
|
| 308 |
+
render();
|
| 309 |
+
document.body.dataset.ready = "1";
|
| 310 |
+
})();
|
| 311 |
+
</script>
|
| 312 |
+
</body>
|
| 313 |
+
</html>
|
openenv/rail_openenv/web/index.html
ADDED
|
@@ -0,0 +1,592 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
+
<title>RailSimEnv train graph</title>
|
| 7 |
+
<link rel="icon" href="data:,">
|
| 8 |
+
<style>
|
| 9 |
+
:root {
|
| 10 |
+
color-scheme: light;
|
| 11 |
+
--page: #f9f9f7; --surface: #fcfcfb; --ink: #0b0b0b; --ink-2: #52514e; --muted: #898781;
|
| 12 |
+
--grid: #e1e0d9; --axis: #c3c2b7; --ring: rgba(11, 11, 11, 0.10);
|
| 13 |
+
--stop: #2a78d6; --fast: #eb6834; --critical: #d03b3b; --warning: #fab219; --serious: #ec835a; --good: #0ca30c;
|
| 14 |
+
--good-text: #006300; --single: #f1efe9; --after: rgba(11, 11, 11, 0.035); --accent: #2a78d6;
|
| 15 |
+
}
|
| 16 |
+
@media (prefers-color-scheme: dark) {
|
| 17 |
+
:root:where(:not([data-theme="light"])) {
|
| 18 |
+
color-scheme: dark;
|
| 19 |
+
--page: #0d0d0d; --surface: #1a1a19; --ink: #ffffff; --ink-2: #c3c2b7; --muted: #898781;
|
| 20 |
+
--grid: #2c2c2a; --axis: #383835; --ring: rgba(255, 255, 255, 0.10);
|
| 21 |
+
--stop: #3987e5; --fast: #d95926; --good-text: #0ca30c; --single: #232321; --after: rgba(255, 255, 255, 0.03);
|
| 22 |
+
--accent: #3987e5;
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
:root[data-theme="dark"] {
|
| 26 |
+
color-scheme: dark;
|
| 27 |
+
--page: #0d0d0d; --surface: #1a1a19; --ink: #ffffff; --ink-2: #c3c2b7; --muted: #898781;
|
| 28 |
+
--grid: #2c2c2a; --axis: #383835; --ring: rgba(255, 255, 255, 0.10);
|
| 29 |
+
--stop: #3987e5; --fast: #d95926; --good-text: #0ca30c; --single: #232321; --after: rgba(255, 255, 255, 0.03);
|
| 30 |
+
--accent: #3987e5;
|
| 31 |
+
}
|
| 32 |
+
* { box-sizing: border-box; }
|
| 33 |
+
[hidden] { display: none !important; }
|
| 34 |
+
body { margin: 0; background: var(--page); color: var(--ink); font: 14px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif; }
|
| 35 |
+
a { color: var(--accent); }
|
| 36 |
+
nav.site { display: flex; gap: 4px 18px; flex-wrap: wrap; align-items: baseline; padding: 12px 16px 0; }
|
| 37 |
+
nav.site b { font-size: 15px; margin-right: 6px; }
|
| 38 |
+
nav.site a { color: var(--ink-2); text-decoration: none; padding: 2px 0; border-bottom: 2px solid transparent; }
|
| 39 |
+
nav.site a:hover { color: var(--ink); }
|
| 40 |
+
nav.site a[aria-current] { color: var(--ink); border-bottom-color: var(--accent); }
|
| 41 |
+
main { padding: 12px 16px 24px; max-width: 1440px; margin: 0 auto; }
|
| 42 |
+
.controls { display: flex; gap: 8px 12px; flex-wrap: wrap; align-items: end; margin-bottom: 14px; }
|
| 43 |
+
.controls label { display: grid; gap: 2px; font-size: 12px; color: var(--ink-2); min-width: 0; }
|
| 44 |
+
.controls select { font: inherit; font-size: 14px; color: var(--ink); background: var(--surface); border: 1px solid var(--axis);
|
| 45 |
+
border-radius: 6px; padding: 6px 8px; max-width: min(92vw, 460px); }
|
| 46 |
+
.controls .open3d { margin-left: auto; padding: 7px 12px; border-radius: 6px; border: 1px solid var(--axis); text-decoration: none;
|
| 47 |
+
color: var(--ink); background: var(--surface); white-space: nowrap; }
|
| 48 |
+
.card { background: var(--surface); border: 1px solid var(--ring); border-radius: 10px; padding: 16px; }
|
| 49 |
+
.head { display: flex; gap: 12px 24px; flex-wrap: wrap; justify-content: space-between; align-items: start; }
|
| 50 |
+
.head h1 { font-size: 20px; line-height: 1.25; margin: 0 0 4px; font-weight: 650; }
|
| 51 |
+
.head .sub { color: var(--ink-2); margin: 0; max-width: 78ch; }
|
| 52 |
+
.stats { display: flex; flex-wrap: wrap; gap: 8px 28px; margin: 14px 0 6px; }
|
| 53 |
+
.stat { display: grid; gap: 0; }
|
| 54 |
+
.stat .k { font-size: 12px; color: var(--ink-2); }
|
| 55 |
+
.stat .v { font-size: 20px; font-weight: 600; }
|
| 56 |
+
.stat .v small { font-size: 13px; font-weight: 400; color: var(--ink-2); margin-left: 4px; }
|
| 57 |
+
.badge { display: inline-flex; gap: 6px; align-items: center; font-size: 13px; font-weight: 600; padding: 3px 10px;
|
| 58 |
+
border-radius: 999px; border: 1px solid var(--ring); white-space: nowrap; }
|
| 59 |
+
.badge.ok i { color: var(--good); font-style: normal; }
|
| 60 |
+
.badge.bad i { color: var(--critical); font-style: normal; }
|
| 61 |
+
.legend { display: flex; flex-wrap: wrap; gap: 6px 18px; margin: 12px 0 4px; font-size: 13px; color: var(--ink-2); }
|
| 62 |
+
.legend span { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
|
| 63 |
+
.legend svg { flex: none; }
|
| 64 |
+
.chartwrap { position: relative; overflow-x: auto; margin-top: 4px; }
|
| 65 |
+
#chart { display: block; }
|
| 66 |
+
#chart text { font-family: system-ui, -apple-system, "Segoe UI", sans-serif; }
|
| 67 |
+
.halo { paint-order: stroke; stroke: var(--surface); stroke-width: 4px; stroke-linejoin: round; }
|
| 68 |
+
.num { font-variant-numeric: tabular-nums; }
|
| 69 |
+
.paths.focus .run:not(.on) { opacity: 0.18; }
|
| 70 |
+
.paths.focus .run.on path.line { stroke-width: 3; }
|
| 71 |
+
.tip { position: absolute; pointer-events: none; background: var(--surface); color: var(--ink); border: 1px solid var(--ring);
|
| 72 |
+
border-radius: 8px; padding: 8px 10px; font-size: 13px; line-height: 1.4; box-shadow: 0 4px 16px rgba(0,0,0,0.12);
|
| 73 |
+
max-width: 300px; z-index: 2; }
|
| 74 |
+
.tip b { font-size: 14px; }
|
| 75 |
+
.tip .key { display: inline-block; width: 14px; height: 0; border-top: 2px solid; vertical-align: middle; margin-right: 6px; }
|
| 76 |
+
.tip .m { color: var(--ink-2); }
|
| 77 |
+
details { margin-top: 14px; }
|
| 78 |
+
summary { cursor: pointer; color: var(--ink-2); }
|
| 79 |
+
table { border-collapse: collapse; width: 100%; font-size: 13px; margin-top: 8px; }
|
| 80 |
+
th, td { text-align: left; padding: 5px 8px; border-bottom: 1px solid var(--grid); vertical-align: top; }
|
| 81 |
+
th { color: var(--ink-2); font-weight: 600; }
|
| 82 |
+
td.n, th.n { text-align: right; font-variant-numeric: tabular-nums; }
|
| 83 |
+
.tablewrap { overflow-x: auto; }
|
| 84 |
+
.breaks { margin: 10px 0 0; padding-left: 18px; color: var(--ink-2); font-size: 13px; }
|
| 85 |
+
.err { color: var(--critical); }
|
| 86 |
+
body.figure main { padding: 0; max-width: none; }
|
| 87 |
+
body.figure .card { border: 0; border-radius: 0; padding: 22px 26px 18px; }
|
| 88 |
+
body.figure .head h1 { font-size: 22px; }
|
| 89 |
+
body.figure .sub { font-size: 15px; }
|
| 90 |
+
body.figure .legend { font-size: 14px; }
|
| 91 |
+
</style>
|
| 92 |
+
</head>
|
| 93 |
+
<body>
|
| 94 |
+
<nav class="site" id="nav">
|
| 95 |
+
<b>RailSimEnv</b>
|
| 96 |
+
<a href="/explorer/">Eval explorer</a>
|
| 97 |
+
<a href="/twin/">3D replay</a>
|
| 98 |
+
<a href="/viewer/" aria-current="page">Train graph</a>
|
| 99 |
+
<a href="/web">Environment</a>
|
| 100 |
+
</nav>
|
| 101 |
+
<main>
|
| 102 |
+
<div class="controls" id="controls">
|
| 103 |
+
<label>Task<select id="task" aria-label="Task"></select></label>
|
| 104 |
+
<label>Plan<select id="plan" aria-label="Plan"></select></label>
|
| 105 |
+
<a class="open3d" id="open3d" href="/twin/">Watch it in 3D ↗</a>
|
| 106 |
+
</div>
|
| 107 |
+
<section class="card" aria-live="polite">
|
| 108 |
+
<div class="head">
|
| 109 |
+
<div>
|
| 110 |
+
<h1 id="title">Loading…</h1>
|
| 111 |
+
<p class="sub" id="sub"></p>
|
| 112 |
+
</div>
|
| 113 |
+
<div id="badge"></div>
|
| 114 |
+
</div>
|
| 115 |
+
<div class="stats" id="stats"></div>
|
| 116 |
+
<ol class="breaks" id="breaks" hidden></ol>
|
| 117 |
+
<div class="legend" id="legend"></div>
|
| 118 |
+
<div class="chartwrap" id="chartwrap">
|
| 119 |
+
<svg id="chart" role="img" aria-labelledby="title"></svg>
|
| 120 |
+
<div class="tip" id="tip" hidden></div>
|
| 121 |
+
</div>
|
| 122 |
+
<details id="tablebox">
|
| 123 |
+
<summary>Every train as a table</summary>
|
| 124 |
+
<div class="tablewrap"><table id="table"></table></div>
|
| 125 |
+
</details>
|
| 126 |
+
</section>
|
| 127 |
+
</main>
|
| 128 |
+
<script>
|
| 129 |
+
"use strict";
|
| 130 |
+
const $ = (s) => document.querySelector(s);
|
| 131 |
+
const params = new URLSearchParams(location.search);
|
| 132 |
+
const FIGURE = params.has("figure");
|
| 133 |
+
const EMBED = FIGURE || params.has("embed");
|
| 134 |
+
const FEATURED = "gnlw-2025-05-01-0800-180m-blockage-0";
|
| 135 |
+
const TIER = { real: "real disruption", blockage: "blockage", loop: "lost passing track", compound: "compound" };
|
| 136 |
+
const WHAT = { closed: "closed", slow: "slow running", track: "a platform track out of use" };
|
| 137 |
+
const S = { tasks: [], R: null, paths: [], geo: null };
|
| 138 |
+
|
| 139 |
+
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
| 140 |
+
const hhmm = (m) => { m = Math.round(m); const d = ((m % 1440) + 1440) % 1440; return `${String(Math.floor(d / 60)).padStart(2, "0")}:${String(d % 60).padStart(2, "0")}`; };
|
| 141 |
+
const fmt = (n) => (n === null || n === undefined ? "–" : Number(n).toLocaleString("en-GB"));
|
| 142 |
+
async function json(url) {
|
| 143 |
+
const r = await fetch(url);
|
| 144 |
+
if (!r.ok) throw new Error(`${url}: HTTP ${r.status}`);
|
| 145 |
+
return r.json();
|
| 146 |
+
}
|
| 147 |
+
function longDay(day) {
|
| 148 |
+
return new Date(`${day}T12:00:00Z`).toLocaleDateString("en-GB", { weekday: "long", day: "numeric", month: "long", year: "numeric", timeZone: "UTC" });
|
| 149 |
+
}
|
| 150 |
+
function shortDay(day) {
|
| 151 |
+
return new Date(`${day}T12:00:00Z`).toLocaleDateString("en-GB", { day: "numeric", month: "short", timeZone: "UTC" });
|
| 152 |
+
}
|
| 153 |
+
const shortName = (p) => (p.kind === "junction" ? p.name.replace(/ (east|west) junction$/, " jn.") : p.name);
|
| 154 |
+
function orderText(o) {
|
| 155 |
+
if (!o) return "";
|
| 156 |
+
const out = [];
|
| 157 |
+
if (o.cancel) out.push("cancelled");
|
| 158 |
+
if (o.from) out.push(`starts at ${o.from}`);
|
| 159 |
+
if (o.to) out.push(`turns back at ${o.to}`);
|
| 160 |
+
for (const [c, t] of Object.entries(o.hold || {})) out.push(`held at ${c} until ${t}`);
|
| 161 |
+
return out.join("; ");
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
// ---- geometry ------------------------------------------------------------------------------------------------
|
| 165 |
+
|
| 166 |
+
const measure = (() => {
|
| 167 |
+
const g = document.createElement("canvas").getContext("2d");
|
| 168 |
+
return (text, px, weight = 400) => { g.font = `${weight} ${px}px system-ui, -apple-system, "Segoe UI", sans-serif`; return g.measureText(text).width; };
|
| 169 |
+
})();
|
| 170 |
+
|
| 171 |
+
function pathOf(r, km) {
|
| 172 |
+
const pts = [];
|
| 173 |
+
if (r.started && r.fixed_from !== null && r.fixed_from !== undefined) pts.push([r.fixed_from, km[r.route[r.fixed_to - 1]]]);
|
| 174 |
+
const from = r.started ? r.fixed_to : r.first;
|
| 175 |
+
for (let i = from; i <= r.last; i++) {
|
| 176 |
+
const k = km[r.route[i]];
|
| 177 |
+
if (r.arr[i] !== null && r.arr[i] !== undefined && !(i === from && !r.started)) pts.push([r.arr[i], k]);
|
| 178 |
+
if (r.dep[i] !== null && r.dep[i] !== undefined && i < r.last) pts.push([r.dep[i], k]);
|
| 179 |
+
}
|
| 180 |
+
return pts;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
function timetablePath(r, km) {
|
| 184 |
+
const pts = [];
|
| 185 |
+
for (let i = 0; i < r.route.length; i++) {
|
| 186 |
+
if (r.sched_arr[i] !== null && i > 0) pts.push([r.sched_arr[i], km[r.route[i]]]);
|
| 187 |
+
if (r.sched_dep[i] !== null && i < r.route.length - 1) pts.push([r.sched_dep[i], km[r.route[i]]]);
|
| 188 |
+
}
|
| 189 |
+
return pts;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
function holdsOf(r, km) {
|
| 193 |
+
const out = [];
|
| 194 |
+
for (const code of Object.keys((r.order && r.order.hold) || {})) {
|
| 195 |
+
for (let i = r.first; i <= r.last; i++) {
|
| 196 |
+
if (r.route[i] !== code) continue;
|
| 197 |
+
const a = r.arr[i] ?? r.sched_dep[i], d = r.dep[i];
|
| 198 |
+
if (a !== null && d !== null && d > a) out.push([a, d, km[code]]);
|
| 199 |
+
}
|
| 200 |
+
}
|
| 201 |
+
return out;
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
function segHitsBox(ax, ay, bx, by, b) { // Liang-Barsky: does segment a-b cross box b?
|
| 205 |
+
let t0 = 0, t1 = 1;
|
| 206 |
+
const dx = bx - ax, dy = by - ay;
|
| 207 |
+
for (const [p, q] of [[-dx, ax - b.x0], [dx, b.x1 - ax], [-dy, ay - b.y0], [dy, b.y1 - ay]]) {
|
| 208 |
+
if (p === 0) { if (q < 0) return false; continue; }
|
| 209 |
+
const r = q / p;
|
| 210 |
+
if (p < 0) { if (r > t1) return false; if (r > t0) t0 = r; } else { if (r < t0) return false; if (r < t1) t1 = r; }
|
| 211 |
+
}
|
| 212 |
+
return true;
|
| 213 |
+
}
|
| 214 |
+
const overlap = (a, b) => a.x0 < b.x1 && b.x0 < a.x1 && a.y0 < b.y1 && b.y0 < a.y1;
|
| 215 |
+
|
| 216 |
+
// ---- the chart -----------------------------------------------------------------------------------------------
|
| 217 |
+
|
| 218 |
+
function draw() {
|
| 219 |
+
const R = S.R;
|
| 220 |
+
if (!R) return;
|
| 221 |
+
const wrap = $("#chartwrap");
|
| 222 |
+
const W = FIGURE ? Math.floor(wrap.clientWidth) : Math.max(760, Math.floor(wrap.clientWidth));
|
| 223 |
+
const fs = FIGURE ? 14 : 13, fsSmall = fs - 2, fsTrain = FIGURE ? 12.5 : 12;
|
| 224 |
+
const km = Object.fromEntries(R.points.map((p) => [p.code, p.km]));
|
| 225 |
+
const kmMax = Math.max(...R.points.map((p) => p.km));
|
| 226 |
+
const compact = W < 900 && !FIGURE;
|
| 227 |
+
const label = (p) => (compact ? p.code : shortName(p));
|
| 228 |
+
const nameW = Math.max(...R.points.map((p) => measure(label(p), p.kind === "junction" ? fsSmall : fs, p.kind === "terminal" ? 600 : 400)
|
| 229 |
+
+ (compact || p.kind === "junction" ? 0 : measure(` ${p.code}`, fsSmall))));
|
| 230 |
+
const L = Math.ceil(nameW) + 44, Rm = 18, T = 38, B = 34;
|
| 231 |
+
const H = FIGURE ? 600 : Math.max(520, Math.min(680, Math.round(W * 0.5)));
|
| 232 |
+
const paths = R.runs.filter((r) => !r.cancelled).map((r) => ({ r, pts: pathOf(r, km) })).filter((p) => p.pts.length > 1);
|
| 233 |
+
const allT = paths.flatMap((p) => p.pts.map((q) => q[0]));
|
| 234 |
+
const t0 = Math.min(R.start - 10, ...allT.filter((t) => t >= R.start - 30)) - 2;
|
| 235 |
+
const t1 = Math.min(R.end + 75, Math.max(R.end + 15, ...allT)) + 3;
|
| 236 |
+
const x = (t) => L + (t - t0) / (t1 - t0) * (W - L - Rm);
|
| 237 |
+
const y = (k) => T + k / kmMax * (H - T - B);
|
| 238 |
+
const s = [];
|
| 239 |
+
|
| 240 |
+
s.push(`<defs>
|
| 241 |
+
<clipPath id="plot"><rect x="${L}" y="${T - 6}" width="${W - L - Rm}" height="${H - T - B + 12}"/></clipPath>
|
| 242 |
+
<pattern id="hatch-closed" width="7" height="7" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
|
| 243 |
+
<rect width="7" height="7" fill="var(--critical)" fill-opacity="0.10"/><line x1="0" y1="0" x2="0" y2="7" stroke="var(--critical)" stroke-opacity="0.55" stroke-width="2"/></pattern>
|
| 244 |
+
<pattern id="hatch-slow" width="7" height="7" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
|
| 245 |
+
<rect width="7" height="7" fill="var(--warning)" fill-opacity="0.14"/><line x1="0" y1="0" x2="0" y2="7" stroke="var(--warning)" stroke-opacity="0.75" stroke-width="2"/></pattern>
|
| 246 |
+
<pattern id="hatch-track" width="7" height="7" patternUnits="userSpaceOnUse" patternTransform="rotate(135)">
|
| 247 |
+
<rect width="7" height="7" fill="var(--serious)" fill-opacity="0.12"/><line x1="0" y1="0" x2="0" y2="7" stroke="var(--serious)" stroke-opacity="0.7" stroke-width="2"/></pattern>
|
| 248 |
+
</defs>`);
|
| 249 |
+
|
| 250 |
+
// single-track rows, the time grid, the window
|
| 251 |
+
R.sections.forEach((sec) => {
|
| 252 |
+
if (!sec.single) return;
|
| 253 |
+
s.push(`<rect x="${L}" y="${y(km[sec.a])}" width="${W - L - Rm}" height="${y(km[sec.b]) - y(km[sec.a])}" fill="var(--single)"/>`);
|
| 254 |
+
});
|
| 255 |
+
if (R.end < t1) s.push(`<rect x="${x(R.end)}" y="${T}" width="${x(t1) - x(R.end)}" height="${H - T - B}" fill="var(--after)"/>`);
|
| 256 |
+
for (const p of R.points) s.push(`<line x1="${L}" x2="${W - Rm}" y1="${y(p.km)}" y2="${y(p.km)}" stroke="var(--grid)"/>`);
|
| 257 |
+
const step = (W - L - Rm) / (t1 - t0) * 30 < 46 ? 60 : 30;
|
| 258 |
+
for (let t = Math.ceil(t0 / step) * step; t <= t1; t += step) {
|
| 259 |
+
const hour = t % 60 === 0;
|
| 260 |
+
s.push(`<line x1="${x(t)}" x2="${x(t)}" y1="${T}" y2="${H - B}" stroke="${hour ? "var(--axis)" : "var(--grid)"}"/>`);
|
| 261 |
+
if (x(t) + measure(hhmm(t), fs, 600) / 2 > W) continue;
|
| 262 |
+
for (const yy of [T - 12, H - B + 21]) {
|
| 263 |
+
s.push(`<text x="${x(t)}" y="${yy}" text-anchor="middle" font-size="${hour ? fs : fsSmall}" font-weight="${hour ? 600 : 400}" fill="${hour ? "var(--ink-2)" : "var(--muted)"}" class="num">${hhmm(t)}</text>`);
|
| 264 |
+
}
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
// stations and the track: one line for single track, two for double
|
| 268 |
+
const tx = L - 18;
|
| 269 |
+
R.sections.forEach((sec) => {
|
| 270 |
+
const ya = y(km[sec.a]), yb = y(km[sec.b]);
|
| 271 |
+
if (sec.single) s.push(`<line x1="${tx}" x2="${tx}" y1="${ya}" y2="${yb}" stroke="var(--ink-2)" stroke-width="2.5"/>`);
|
| 272 |
+
else for (const d of [-2.6, 2.6]) s.push(`<line x1="${tx + d}" x2="${tx + d}" y1="${ya}" y2="${yb}" stroke="var(--ink-2)" stroke-width="1.3"/>`);
|
| 273 |
+
});
|
| 274 |
+
for (const p of R.points) {
|
| 275 |
+
const yy = y(p.km);
|
| 276 |
+
if (p.kind === "terminal") s.push(`<rect x="${tx - 6}" y="${yy - 2.5}" width="12" height="5" rx="1.5" fill="var(--ink)"/>`);
|
| 277 |
+
else if (p.kind === "station") s.push(`<circle cx="${tx}" cy="${yy}" r="4.6" fill="var(--surface)" stroke="var(--ink)" stroke-width="1.6"/>`);
|
| 278 |
+
else if (p.kind === "halt") s.push(`<circle cx="${tx}" cy="${yy}" r="3" fill="var(--ink)"/>`);
|
| 279 |
+
const junction = p.kind === "junction";
|
| 280 |
+
const code = compact || junction ? "" : ` <tspan font-size="${fsSmall}" fill="var(--muted)">${esc(p.code)}</tspan>`;
|
| 281 |
+
s.push(`<text x="${tx - 14}" y="${yy + fs * 0.36}" text-anchor="end" font-size="${junction ? fsSmall : fs}" font-weight="${p.kind === "terminal" ? 600 : 400}" fill="${junction ? "var(--muted)" : "var(--ink)"}"><title>${esc(p.name)} (${esc(p.code)}${p.tracks ? `, ${p.tracks} platform track${p.tracks > 1 ? "s" : ""}` : ""})</title>${esc(label(p))}${code}</text>`);
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
// disruptions
|
| 285 |
+
const boxes = [];
|
| 286 |
+
const closures = [];
|
| 287 |
+
for (const c of R.closures) {
|
| 288 |
+
const a = km[c.where[0]], b = km[c.where[c.where.length - 1]];
|
| 289 |
+
const k0 = c.kind === "track" ? a - 0.55 : Math.min(a, b), k1 = c.kind === "track" ? a + 0.55 : Math.max(a, b);
|
| 290 |
+
const bx = x(Math.max(c.start, t0)), bw = x(Math.min(c.end, t1)) - bx, by = y(k0), bh = Math.max(8, y(k1) - y(k0));
|
| 291 |
+
s.push(`<rect x="${bx}" y="${by}" width="${bw}" height="${bh}" fill="url(#hatch-${c.kind})" stroke="${c.kind === "closed" ? "var(--critical)" : c.kind === "slow" ? "var(--warning)" : "var(--serious)"}" stroke-width="1.2" clip-path="url(#plot)"><title>${esc(c.cause)}: ${WHAT[c.kind] || c.kind} ${hhmm(c.start)}–${hhmm(c.end)}</title></rect>`);
|
| 292 |
+
closures.push({ c, bx, bw, by, bh });
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
// trains
|
| 296 |
+
const geo = [];
|
| 297 |
+
const series = (r) => (r.problems && r.problems.length ? "var(--critical)" : r.kind === "fast" ? "var(--fast)" : "var(--stop)");
|
| 298 |
+
s.push(`<g class="paths" id="paths" clip-path="url(#plot)">`);
|
| 299 |
+
for (const r of R.runs.filter((r) => r.cancelled)) {
|
| 300 |
+
const pts = timetablePath(r, km);
|
| 301 |
+
if (pts.length < 2) continue;
|
| 302 |
+
const d = pts.map((q, i) => `${i ? "L" : "M"}${x(q[0]).toFixed(1)},${y(q[1]).toFixed(1)}`).join("");
|
| 303 |
+
s.push(`<g class="run" data-unit="${esc(r.unit)}"><path d="${d}" fill="none" stroke="var(--muted)" stroke-width="1.3" stroke-dasharray="2 4" stroke-linecap="round"/></g>`);
|
| 304 |
+
geo.push({ r, xy: pts.map((q) => [x(q[0]), y(q[1])]), cancelled: true });
|
| 305 |
+
}
|
| 306 |
+
for (const { r, pts } of paths) {
|
| 307 |
+
const xy = pts.map((q) => [x(q[0]), y(q[1])]);
|
| 308 |
+
const d = xy.map((q, i) => `${i ? "L" : "M"}${q[0].toFixed(1)},${q[1].toFixed(1)}`).join("");
|
| 309 |
+
const col = series(r), bad = r.problems && r.problems.length;
|
| 310 |
+
const parts = [`<path class="line" d="${d}" fill="none" stroke="${col}" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"${bad ? ' stroke-dasharray="7 4"' : ""}/>`];
|
| 311 |
+
for (const [a, b, k] of holdsOf(r, km)) parts.push(`<line x1="${x(a)}" x2="${x(b)}" y1="${y(k)}" y2="${y(k)}" stroke="${col}" stroke-width="6" stroke-linecap="round"/>`);
|
| 312 |
+
const o = r.order || {};
|
| 313 |
+
if (o.to) { const q = xy[xy.length - 1]; parts.push(`<circle cx="${q[0]}" cy="${q[1]}" r="5" fill="var(--surface)" stroke="${col}" stroke-width="2.4"/>`); }
|
| 314 |
+
if (o.from) { const q = xy[0]; parts.push(`<circle cx="${q[0]}" cy="${q[1]}" r="5" fill="var(--surface)" stroke="${col}" stroke-width="2.4"/>`); }
|
| 315 |
+
s.push(`<g class="run" data-unit="${esc(r.unit)}">${parts.join("")}</g>`);
|
| 316 |
+
geo.push({ r, xy });
|
| 317 |
+
}
|
| 318 |
+
s.push(`</g>`);
|
| 319 |
+
|
| 320 |
+
// labels: closures first, then each train once, where it collides least
|
| 321 |
+
const plot = { x0: L + 2, x1: W - Rm - 2, y0: T + 1, y1: H - B - 1 };
|
| 322 |
+
const labels = [];
|
| 323 |
+
for (const { c, bx, bw, by, bh } of closures) {
|
| 324 |
+
// a tag with the closure's kind, time and cause: inside the hatched area when it fits (wrapped), else beside it
|
| 325 |
+
const head = c.kind === "closed" ? "Closed" : c.kind === "slow" ? `Slow, +${c.extra} min` : "Track out of use";
|
| 326 |
+
const lh = fsSmall + 4;
|
| 327 |
+
const wrap = (words, maxW) => words.reduce((ls, wd) => {
|
| 328 |
+
const last = ls[ls.length - 1];
|
| 329 |
+
if (last && measure(`${last} ${wd}`, fsSmall) <= maxW) ls[ls.length - 1] = `${last} ${wd}`; else ls.push(wd);
|
| 330 |
+
return ls;
|
| 331 |
+
}, []);
|
| 332 |
+
let lines = [[head, 600], [`${hhmm(c.start)}–${hhmm(c.end)}`, 400], ...wrap((c.cause || "").split(/\s+/).filter(Boolean), bw - 22).map((t) => [t, 400])];
|
| 333 |
+
let w = Math.max(...lines.map(([t, wt]) => measure(t, fsSmall, wt))) + 14, h = lines.length * lh + 8;
|
| 334 |
+
let lx = bx + 6, ly = by + 6;
|
| 335 |
+
const stroke = c.kind === "closed" ? "var(--critical)" : c.kind === "slow" ? "var(--warning)" : "var(--serious)";
|
| 336 |
+
const tag = (ls, cx, cy, rot) => { // a tag of lines centred on (cx, cy), upright when rot
|
| 337 |
+
const tw = Math.max(...ls.map(([t, wt]) => measure(t, fsSmall, wt))) + 14, th = ls.length * lh + 8;
|
| 338 |
+
const [bw2, bh2] = rot ? [th, tw] : [tw, th];
|
| 339 |
+
boxes.push({ x0: cx - bw2 / 2, x1: cx + bw2 / 2, y0: cy - bh2 / 2, y1: cy + bh2 / 2 });
|
| 340 |
+
labels.push(`<g transform="translate(${cx} ${cy})${rot ? " rotate(-90)" : ""}"><rect x="${-tw / 2}" y="${-th / 2}" width="${tw}" height="${th}" rx="5" fill="var(--surface)" stroke="${stroke}" stroke-width="1"/>`
|
| 341 |
+
+ ls.map(([t, wt], i) => `<text x="${-tw / 2 + 7}" y="${-th / 2 + 4 + i * lh + lh / 2 + fsSmall * 0.36}" font-size="${fsSmall}" font-weight="${wt}" fill="${i ? "var(--ink-2)" : "var(--ink)"}">${esc(t)}</text>`).join("") + "</g>");
|
| 342 |
+
};
|
| 343 |
+
if (w <= bw - 10 && h <= bh - 10) { tag(lines, bx + 6 + w / 2, by + 6 + h / 2, false); continue; }
|
| 344 |
+
const two = [[`${head} ${hhmm(c.start)}–${hhmm(c.end)}`, 600], ...(c.cause ? [[c.cause, 400]] : [])];
|
| 345 |
+
const tw = Math.max(...two.map(([t, wt]) => measure(t, fsSmall, wt))) + 14, th = two.length * lh + 8;
|
| 346 |
+
if (tw <= bw - 10 && th <= bh - 2) { tag(two, bx + 6 + tw / 2, by + bh / 2, false); continue; }
|
| 347 |
+
if (bw >= th + 8 && bh >= tw + 12) { tag(two, bx + bw / 2, by + bh / 2, true); continue; }
|
| 348 |
+
lines = [[`${head} · ${hhmm(c.start)}–${hhmm(c.end)}${c.cause ? ` · ${c.cause}` : ""}`, 600]];
|
| 349 |
+
w = measure(lines[0][0], fsSmall, 600) + 14; h = lh + 8;
|
| 350 |
+
lx = bx + bw + 6; ly = by + bh / 2 - h / 2;
|
| 351 |
+
if (lx + w > plot.x1) lx = Math.max(plot.x0, bx - w - 6);
|
| 352 |
+
boxes.push({ x0: lx, x1: lx + w, y0: ly, y1: ly + h });
|
| 353 |
+
labels.push(`<rect x="${lx}" y="${ly}" width="${w}" height="${h}" rx="5" fill="var(--surface)" stroke="${stroke}" stroke-width="1"/>`
|
| 354 |
+
+ lines.map(([t, wt], i) => `<text x="${lx + 7}" y="${ly + 4 + i * lh + lh / 2 + fsSmall * 0.36}" font-size="${fsSmall}" font-weight="${wt}" fill="${i ? "var(--ink-2)" : "var(--ink)"}">${esc(t)}</text>`).join(""));
|
| 355 |
+
}
|
| 356 |
+
for (const yy of [x(R.start), x(R.end)]) boxes.push({ x0: yy - 1, x1: yy + 1, y0: T - 30, y1: T });
|
| 357 |
+
const segs = geo.filter((g) => !g.cancelled).flatMap((g, gi) => g.xy.slice(1).map((q, i) => [g.xy[i][0], g.xy[i][1], q[0], q[1], gi]));
|
| 358 |
+
const trainBox = [];
|
| 359 |
+
geo.forEach((g, gi) => {
|
| 360 |
+
if (g.cancelled) return;
|
| 361 |
+
const text = g.r.train, w = measure(text, fsTrain, 500) + 4, h = fsTrain + 2;
|
| 362 |
+
const cands = [];
|
| 363 |
+
let acc = 0;
|
| 364 |
+
const total = g.xy.slice(1).reduce((a, q, i) => a + Math.hypot(q[0] - g.xy[i][0], q[1] - g.xy[i][1]), 0);
|
| 365 |
+
for (let i = 1; i < g.xy.length; i++) {
|
| 366 |
+
const [ax, ay] = g.xy[i - 1], [bx, by] = g.xy[i];
|
| 367 |
+
const len = Math.hypot(bx - ax, by - ay);
|
| 368 |
+
if (len < 1) continue;
|
| 369 |
+
const ux = (bx - ax) / len, uy = (by - ay) / len, nx = uy, ny = -ux; // the normal on the upper side
|
| 370 |
+
for (let d = 0; d <= len; d += 12) {
|
| 371 |
+
const px = ax + ux * d, py = ay + uy * d;
|
| 372 |
+
const off = Math.abs(nx) * w / 2 + Math.abs(ny) * h / 2 + 3;
|
| 373 |
+
const cx = px + nx * off, cy = py + ny * off;
|
| 374 |
+
cands.push({ box: { x0: cx - w / 2, x1: cx + w / 2, y0: cy - h / 2, y1: cy + h / 2 }, s: (acc + d) / Math.max(1, total) });
|
| 375 |
+
}
|
| 376 |
+
acc += len;
|
| 377 |
+
}
|
| 378 |
+
let best = null;
|
| 379 |
+
for (const c of cands) {
|
| 380 |
+
const b = c.box;
|
| 381 |
+
if (b.x0 < plot.x0 || b.x1 > plot.x1 || b.y0 < plot.y0 || b.y1 > plot.y1) continue;
|
| 382 |
+
if (boxes.some((o) => overlap(o, b)) || trainBox.some((o) => overlap(o, b))) continue;
|
| 383 |
+
let pen = c.s * 4;
|
| 384 |
+
for (const [ax, ay, bx, by, gj] of segs) {
|
| 385 |
+
if (Math.max(ax, bx) < b.x0 || Math.min(ax, bx) > b.x1 || Math.max(ay, by) < b.y0 || Math.min(ay, by) > b.y1) continue;
|
| 386 |
+
if (segHitsBox(ax, ay, bx, by, b)) pen += gj === gi ? 6 : 3;
|
| 387 |
+
}
|
| 388 |
+
if (!best || pen < best.pen) best = { ...c, pen };
|
| 389 |
+
}
|
| 390 |
+
if (!best) return;
|
| 391 |
+
trainBox.push({ x0: best.box.x0 - 2, x1: best.box.x1 + 2, y0: best.box.y0 - 1, y1: best.box.y1 + 1 });
|
| 392 |
+
labels.push(`<text x="${(best.box.x0 + best.box.x1) / 2}" y="${(best.box.y0 + best.box.y1) / 2 + fsTrain * 0.36}" text-anchor="middle" font-size="${fsTrain}" font-weight="500" fill="var(--ink-2)" class="halo num">${esc(text)}</text>`);
|
| 393 |
+
});
|
| 394 |
+
|
| 395 |
+
// the window's edges
|
| 396 |
+
s.push(`<line x1="${x(R.start)}" x2="${x(R.start)}" y1="${T - 4}" y2="${H - B}" stroke="var(--ink)" stroke-width="1.4"/>`);
|
| 397 |
+
if (R.end < t1) s.push(`<line x1="${x(R.end)}" x2="${x(R.end)}" y1="${T - 4}" y2="${H - B}" stroke="var(--ink)" stroke-width="1.4"/>`);
|
| 398 |
+
s.push(`<text x="${x(R.start) + 6}" y="${T - 27}" font-size="${fsSmall}" fill="var(--ink-2)">window opens</text>`);
|
| 399 |
+
if (R.end < t1) {
|
| 400 |
+
const msg = "window closes · lateness after it is handed on";
|
| 401 |
+
const right = x(R.end) + 6 + measure(msg, fsSmall) < W - Rm;
|
| 402 |
+
s.push(`<text x="${right ? x(R.end) + 6 : x(R.end) - 6}" y="${T - 27}" text-anchor="${right ? "start" : "end"}" font-size="${fsSmall}" fill="var(--ink-2)">${right ? msg : "window closes"}</text>`);
|
| 403 |
+
}
|
| 404 |
+
s.push(labels.join(""));
|
| 405 |
+
|
| 406 |
+
const svg = $("#chart");
|
| 407 |
+
svg.setAttribute("viewBox", `0 0 ${W} ${H}`);
|
| 408 |
+
svg.setAttribute("width", W);
|
| 409 |
+
svg.setAttribute("height", H);
|
| 410 |
+
svg.innerHTML = s.join("");
|
| 411 |
+
S.geo = geo;
|
| 412 |
+
S.frame = { x, y, t0, t1, L, T, W, H, B, Rm, km };
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
// ---- hover: the nearest train, and its train set ---------------------------------------------------------------
|
| 416 |
+
|
| 417 |
+
function nearest(mx, my) {
|
| 418 |
+
let best = null, bd = 12;
|
| 419 |
+
for (const g of S.geo || []) {
|
| 420 |
+
for (let i = 1; i < g.xy.length; i++) {
|
| 421 |
+
const [ax, ay] = g.xy[i - 1], [bx, by] = g.xy[i];
|
| 422 |
+
const dx = bx - ax, dy = by - ay, L2 = dx * dx + dy * dy || 1;
|
| 423 |
+
const u = Math.max(0, Math.min(1, ((mx - ax) * dx + (my - ay) * dy) / L2));
|
| 424 |
+
const d = Math.hypot(mx - ax - u * dx, my - ay - u * dy);
|
| 425 |
+
if (d < bd) { bd = d; best = g; }
|
| 426 |
+
}
|
| 427 |
+
}
|
| 428 |
+
return best;
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
function tipHTML(r) {
|
| 432 |
+
const col = r.problems && r.problems.length ? "var(--critical)" : r.kind === "fast" ? "var(--fast)" : "var(--stop)";
|
| 433 |
+
const origin = r.route[0], dest = r.route[r.route.length - 1];
|
| 434 |
+
const dep = r.sched_dep[0] !== null ? hhmm(r.sched_dep[0]) : "";
|
| 435 |
+
const lines = [`<div><span class="key" style="border-color:${col}"></span><b class="num">${esc(r.train)}</b> <span class="m">${r.kind === "fast" ? "fast" : "stopping"} train · set ${esc(r.unit)}</span></div>`,
|
| 436 |
+
`<div class="m">${esc(origin)} ${dep} → ${esc(dest)}</div>`];
|
| 437 |
+
if (r.cancelled) lines.push(`<div><b>cancelled</b> · ${r.unserved} stops not served</div>`);
|
| 438 |
+
else lines.push(`<div><b class="num">${fmt(r.delay)}</b> <span class="m">stop-minutes late</span> · <b class="num">${fmt(r.unserved)}</b> <span class="m">stops not served</span></div>`);
|
| 439 |
+
const o = orderText(r.order);
|
| 440 |
+
if (o) lines.push(`<div><span class="m">plan:</span> ${esc(o)}</div>`);
|
| 441 |
+
if (r.problems && r.problems.length) lines.push(`<div style="color:var(--critical)">breaks: ${esc(r.problems.join(", "))}</div>`);
|
| 442 |
+
return lines.join("");
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
function onMove(e) {
|
| 446 |
+
const svg = $("#chart"), tip = $("#tip"), wrap = $("#chartwrap");
|
| 447 |
+
const rect = svg.getBoundingClientRect();
|
| 448 |
+
const sx = S.frame ? S.frame.W / rect.width : 1;
|
| 449 |
+
const mx = (e.clientX - rect.left) * sx, my = (e.clientY - rect.top) * sx;
|
| 450 |
+
const g = nearest(mx, my);
|
| 451 |
+
const group = $("#paths");
|
| 452 |
+
if (!g) { tip.hidden = true; group && group.classList.remove("focus"); return; }
|
| 453 |
+
group.classList.add("focus");
|
| 454 |
+
for (const el of group.querySelectorAll(".run")) el.classList.toggle("on", el.dataset.unit === g.r.unit);
|
| 455 |
+
tip.innerHTML = tipHTML(g.r);
|
| 456 |
+
tip.hidden = false;
|
| 457 |
+
const wr = wrap.getBoundingClientRect();
|
| 458 |
+
const left = e.clientX - wr.left + wrap.scrollLeft + 14, top = e.clientY - wr.top + 14;
|
| 459 |
+
tip.style.left = `${Math.min(left, wrap.scrollLeft + wr.width - tip.offsetWidth - 4)}px`;
|
| 460 |
+
tip.style.top = `${top}px`;
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
// ---- the panels around the chart -----------------------------------------------------------------------------
|
| 464 |
+
|
| 465 |
+
function legendKey(kind) {
|
| 466 |
+
const line = (stroke, dash = "", w = 2) => `<svg width="22" height="10" aria-hidden="true"><line x1="1" y1="5" x2="21" y2="5" stroke="${stroke}" stroke-width="${w}" stroke-linecap="round" ${dash ? `stroke-dasharray="${dash}"` : ""}/></svg>`;
|
| 467 |
+
const rect = (fill, stroke) => `<svg width="20" height="12" aria-hidden="true"><rect x="1" y="1" width="18" height="10" rx="2" fill="${fill}" stroke="${stroke}"/></svg>`;
|
| 468 |
+
return {
|
| 469 |
+
stop: line("var(--stop)"), fast: line("var(--fast)"), bad: line("var(--critical)", "7 4"), cancel: line("var(--muted)", "2 4", 1.4),
|
| 470 |
+
hold: line("var(--ink-2)", "", 6), turn: `<svg width="14" height="14" aria-hidden="true"><circle cx="7" cy="7" r="5" fill="var(--surface)" stroke="var(--ink-2)" stroke-width="2.2"/></svg>`,
|
| 471 |
+
single: rect("var(--single)", "var(--grid)"), closed: rect("url(#lg-closed)", "var(--critical)"),
|
| 472 |
+
slow: rect("var(--warning)", "var(--warning)"), track: rect("var(--serious)", "var(--serious)"),
|
| 473 |
+
}[kind];
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
function renderPanels(R) {
|
| 477 |
+
const where = (c) => {
|
| 478 |
+
const names = Object.fromEntries(R.points.map((p) => [p.code, p.name]));
|
| 479 |
+
const a = c.where[0], b = c.where[c.where.length - 1];
|
| 480 |
+
return a === b ? names[a] : `${names[a]} – ${names[b]}`;
|
| 481 |
+
};
|
| 482 |
+
const dis = R.closures.map((c) => `${where(c)} ${WHAT[c.kind] || c.kind}${c.kind === "slow" ? ` (+${c.extra} min)` : ""} ${hhmm(c.start)}–${hhmm(c.end)}${c.cause ? ` (${c.cause})` : ""}`);
|
| 483 |
+
$("#title").textContent = `${longDay(R.day)}, ${hhmm(R.start)}–${hhmm(R.end)}`;
|
| 484 |
+
const turned = R.short_turned, cx = R.cancelled;
|
| 485 |
+
const decisions = [turned ? `${turned} short-turn${turned === 1 ? "" : "s"}` : "", cx ? `${cx} cancellation${cx === 1 ? "" : "s"}` : ""].filter(Boolean).join(", ");
|
| 486 |
+
$("#sub").textContent = `${dis.join("; ") || "No closures"} · ${TIER[R.difficulty] || R.difficulty} · ${R.runs.length} trains. `
|
| 487 |
+
+ (FIGURE ? `${R.plan_id === "optimal" ? "The proven-optimal plan (CP-SAT)" : R.plan}: cost ${fmt(R.cost)} stop-minutes${decisions ? `, ${decisions}` : ""}.` : `Plan: ${R.plan}.`);
|
| 488 |
+
const nb = R.violations.length;
|
| 489 |
+
$("#badge").innerHTML = R.feasible ? `<span class="badge ok"><i>✓</i>legal plan</span>`
|
| 490 |
+
: `<span class="badge bad"><i>✕</i>${nb} rule break${nb === 1 ? "" : "s"}: scores under 0.2</span>`;
|
| 491 |
+
const over = R.cost !== null && R.optimal_cost ? R.cost - R.optimal_cost : null;
|
| 492 |
+
const stat = (k, v, small = "") => `<div class="stat"><span class="k">${k}</span><span class="v num">${v}${small ? `<small>${small}</small>` : ""}</span></div>`;
|
| 493 |
+
$("#stats").innerHTML = FIGURE ? "" : [
|
| 494 |
+
stat("Reward", R.reward.toFixed(3)),
|
| 495 |
+
stat("Cost, stop-minutes", R.cost !== null ? fmt(R.cost) : `${fmt(R.cost_if_legal)}`, R.cost === null ? "if it were legal" : over > 0 ? `+${fmt(over)} over the optimum` : "the optimum"),
|
| 496 |
+
stat("Optimum", fmt(R.optimal_cost), `floor ${fmt(R.floor_cost)}`),
|
| 497 |
+
stat("Delay", fmt(R.cost_parts.delay), "stop-min"),
|
| 498 |
+
stat("Stops not served", fmt(R.cost_parts.unserved / (R.rules.cancel_cost || 30))),
|
| 499 |
+
stat("Short-turns", fmt(R.short_turned)), stat("Cancelled", fmt(R.cancelled)),
|
| 500 |
+
].join("");
|
| 501 |
+
const br = $("#breaks");
|
| 502 |
+
br.hidden = !nb;
|
| 503 |
+
br.innerHTML = R.violations.slice(0, 12).map((v) => `<li>${esc(v.time || "")} ${esc((v.rule || "").replace(/_/g, " "))}: ${esc(v.detail || "")}</li>`).join("")
|
| 504 |
+
+ (nb > 12 ? `<li>… and ${nb - 12} more</li>` : "");
|
| 505 |
+
|
| 506 |
+
const kinds = new Set(R.closures.map((c) => c.kind));
|
| 507 |
+
const anyHold = R.runs.some((r) => r.order && r.order.hold && Object.keys(r.order.hold).length);
|
| 508 |
+
const anyTurn = R.runs.some((r) => r.order && (r.order.to || r.order.from));
|
| 509 |
+
const items = [["stop", "stopping train"], ["fast", "fast train"]];
|
| 510 |
+
if (R.runs.some((r) => r.problems && r.problems.length)) items.push(["bad", "breaks a rule"]);
|
| 511 |
+
if (anyHold) items.push(["hold", "held by the plan"]);
|
| 512 |
+
if (anyTurn) items.push(["turn", "short-turn (run ends or starts early)"]);
|
| 513 |
+
if (R.cancelled) items.push(["cancel", "cancelled (its timetable path)"]);
|
| 514 |
+
items.push(["single", "single track"]);
|
| 515 |
+
for (const k of ["closed", "slow", "track"]) if (kinds.has(k)) items.push([k, WHAT[k]]);
|
| 516 |
+
$("#legend").innerHTML = `<svg width="0" height="0" style="position:absolute"><defs><pattern id="lg-closed" width="5" height="5" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><rect width="5" height="5" fill="var(--critical)" fill-opacity="0.12"/><line x1="0" y1="0" x2="0" y2="5" stroke="var(--critical)" stroke-opacity="0.6" stroke-width="1.6"/></pattern></defs></svg>`
|
| 517 |
+
+ items.map(([k, t]) => `<span>${legendKey(k)}${esc(t)}</span>`).join("");
|
| 518 |
+
|
| 519 |
+
const rows = [...R.runs].sort((a, b) => (a.sched_dep[0] ?? 0) - (b.sched_dep[0] ?? 0));
|
| 520 |
+
$("#table").innerHTML = `<thead><tr><th>Train</th><th>Set</th><th>Type</th><th>Timetable</th><th>Plan</th><th class="n">Delay (stop-min)</th><th class="n">Stops not served</th><th>Rule breaks</th></tr></thead><tbody>`
|
| 521 |
+
+ rows.map((r) => `<tr><td class="num">${esc(r.train)}</td><td>${esc(r.unit)}</td><td>${r.kind === "fast" ? "fast" : "stopping"}</td>`
|
| 522 |
+
+ `<td>${esc(r.route[0])} ${r.sched_dep[0] !== null ? hhmm(r.sched_dep[0]) : ""} → ${esc(r.route[r.route.length - 1])}</td><td>${esc(orderText(r.order))}</td>`
|
| 523 |
+
+ `<td class="n">${fmt(r.delay)}</td><td class="n">${fmt(r.unserved)}</td><td>${esc((r.problems || []).join(", "))}</td></tr>`).join("") + "</tbody>";
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
// ---- loading ---------------------------------------------------------------------------------------------------
|
| 527 |
+
|
| 528 |
+
function setURL() {
|
| 529 |
+
if (EMBED && !params.has("embed")) return;
|
| 530 |
+
const p = new URLSearchParams();
|
| 531 |
+
if (params.has("embed")) p.set("embed", "1");
|
| 532 |
+
p.set("task", $("#task").value);
|
| 533 |
+
p.set("plan", $("#plan").value);
|
| 534 |
+
history.replaceState(null, "", `?${p}`);
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
async function loadPlans(taskId, want) {
|
| 538 |
+
const plans = await json(`/api/tasks/${encodeURIComponent(taskId)}/plans`);
|
| 539 |
+
const groups = { ref: [], model: [], live: [] };
|
| 540 |
+
for (const p of plans) {
|
| 541 |
+
const g = p.id.startsWith("run:") ? "model" : p.id.startsWith("episode:") ? "live" : "ref";
|
| 542 |
+
const r = p.reward === null || p.reward === undefined ? "" : ` · ${Number(p.reward).toFixed(3)}`;
|
| 543 |
+
groups[g].push(`<option value="${esc(p.id)}">${esc(p.label)}${r}</option>`);
|
| 544 |
+
}
|
| 545 |
+
$("#plan").innerHTML = [["ref", "References"], ["model", "Models' submitted plans"], ["live", "Live episodes"]]
|
| 546 |
+
.filter(([k]) => groups[k].length).map(([k, n]) => `<optgroup label="${n}">${groups[k].join("")}</optgroup>`).join("");
|
| 547 |
+
$("#plan").value = plans.some((p) => p.id === want) ? want : "optimal";
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
async function refresh() {
|
| 551 |
+
const task = $("#task").value, plan = $("#plan").value;
|
| 552 |
+
$("#open3d").href = `/twin/?task=${encodeURIComponent(task)}&plan=${encodeURIComponent(plan)}`;
|
| 553 |
+
setURL();
|
| 554 |
+
try {
|
| 555 |
+
S.R = await json(`/api/tasks/${encodeURIComponent(task)}/replay?plan=${encodeURIComponent(plan)}`);
|
| 556 |
+
renderPanels(S.R);
|
| 557 |
+
draw();
|
| 558 |
+
} catch (err) {
|
| 559 |
+
$("#title").innerHTML = `<span class="err">${esc(err.message)}</span>`;
|
| 560 |
+
}
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
(async () => {
|
| 564 |
+
if (EMBED) $("#nav").hidden = true;
|
| 565 |
+
if (FIGURE) { document.body.classList.add("figure"); $("#controls").hidden = true; $("#tablebox").hidden = true; }
|
| 566 |
+
if (params.get("theme")) document.documentElement.dataset.theme = params.get("theme");
|
| 567 |
+
try {
|
| 568 |
+
S.tasks = await json("/api/tasks");
|
| 569 |
+
} catch (err) {
|
| 570 |
+
$("#title").innerHTML = `<span class="err">${esc(err.message)}</span>`;
|
| 571 |
+
return;
|
| 572 |
+
}
|
| 573 |
+
const group = (split) => S.tasks.filter((t) => t.split === split).map((t) =>
|
| 574 |
+
`<option value="${esc(t.task_id)}">${shortDay(t.day)} ${t.start}–${t.end} · ${esc(TIER[t.difficulty] || t.difficulty)} · ${t.runs} trains</option>`).join("");
|
| 575 |
+
const splits = [...new Set(S.tasks.map((t) => t.split))].sort((a, b) => (a === "eval" ? -1 : b === "eval" ? 1 : a.localeCompare(b)));
|
| 576 |
+
$("#task").innerHTML = splits.map((sp) => `<optgroup label="${esc(sp)} split">${group(sp)}</optgroup>`).join("");
|
| 577 |
+
const want = params.get("task") || FEATURED;
|
| 578 |
+
$("#task").value = S.tasks.some((t) => t.task_id === want) ? want : S.tasks[0].task_id;
|
| 579 |
+
await loadPlans($("#task").value, params.get("plan") || "optimal");
|
| 580 |
+
$("#task").onchange = async () => { await loadPlans($("#task").value, $("#plan").value); refresh(); };
|
| 581 |
+
$("#plan").onchange = refresh;
|
| 582 |
+
const svg = $("#chart");
|
| 583 |
+
svg.addEventListener("pointermove", onMove);
|
| 584 |
+
svg.addEventListener("pointerleave", () => { $("#tip").hidden = true; $("#paths")?.classList.remove("focus"); });
|
| 585 |
+
let rt;
|
| 586 |
+
window.addEventListener("resize", () => { clearTimeout(rt); rt = setTimeout(draw, 120); });
|
| 587 |
+
await refresh();
|
| 588 |
+
document.body.dataset.ready = "1";
|
| 589 |
+
})();
|
| 590 |
+
</script>
|
| 591 |
+
</body>
|
| 592 |
+
</html>
|
openenv/rail_openenv/web/twin/SOURCES.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# The line twin: sources
|
| 2 |
+
|
| 3 |
+
`twin.json.gz` is built by `envs/rail_ops/tools/twin/build_twin.py` (`fetch`, then `build`). Frame: metres east and
|
| 4 |
+
north of 53.22 N, 6.19 E (equirectangular), stored as integer decimetres.
|
| 5 |
+
|
| 6 |
+
| What | Source and licence |
|
| 7 |
+
|---|---|
|
| 8 |
+
| track (every rail way near the line: main line, passing loops, yards), platforms, level crossings, station positions | © OpenStreetMap contributors, [ODbL 1.0](https://www.openstreetmap.org/copyright) (Overpass API, data of July-October 2026) |
|
| 9 |
+
| 42,654 buildings (within 700 m of the line), land cover, water, roads and cycle paths, tree rows and trees, wind turbines, windmills, towers, place names | © OpenStreetMap contributors, ODbL 1.0 (same queries) |
|
| 10 |
+
| which platform each direction uses at every station, and the usual terminal tracks | Rijden de Treinen train archive, January-June 2025 (planned platforms; CC BY 4.0, "Rijden de Treinen") |
|
| 11 |
+
| train times | `rail_core.replay`: the grader's own simulation of the plan (`check.simulate`) |
|
| 12 |
+
|
| 13 |
+
## Real, derived and drawn
|
| 14 |
+
|
| 15 |
+
**Real:**
|
| 16 |
+
- Track geometry, switches and station layout, as mapped in OSM. Trains follow it per car, so they take the curves
|
| 17 |
+
and the loop tracks.
|
| 18 |
+
- Each direction's platform at each station, from the record: Hurdegaryp 1 eastbound / 2 westbound, Feanwâlden 2 / 1,
|
| 19 |
+
De Westereen 2 / 1, Buitenpost 1 / 2, Grijpskerk 2 / 1, Zuidhorn 1 / 2.
|
| 20 |
+
- Leeuwarden bays 6b and 8, the tracks Leeuwarden - Groningen trains used there.
|
| 21 |
+
- The times: every arrival, departure and passing minute is the simulation's. Between them the picture moves
|
| 22 |
+
smoothly: zero speed at stops, passing speed kept through points the train does not stop at.
|
| 23 |
+
|
| 24 |
+
**Derived:**
|
| 25 |
+
- Groningen. OSM maps the station as rebuilt (Spoorzone works, 2026): tracks 5 and 6, which the record's 5a and 6a
|
| 26 |
+
trains used, end at buffer stops on the west side, so no train from Leeuwarden can reach them. The twin stands those
|
| 27 |
+
trains on OSM tracks 2 and 3, the island platform beside them that is reachable from the west.
|
| 28 |
+
- Terminal track per train: the record's usual split by train type (Leeuwarden: westbound fast trains to 8, stopping
|
| 29 |
+
trains to 6b; Groningen: fast 6a, stopping 5a). A second train on the same track stands behind the first.
|
| 30 |
+
- Which of the two tracks a train uses at a passing station: its direction's platform, or the other one when that is
|
| 31 |
+
taken or the train is turning back there.
|
| 32 |
+
- Level crossing barriers close about 35 seconds before a train reaches the crossing and open once it has passed.
|
| 33 |
+
- Signals: one at each entry of every single-track section, red while a train is inside or the section is closed.
|
| 34 |
+
They show the rule the grader enforces; they are not the line's real signals.
|
| 35 |
+
|
| 36 |
+
**Drawn (illustrative):**
|
| 37 |
+
- Building heights: OSM `height` or `building:levels` where tagged, else by building type (a house 7 m, a barn 8 m,
|
| 38 |
+
a warehouse 9 m).
|
| 39 |
+
- Heights of landmarks without a tag: windmills 16 m, wind turbines a 60 m hub, towers 25 m.
|
| 40 |
+
- Trees: the mapped trees and tree rows, plus woods filled at about one tree per 120 m² (capped at 60,000).
|
| 41 |
+
- Train models: every train is drawn as a two-car unit (about 55 m), white, with a blue stripe for stopping trains and
|
| 42 |
+
orange for fast ones. The record does not say which unit ran which train.
|
| 43 |
+
- The sky and the sun: the solar position for the task's date and minute at the line (local time, CET/CEST).
|
| 44 |
+
|
| 45 |
+
The replay never changes a cost or a reward. The viewer loads three.js 0.180 from the jsDelivr CDN.
|
openenv/rail_openenv/web/twin/app.js
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// The line twin: pick a task and a plan, and watch it run on the real line. Data: /api (rail_core.replay, the grader's
|
| 2 |
+
// own simulation) and ./twin.json.gz (OpenStreetMap). URL: ?task=<id>&plan=<id>&t=<HH:MM>&follow=<unit>&cam=map|chase|cab
|
| 3 |
+
|
| 4 |
+
import { Line } from "./geo.js";
|
| 5 |
+
import { buildSim, hhmm } from "./motion.js";
|
| 6 |
+
import { CostChart, SERIES, TrainGraph } from "./graph.js";
|
| 7 |
+
import { KIND_COLOR, TwinScene } from "./scene.js";
|
| 8 |
+
|
| 9 |
+
const $ = (s) => document.querySelector(s);
|
| 10 |
+
const params = new URLSearchParams(location.search);
|
| 11 |
+
const S = { t: 0, playing: false, speed: 60, sim: null, R: null, opt: null, sel: null, cam: "map", lastEvent: -1 };
|
| 12 |
+
const TIER = { real: "real disruption", blockage: "blockage", loop: "lost passing track", compound: "compound" };
|
| 13 |
+
const FEATURED = "gnlw-2025-03-10-0700-210m-blockage-0";
|
| 14 |
+
|
| 15 |
+
async function json(url) {
|
| 16 |
+
const r = await fetch(url);
|
| 17 |
+
if (!r.ok) throw new Error(`${url}: ${r.status} ${await r.text()}`);
|
| 18 |
+
return r.json();
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
async function loadTwin() {
|
| 22 |
+
const r = await fetch("./twin.json.gz");
|
| 23 |
+
if (!r.ok) throw new Error(`twin.json.gz: HTTP ${r.status}`);
|
| 24 |
+
const buf = new Uint8Array(await r.arrayBuffer());
|
| 25 |
+
if (buf[0] === 0x1f && buf[1] === 0x8b) {
|
| 26 |
+
const stream = new Blob([buf]).stream().pipeThrough(new DecompressionStream("gzip"));
|
| 27 |
+
return JSON.parse(await new Response(stream).text());
|
| 28 |
+
}
|
| 29 |
+
return JSON.parse(new TextDecoder().decode(buf)); // a proxy already decoded it
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
function parseClock(v, R) {
|
| 33 |
+
const m = /^(\d{1,2}):(\d{2})$/.exec(v || "");
|
| 34 |
+
if (!m) return null;
|
| 35 |
+
let t = +m[1] * 60 + +m[2];
|
| 36 |
+
if (t < R.start - 120) t += 1440;
|
| 37 |
+
return t;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
function weekday(day) {
|
| 41 |
+
const d = new Date(`${day}T12:00:00Z`);
|
| 42 |
+
return d.toLocaleDateString("en-GB", { weekday: "long", day: "numeric", month: "long", year: "numeric", timeZone: "UTC" });
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
function setURL() {
|
| 46 |
+
const p = new URLSearchParams();
|
| 47 |
+
p.set("task", $("#task").value);
|
| 48 |
+
p.set("plan", $("#plan").value);
|
| 49 |
+
p.set("t", hhmm(S.t));
|
| 50 |
+
if (S.sel) p.set("follow", S.sel);
|
| 51 |
+
if (S.cam !== "map") p.set("cam", S.cam);
|
| 52 |
+
history.replaceState(null, "", `?${p}`);
|
| 53 |
+
$("#graphlink").href = `/viewer/?task=${encodeURIComponent($("#task").value)}&plan=${encodeURIComponent($("#plan").value)}`;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
// --- panels ----------------------------------------------------------------------------------------------------
|
| 57 |
+
|
| 58 |
+
function renderScenario(R) {
|
| 59 |
+
const where = (c) => [...new Set([c.where[0], c.where[c.where.length - 1]])].join("–");
|
| 60 |
+
const what = { closed: "closed", slow: "slow running", track: "one platform track out of use" };
|
| 61 |
+
const items = R.closures.map((c) => `<li><b>${where(c)}</b> ${what[c.kind] || c.kind}${c.kind === "slow" ? ` (+${c.extra} min)` : ""}, `
|
| 62 |
+
+ `${hhmm(c.start)}–${hhmm(c.end)}<div class="note">${c.cause || ""}${c.real ? " · from the record" : ""}</div></li>`).join("");
|
| 63 |
+
const notes = (R.notices || []).slice(0, 3).map((n) => `<li class="note">${n}</li>`).join("");
|
| 64 |
+
$("#scenario").innerHTML = `<div class="day">${weekday(R.day)}</div>
|
| 65 |
+
<div class="muted">${hhmm(R.start)}–${hhmm(R.end)} · ${R.runs.length} trains<span class="tier">${TIER[R.difficulty] || R.difficulty}</span></div>
|
| 66 |
+
<ul>${items}${notes}</ul>`;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
function renderResult(R) {
|
| 70 |
+
const rb = R.violations.length;
|
| 71 |
+
const pct = R.cost !== null && R.optimal_cost ? Math.round((R.cost / R.optimal_cost - 1) * 100) : null;
|
| 72 |
+
const viol = R.violations.slice(0, 40).map((v, i) => `<li data-i="${i}">${v.time || ""} ${v.rule.replace("_", " ")}: ${v.detail}</li>`).join("");
|
| 73 |
+
$("#result").innerHTML = `<div class="plan-name">${R.plan}</div>
|
| 74 |
+
<div class="hero">${R.reward.toFixed(3)}<small>reward</small></div>
|
| 75 |
+
<div class="badge ${R.feasible ? "ok" : "bad"}">${R.feasible ? "legal plan" : `${rb} rule break${rb === 1 ? "" : "s"}: scores under 0.2`}</div>
|
| 76 |
+
<dl>
|
| 77 |
+
<dt>Cost</dt><dd>${R.cost !== null ? R.cost.toLocaleString() : `${R.cost_if_legal.toLocaleString()} if it were legal`}${pct !== null && pct > 0 ? ` (+${pct}% over the optimum)` : ""}</dd>
|
| 78 |
+
<dt>Optimum</dt><dd>${R.optimal_cost.toLocaleString()} (floor ${R.floor_cost.toLocaleString()})</dd>
|
| 79 |
+
<dt>Delay</dt><dd>${R.cost_parts.delay.toLocaleString()} stop-min</dd>
|
| 80 |
+
<dt>Unserved</dt><dd>${(R.cost_parts.unserved / (R.rules.cancel_cost || 30)).toLocaleString()} stops (${R.cost_parts.unserved.toLocaleString()})</dd>
|
| 81 |
+
<dt>Handed on</dt><dd>${R.cost_parts.handed_on.toLocaleString()}</dd>
|
| 82 |
+
<dt>Cancelled</dt><dd>${R.cancelled} runs · ${R.short_turned} turned short</dd>
|
| 83 |
+
</dl>
|
| 84 |
+
${rb ? `<ul class="viol">${viol}</ul>` : ""}`;
|
| 85 |
+
for (const li of document.querySelectorAll("#result .viol li")) {
|
| 86 |
+
li.onclick = () => {
|
| 87 |
+
const v = R.violations[+li.dataset.i];
|
| 88 |
+
const t = parseClock(v.time, R);
|
| 89 |
+
if (t !== null) seek(t - 1);
|
| 90 |
+
if (v.trains && v.trains[0]) selectRun(v.trains[0]);
|
| 91 |
+
};
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
function renderTrains(states) {
|
| 96 |
+
const ol = $("#trains");
|
| 97 |
+
const rows = states.slice().sort((a, b) => a.unit.localeCompare(b.unit));
|
| 98 |
+
$("#ntrains").textContent = `${rows.length} train sets`;
|
| 99 |
+
const html = rows.map((s) => {
|
| 100 |
+
const d = Math.round(s.delay);
|
| 101 |
+
return `<li data-u="${s.unit}" class="${S.sel === s.unit ? "sel" : ""}"><i class="chip" style="background:${KIND_COLOR[s.kind]}"></i>`
|
| 102 |
+
+ `<span class="num">${s.train}</span><span class="muted">${s.dir === "E" ? "→ Groningen" : "← Leeuwarden"}</span>`
|
| 103 |
+
+ `<span class="dl ${d > 0 ? "late" : ""}">${d > 0 ? `+${d} min` : "on time"}</span><span class="st">${s.status}</span></li>`;
|
| 104 |
+
}).join("");
|
| 105 |
+
if (ol.dataset.html !== html) {
|
| 106 |
+
ol.innerHTML = html;
|
| 107 |
+
ol.dataset.html = html;
|
| 108 |
+
for (const li of ol.children) li.onclick = () => select(li.dataset.u, true);
|
| 109 |
+
}
|
| 110 |
+
const cx = S.sim.cancelled();
|
| 111 |
+
$("#cxl-h").hidden = !cx.length;
|
| 112 |
+
$("#cancelled").innerHTML = cx.map((r) => `<li><i class="chip" style="background:${KIND_COLOR[r.kind]}"></i>`
|
| 113 |
+
+ `<span class="num">${r.train}</span><span>${r.route[0]} ${hhmm(r.sched_dep[0])} → ${r.route[r.route.length - 1]}</span>`
|
| 114 |
+
+ `<span>${r.stops.filter(Boolean).length} stops</span></li>`).join("");
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
function toast(ev) {
|
| 118 |
+
const box = $("#toasts");
|
| 119 |
+
const el = document.createElement("div");
|
| 120 |
+
el.className = `toast ${ev.kind}`;
|
| 121 |
+
el.innerHTML = `<time>${hhmm(ev.t)}</time>${ev.text}`;
|
| 122 |
+
box.prepend(el);
|
| 123 |
+
while (box.children.length > 4) box.lastChild.remove();
|
| 124 |
+
setTimeout(() => { el.style.opacity = 0; setTimeout(() => el.remove(), 700); }, 4200);
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
// --- selection and cameras -------------------------------------------------------------------------------------
|
| 128 |
+
|
| 129 |
+
let scene, graph, cost;
|
| 130 |
+
|
| 131 |
+
function select(unit, fly) {
|
| 132 |
+
S.sel = unit;
|
| 133 |
+
scene.selected = unit;
|
| 134 |
+
graph.sel = unit;
|
| 135 |
+
graph.draw();
|
| 136 |
+
if (fly && unit) setCam(S.cam === "cab" ? "cab" : "chase");
|
| 137 |
+
setURL();
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
function selectRun(train) {
|
| 141 |
+
const r = S.R.runs.find((x) => x.train === train);
|
| 142 |
+
if (r) select(r.unit, true);
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
function setCam(mode) {
|
| 146 |
+
S.cam = mode;
|
| 147 |
+
for (const [id, m] of [["#cam-map", "map"], ["#cam-follow", "chase"], ["#cam-cab", "cab"]]) $(id).classList.toggle("on", S.cam === m);
|
| 148 |
+
if (mode === "map") scene.viewAll();
|
| 149 |
+
else if (S.sel) scene.follow(S.sel, mode);
|
| 150 |
+
else {
|
| 151 |
+
const first = S.sim.statesAt(S.t).find((s) => s.moving) || S.sim.statesAt(S.t)[0];
|
| 152 |
+
if (first) { select(first.unit, false); scene.follow(first.unit, mode); }
|
| 153 |
+
}
|
| 154 |
+
setURL();
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
function seek(t) {
|
| 158 |
+
S.t = Math.max(S.sim.t0, Math.min(S.sim.t1, t));
|
| 159 |
+
S.lastEvent = S.R.events.findIndex((e) => e.t !== null && e.t > S.t) - 1;
|
| 160 |
+
if (S.lastEvent < -1) S.lastEvent = S.R.events.length - 1;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
// --- loading a task and a plan ----------------------------------------------------------------------------
|
| 164 |
+
|
| 165 |
+
async function loadPlans(taskId, want) {
|
| 166 |
+
const plans = await json(`/api/tasks/${encodeURIComponent(taskId)}/plans`);
|
| 167 |
+
$("#plan").innerHTML = plans.map((p) => `<option value="${p.id}">${p.label}${p.reward !== null && p.reward !== undefined ? ` · ${(+p.reward).toFixed(3)}` : ""}</option>`).join("");
|
| 168 |
+
const model = plans.find((p) => p.id.startsWith("run:"));
|
| 169 |
+
$("#plan").value = plans.some((p) => p.id === want) ? want : model ? model.id : "optimal";
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
async function loadReplay(keepTime) {
|
| 173 |
+
const taskId = $("#task").value, planId = $("#plan").value;
|
| 174 |
+
const [R, opt] = await Promise.all([
|
| 175 |
+
json(`/api/tasks/${encodeURIComponent(taskId)}/replay?plan=${encodeURIComponent(planId)}`),
|
| 176 |
+
planId === "optimal" ? null : json(`/api/tasks/${encodeURIComponent(taskId)}/replay?plan=optimal`),
|
| 177 |
+
]);
|
| 178 |
+
const sim = buildSim(R);
|
| 179 |
+
const prevT = S.t;
|
| 180 |
+
S.R = R; S.sim = sim; S.opt = opt || R;
|
| 181 |
+
if (!scene) {
|
| 182 |
+
scene = new TwinScene($("#stage"), TWIN, new Line(TWIN, R.points));
|
| 183 |
+
// a click on a train follows it; the end of a drag (panning the map) is not a click
|
| 184 |
+
let down = null;
|
| 185 |
+
const el = scene.labels.domElement;
|
| 186 |
+
el.addEventListener("pointerdown", (e) => { down = [e.clientX, e.clientY]; });
|
| 187 |
+
el.addEventListener("click", (e) => {
|
| 188 |
+
if (down && Math.hypot(e.clientX - down[0], e.clientY - down[1]) > 5) return;
|
| 189 |
+
const u = scene.pick(e.clientX, e.clientY);
|
| 190 |
+
if (u) select(u, true);
|
| 191 |
+
});
|
| 192 |
+
window.twin = { state: S, scene, seek, select, setCam }; // for the browser console and the viewer checks
|
| 193 |
+
}
|
| 194 |
+
scene.setReplay(R, sim);
|
| 195 |
+
graph.set(R, sim.t0, sim.t1);
|
| 196 |
+
const series = [{ name: planId === "optimal" ? "optimum" : "this plan", costs: R.costs, color: SERIES.plan }];
|
| 197 |
+
if (opt) series.push({ name: "optimum", costs: opt.costs, color: "#898781", dashed: true });
|
| 198 |
+
cost.set(series, sim.t0, sim.t1);
|
| 199 |
+
renderScenario(R);
|
| 200 |
+
renderResult(R);
|
| 201 |
+
const clock = parseClock(params.get("t"), R);
|
| 202 |
+
seek(keepTime ? prevT : clock !== null && !S.booted ? clock : sim.t0);
|
| 203 |
+
S.booted = true;
|
| 204 |
+
if (S.sel && !sim.units.some((u) => u.id === S.sel)) S.sel = null;
|
| 205 |
+
setURL();
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
// --- main loop ---------------------------------------------------------------------------------------------
|
| 209 |
+
|
| 210 |
+
let TWIN;
|
| 211 |
+
let last = performance.now(), lastList = 0;
|
| 212 |
+
|
| 213 |
+
function frame(now) {
|
| 214 |
+
const dt = Math.min(0.5, (now - last) / 1000);
|
| 215 |
+
last = now;
|
| 216 |
+
if (S.sim) {
|
| 217 |
+
if (S.playing) {
|
| 218 |
+
S.t += dt * S.speed / 60;
|
| 219 |
+
if (S.t >= S.sim.t1) { S.t = S.sim.t1; S.playing = false; $("#play").textContent = "▶"; }
|
| 220 |
+
// the plan's moments as they happen
|
| 221 |
+
while (S.lastEvent + 1 < S.R.events.length && S.R.events[S.lastEvent + 1].t !== null && S.R.events[S.lastEvent + 1].t <= S.t) {
|
| 222 |
+
S.lastEvent++;
|
| 223 |
+
if (S.speed <= 120) toast(S.R.events[S.lastEvent]);
|
| 224 |
+
}
|
| 225 |
+
}
|
| 226 |
+
const states = S.sim.statesAt(S.t);
|
| 227 |
+
scene.update(S.t, states, dt);
|
| 228 |
+
graph.setTime(S.t);
|
| 229 |
+
cost.setTime(S.t);
|
| 230 |
+
$("#clock b").textContent = `${hhmm(S.t)}:${String(Math.floor((S.t % 1) * 60)).padStart(2, "0")}`;
|
| 231 |
+
$("#clock span").textContent = `cost so far ${S.sim.costAt(S.t).toLocaleString()}`;
|
| 232 |
+
if (now - lastList > 250) { renderTrains(states); lastList = now; }
|
| 233 |
+
scene.render();
|
| 234 |
+
}
|
| 235 |
+
requestAnimationFrame(frame);
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
async function main() {
|
| 239 |
+
$("#loadmsg").textContent = "OpenStreetMap: track, stations, buildings…";
|
| 240 |
+
const [twin, tasks] = await Promise.all([loadTwin(), json("/api/tasks")]);
|
| 241 |
+
TWIN = twin;
|
| 242 |
+
const label = (t) => `${t.day} ${t.start}–${t.end} · ${TIER[t.difficulty] || t.difficulty} · ${t.runs} trains${t.model_plans ? " · ★" : ""}`;
|
| 243 |
+
const group = (split) => tasks.filter((t) => t.split === split).sort((a, b) => (b.model_plans - a.model_plans) || a.task_id.localeCompare(b.task_id));
|
| 244 |
+
$("#task").innerHTML = ["eval", "train"].map((sp) => `<optgroup label="${sp === "eval" ? "Eval (★ has a model's plan)" : "Train"}">`
|
| 245 |
+
+ group(sp).map((t) => `<option value="${t.task_id}">${label(t)}</option>`).join("") + "</optgroup>").join("");
|
| 246 |
+
const want = params.get("task");
|
| 247 |
+
$("#task").value = tasks.some((t) => t.task_id === want) ? want : tasks.some((t) => t.task_id === FEATURED) ? FEATURED : tasks[0].task_id;
|
| 248 |
+
$("#station").innerHTML += twin.stations.map((s) => `<option value="${s.code}">${s.name}</option>`).join("");
|
| 249 |
+
graph = new TrainGraph($("#graph"), $("#gtip"), (t) => seek(t), (u) => select(u, false));
|
| 250 |
+
cost = new CostChart($("#cost"), $("#ctip"), (t) => seek(t));
|
| 251 |
+
$("#loadmsg").textContent = "The plan: every train's simulated times…";
|
| 252 |
+
await loadPlans($("#task").value, params.get("plan"));
|
| 253 |
+
await loadReplay(false);
|
| 254 |
+
if (params.get("follow")) select(params.get("follow"), false);
|
| 255 |
+
if (params.get("cam") && params.get("cam") !== "map") setCam(params.get("cam"));
|
| 256 |
+
$("#loading").classList.add("done");
|
| 257 |
+
|
| 258 |
+
$("#task").onchange = async () => { S.sel = null; scene.selected = null; await loadPlans($("#task").value, $("#plan").value); await loadReplay(false); setCam("map"); };
|
| 259 |
+
$("#plan").onchange = () => loadReplay(true);
|
| 260 |
+
$("#play").onclick = () => { S.playing = !S.playing; if (S.playing && S.t >= S.sim.t1) seek(S.sim.t0); $("#play").textContent = S.playing ? "⏸" : "▶"; };
|
| 261 |
+
$("#restart").onclick = () => seek(S.sim.t0);
|
| 262 |
+
$("#speed").onchange = () => { S.speed = +$("#speed").value; };
|
| 263 |
+
$("#cam-map").onclick = () => setCam("map");
|
| 264 |
+
$("#cam-follow").onclick = () => setCam("chase");
|
| 265 |
+
$("#cam-cab").onclick = () => setCam("cab");
|
| 266 |
+
$("#panels").onclick = () => { document.body.classList.toggle("clean"); $("#panels").classList.toggle("on", document.body.classList.contains("clean")); };
|
| 267 |
+
$("#station").onchange = () => { if ($("#station").value) { S.cam = "map"; scene.viewStation($("#station").value); $("#station").value = ""; } };
|
| 268 |
+
window.addEventListener("keydown", (e) => {
|
| 269 |
+
if (e.target.tagName === "SELECT") return;
|
| 270 |
+
if (e.code === "Space") { e.preventDefault(); $("#play").click(); }
|
| 271 |
+
else if (e.code === "ArrowRight") seek(S.t + (e.shiftKey ? 10 : 1));
|
| 272 |
+
else if (e.code === "ArrowLeft") seek(S.t - (e.shiftKey ? 10 : 1));
|
| 273 |
+
else if (e.code === "Home") seek(S.sim.t0);
|
| 274 |
+
else if (e.key === "m") setCam("map");
|
| 275 |
+
else if (e.key === "f") setCam("chase");
|
| 276 |
+
else if (e.key === "c") setCam("cab");
|
| 277 |
+
else if (e.key === "h") $("#panels").click();
|
| 278 |
+
});
|
| 279 |
+
setInterval(setURL, 2000);
|
| 280 |
+
requestAnimationFrame(frame);
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
main().catch((e) => {
|
| 284 |
+
$("#loadmsg").textContent = `Could not start: ${e.message}`;
|
| 285 |
+
console.error(e);
|
| 286 |
+
});
|
openenv/rail_openenv/web/twin/geo.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// The line on the map: routes (OSM track paths built by tools/twin/build_twin.py) and where a network km lies on them.
|
| 2 |
+
//
|
| 3 |
+
// A route is one direction's lane from a terminal track to the other terminal track, through the platform tracks that
|
| 4 |
+
// direction uses in the record. Every network point has an anchor (its distance along the route), so km -> place is
|
| 5 |
+
// piecewise linear between anchors. A train that stands on the other track at a station (a turned train, or a second
|
| 6 |
+
// train there) blends onto the other direction's route inside that station's zone: fully between the switches that
|
| 7 |
+
// bound a loop, with a ramp where the zone ends on double track (a crossover).
|
| 8 |
+
//
|
| 9 |
+
// Frame: metres east (x) and north (y) of the twin origin. No DOM and no three.js here (node-testable).
|
| 10 |
+
|
| 11 |
+
export const CANONICAL = { E: "E-8-5a", W: "W-5a-8" };
|
| 12 |
+
const RAMP_SWITCH = 0.03; // km: zones bounded by a switch, where the two tracks meet
|
| 13 |
+
const RAMP_CROSS = 0.25; // km: zones that end on double track
|
| 14 |
+
|
| 15 |
+
export function smoothstep(u) {
|
| 16 |
+
const x = Math.min(1, Math.max(0, u));
|
| 17 |
+
return x * x * (3 - 2 * x);
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
export class Route {
|
| 21 |
+
constructor(key, data, kmOf) {
|
| 22 |
+
this.key = key;
|
| 23 |
+
this.dir = data.dir;
|
| 24 |
+
const p = data.p;
|
| 25 |
+
const n = p.length / 2;
|
| 26 |
+
this.x = new Float64Array(n);
|
| 27 |
+
this.y = new Float64Array(n);
|
| 28 |
+
this.s = new Float64Array(n);
|
| 29 |
+
for (let i = 0; i < n; i++) {
|
| 30 |
+
this.x[i] = p[2 * i] / 10;
|
| 31 |
+
this.y[i] = p[2 * i + 1] / 10;
|
| 32 |
+
if (i) this.s[i] = this.s[i - 1] + Math.hypot(this.x[i] - this.x[i - 1], this.y[i] - this.y[i - 1]);
|
| 33 |
+
}
|
| 34 |
+
this.length = this.s[n - 1];
|
| 35 |
+
// anchors sorted by km (the route's s grows with km eastbound, shrinks westbound)
|
| 36 |
+
this.anchors = Object.entries(data.anchors)
|
| 37 |
+
.filter(([c]) => kmOf[c] !== undefined)
|
| 38 |
+
.map(([c, s]) => [kmOf[c], s])
|
| 39 |
+
.sort((a, b) => a[0] - b[0]);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
sAtKm(km) {
|
| 43 |
+
const a = this.anchors;
|
| 44 |
+
if (km <= a[0][0]) return a[0][1] + (km - a[0][0]) * 1000 * (this.dir === "E" ? 1 : -1);
|
| 45 |
+
for (let i = 1; i < a.length; i++) {
|
| 46 |
+
if (km <= a[i][0]) {
|
| 47 |
+
const [k0, s0] = a[i - 1], [k1, s1] = a[i];
|
| 48 |
+
return s0 + (s1 - s0) * (km - k0) / (k1 - k0);
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
const [kl, sl] = a[a.length - 1];
|
| 52 |
+
return sl + (km - kl) * 1000 * (this.dir === "E" ? 1 : -1);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
kmAtS(s) {
|
| 56 |
+
const a = this.dir === "E" ? this.anchors : [...this.anchors].reverse();
|
| 57 |
+
if (s <= a[0][1]) return a[0][0] + (s - a[0][1]) / 1000 * (this.dir === "E" ? 1 : -1);
|
| 58 |
+
for (let i = 1; i < a.length; i++) {
|
| 59 |
+
if (s <= a[i][1]) {
|
| 60 |
+
const [k0, s0] = a[i - 1], [k1, s1] = a[i];
|
| 61 |
+
return k0 + (k1 - k0) * (s - s0) / (s1 - s0);
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
const [kl, sl] = a[a.length - 1];
|
| 65 |
+
return kl + (s - sl) / 1000 * (this.dir === "E" ? 1 : -1);
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
// [x, y] at distance s (clamped to the route)
|
| 69 |
+
pointAtS(s) {
|
| 70 |
+
const S = this.s;
|
| 71 |
+
const n = S.length;
|
| 72 |
+
if (s <= 0) return [this.x[0], this.y[0]];
|
| 73 |
+
if (s >= this.length) return [this.x[n - 1], this.y[n - 1]];
|
| 74 |
+
let lo = 0, hi = n - 1;
|
| 75 |
+
while (hi - lo > 1) {
|
| 76 |
+
const mid = (lo + hi) >> 1;
|
| 77 |
+
if (S[mid] <= s) lo = mid; else hi = mid;
|
| 78 |
+
}
|
| 79 |
+
const L = S[hi] - S[lo];
|
| 80 |
+
const t = L > 0 ? (s - S[lo]) / L : 0;
|
| 81 |
+
return [this.x[lo] + t * (this.x[hi] - this.x[lo]), this.y[lo] + t * (this.y[hi] - this.y[lo])];
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
pointAtKm(km) {
|
| 85 |
+
return this.pointAtS(this.sAtKm(km));
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
export class Line {
|
| 90 |
+
// twin: the decoded twin.json; points: [{code, km}] from the replay (the network)
|
| 91 |
+
constructor(twin, points) {
|
| 92 |
+
this.kmOf = Object.fromEntries(points.map((p) => [p.code, p.km]));
|
| 93 |
+
this.routes = {};
|
| 94 |
+
for (const [k, d] of Object.entries(twin.routes)) this.routes[k] = new Route(k, d, this.kmOf);
|
| 95 |
+
this.zones = twin.zones || {};
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
routeKey(dir, lw, gn) {
|
| 99 |
+
return dir === "E" ? `E-${lw}-${gn}` : `W-${gn}-${lw}`;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
route(dir, lw, gn) {
|
| 103 |
+
return this.routes[this.routeKey(dir, lw, gn)] || this.routes[CANONICAL[dir]];
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
// weight in [0, 1] of the other track at km, for a train standing on track `lane` at station `code`
|
| 107 |
+
blendWeight(code, km) {
|
| 108 |
+
const z = this.zones[code];
|
| 109 |
+
if (!z) return 0;
|
| 110 |
+
const [k0, k1, sw0, sw1] = z;
|
| 111 |
+
if (km < k0 || km > k1) return 0;
|
| 112 |
+
const r0 = sw0 ? RAMP_SWITCH : RAMP_CROSS, r1 = sw1 ? RAMP_SWITCH : RAMP_CROSS;
|
| 113 |
+
return Math.min(smoothstep((km - k0) / r0), smoothstep((k1 - km) / r1));
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
// [x, y] of a point km along a train's route; lanes: {station: "E" | "W"} where it uses that track
|
| 117 |
+
place(dir, lw, gn, km, lanes) {
|
| 118 |
+
const own = this.route(dir, lw, gn);
|
| 119 |
+
const p = own.pointAtKm(km);
|
| 120 |
+
if (!lanes) return p;
|
| 121 |
+
for (const [code, lane] of Object.entries(lanes)) {
|
| 122 |
+
if (lane === dir) continue;
|
| 123 |
+
const w = this.blendWeight(code, km);
|
| 124 |
+
if (w <= 0) continue;
|
| 125 |
+
const q = this.routes[CANONICAL[lane]].pointAtKm(km);
|
| 126 |
+
return [p[0] + w * (q[0] - p[0]), p[1] + w * (q[1] - p[1])];
|
| 127 |
+
}
|
| 128 |
+
return p;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
// points along a train: [[x, y], ...] at the given distances (metres) ahead (+) or behind (-) its centre, which
|
| 132 |
+
// is where the simulation puts it (a stopped train's centre is at the platform's stop point)
|
| 133 |
+
body(dir, lw, gn, kmCentre, offsets, lanes) {
|
| 134 |
+
const r = this.route(dir, lw, gn);
|
| 135 |
+
const s0 = r.sAtKm(kmCentre);
|
| 136 |
+
return offsets.map((d) => this.place(dir, lw, gn, r.kmAtS(s0 + d), lanes));
|
| 137 |
+
}
|
| 138 |
+
}
|
openenv/rail_openenv/web/twin/graph.js
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// The two charts under the map, on 2D canvases: the train graph (time across, distance down: every train's path,
|
| 2 |
+
// disruptions shaded, the moment being shown) with the plan's events on a strip below it, and the cost so far
|
| 3 |
+
// against the optimum's. Both are cursors on the same clock: click or drag to move it, hover to read a value.
|
| 4 |
+
|
| 5 |
+
import { hhmm } from "./motion.js";
|
| 6 |
+
|
| 7 |
+
const INK = { primary: "#ffffff", secondary: "#c3c2b7", muted: "#898781", grid: "#2c2c2a", axis: "#383835",
|
| 8 |
+
surface: "#1a1a19" };
|
| 9 |
+
export const SERIES = { stop: "#3987e5", fast: "#d95926", plan: "#199e70" };
|
| 10 |
+
const STATUS = { critical: "#d03b3b", warning: "#fab219", serious: "#ec835a", good: "#0ca30c" };
|
| 11 |
+
const GLYPH = { hold: "‖", turn: "↩", cancel: "✕", violation: "!", closure: "▼", reopen: "▲" };
|
| 12 |
+
|
| 13 |
+
function fit(canvas) {
|
| 14 |
+
const r = canvas.getBoundingClientRect();
|
| 15 |
+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
| 16 |
+
if (canvas.width !== Math.round(r.width * dpr) || canvas.height !== Math.round(r.height * dpr)) {
|
| 17 |
+
canvas.width = Math.round(r.width * dpr);
|
| 18 |
+
canvas.height = Math.round(r.height * dpr);
|
| 19 |
+
}
|
| 20 |
+
const g = canvas.getContext("2d");
|
| 21 |
+
g.setTransform(dpr, 0, 0, dpr, 0, 0);
|
| 22 |
+
return [g, r.width, r.height];
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
export class TrainGraph {
|
| 26 |
+
constructor(canvas, tip, onSeek, onPick) {
|
| 27 |
+
this.c = canvas;
|
| 28 |
+
this.tip = tip;
|
| 29 |
+
this.onSeek = onSeek;
|
| 30 |
+
this.onPick = onPick;
|
| 31 |
+
this.R = null;
|
| 32 |
+
this.t = 0;
|
| 33 |
+
this.sel = null;
|
| 34 |
+
this.hover = null;
|
| 35 |
+
let drag = false;
|
| 36 |
+
canvas.addEventListener("pointerdown", (e) => { drag = true; canvas.setPointerCapture(e.pointerId); this.seekAt(e, true); });
|
| 37 |
+
canvas.addEventListener("pointermove", (e) => { if (drag) this.seekAt(e, false); this.hoverAt(e); });
|
| 38 |
+
canvas.addEventListener("pointerup", () => { drag = false; });
|
| 39 |
+
canvas.addEventListener("pointerleave", () => { this.hover = null; this.tip.hidden = true; this.draw(); });
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
set(R, t0, t1) {
|
| 43 |
+
this.R = R;
|
| 44 |
+
this.t0 = t0;
|
| 45 |
+
this.t1 = t1;
|
| 46 |
+
this.km = Object.fromEntries(R.points.map((p) => [p.code, p.km]));
|
| 47 |
+
this.kmMax = Math.max(...R.points.map((p) => p.km));
|
| 48 |
+
// each served run as a polyline of (minute, km)
|
| 49 |
+
this.paths = R.runs.filter((r) => !r.cancelled).map((r) => {
|
| 50 |
+
const pts = [];
|
| 51 |
+
if (r.started && r.fixed_from !== null && r.fixed_from !== undefined) pts.push([r.fixed_from, this.km[r.route[r.fixed_to - 1]]]);
|
| 52 |
+
const from = r.started ? r.fixed_to : r.first;
|
| 53 |
+
for (let i = from; i <= r.last; i++) {
|
| 54 |
+
const k = this.km[r.route[i]];
|
| 55 |
+
if (r.arr[i] !== null && r.arr[i] !== undefined && !(i === from && !r.started)) pts.push([r.arr[i], k]);
|
| 56 |
+
if (r.dep[i] !== null && r.dep[i] !== undefined && i < r.last) pts.push([r.dep[i], k]);
|
| 57 |
+
}
|
| 58 |
+
return { train: r.train, unit: r.unit, kind: r.kind, pts, bad: r.problems.length > 0 };
|
| 59 |
+
});
|
| 60 |
+
this.draw();
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
frame() {
|
| 64 |
+
const [g, w, h] = fit(this.c);
|
| 65 |
+
const L = 44, Rp = 10, T = 8, B = 30, strip = 16;
|
| 66 |
+
return { g, w, h, L, Rp, T, B, strip, gh: h - T - B - strip,
|
| 67 |
+
x: (t) => L + (t - this.t0) / (this.t1 - this.t0) * (w - L - Rp),
|
| 68 |
+
tx: (x) => this.t0 + (x - L) / (w - L - Rp) * (this.t1 - this.t0),
|
| 69 |
+
y: (km) => T + km / this.kmMax * (h - T - B - strip) };
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
seekAt(e, pick) {
|
| 73 |
+
if (!this.R) return;
|
| 74 |
+
const f = this.frame();
|
| 75 |
+
const r = this.c.getBoundingClientRect();
|
| 76 |
+
const t = Math.max(this.t0, Math.min(this.t1, f.tx(e.clientX - r.left)));
|
| 77 |
+
this.onSeek(t);
|
| 78 |
+
if (pick && this.hover) this.onPick(this.hover.unit);
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
hoverAt(e) {
|
| 82 |
+
if (!this.R) return;
|
| 83 |
+
const f = this.frame();
|
| 84 |
+
const r = this.c.getBoundingClientRect();
|
| 85 |
+
const mx = e.clientX - r.left, my = e.clientY - r.top;
|
| 86 |
+
let best = null, bd = 10;
|
| 87 |
+
for (const p of this.paths) {
|
| 88 |
+
for (let i = 1; i < p.pts.length; i++) {
|
| 89 |
+
const [ta, ka] = p.pts[i - 1], [tb, kb] = p.pts[i];
|
| 90 |
+
const ax = f.x(ta), ay = f.y(ka), bx = f.x(tb), by = f.y(kb);
|
| 91 |
+
const dx = bx - ax, dy = by - ay, L2 = dx * dx + dy * dy || 1;
|
| 92 |
+
const u = Math.max(0, Math.min(1, ((mx - ax) * dx + (my - ay) * dy) / L2));
|
| 93 |
+
const d = Math.hypot(mx - ax - u * dx, my - ay - u * dy);
|
| 94 |
+
if (d < bd) { bd = d; best = { ...p, t: ta + u * (tb - ta), km: ka + u * (kb - ka) }; }
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
this.hover = best;
|
| 98 |
+
if (best) {
|
| 99 |
+
const code = Object.entries(this.km).reduce((a, [c, k]) => (Math.abs(k - best.km) < Math.abs(a[1] - best.km) ? [c, k] : a), ["", 1e9]);
|
| 100 |
+
const near = Math.abs(code[1] - best.km) < 0.3 ? `at ${code[0]}` : `km ${best.km.toFixed(1)}`;
|
| 101 |
+
this.tip.innerHTML = `<b>${best.train}</b> ${best.kind === "fast" ? "fast" : "stopping"} · ${hhmm(best.t)} ${near}`;
|
| 102 |
+
this.tip.style.left = `${Math.min(r.width - 180, mx + 12)}px`;
|
| 103 |
+
this.tip.style.top = `${my - 28}px`;
|
| 104 |
+
this.tip.hidden = false;
|
| 105 |
+
} else {
|
| 106 |
+
this.tip.hidden = true;
|
| 107 |
+
}
|
| 108 |
+
this.draw();
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
draw() {
|
| 112 |
+
if (!this.R) return;
|
| 113 |
+
const f = this.frame();
|
| 114 |
+
const { g, w, h, L, Rp, T, B, strip } = f;
|
| 115 |
+
g.clearRect(0, 0, w, h);
|
| 116 |
+
g.font = "11px system-ui, -apple-system, 'Segoe UI', sans-serif";
|
| 117 |
+
// disruptions
|
| 118 |
+
for (const c of this.R.closures) {
|
| 119 |
+
const a = this.km[c.where[0]], b = this.km[c.where[c.where.length - 1]];
|
| 120 |
+
const k0 = c.kind === "track" ? a - 0.4 : Math.min(a, b), k1 = c.kind === "track" ? a + 0.4 : Math.max(a, b);
|
| 121 |
+
g.fillStyle = c.kind === "closed" ? STATUS.critical : c.kind === "slow" ? STATUS.warning : STATUS.serious;
|
| 122 |
+
g.globalAlpha = 0.22;
|
| 123 |
+
g.fillRect(f.x(c.start), f.y(k0), f.x(c.end) - f.x(c.start), Math.max(3, f.y(k1) - f.y(k0)));
|
| 124 |
+
g.globalAlpha = 1;
|
| 125 |
+
}
|
| 126 |
+
// stations: hairlines and names
|
| 127 |
+
g.textAlign = "right";
|
| 128 |
+
g.textBaseline = "middle";
|
| 129 |
+
let lastY = -Infinity;
|
| 130 |
+
for (const p of this.R.points) {
|
| 131 |
+
const y = f.y(p.km);
|
| 132 |
+
g.strokeStyle = INK.grid;
|
| 133 |
+
g.lineWidth = 1;
|
| 134 |
+
g.beginPath(); g.moveTo(L, y + 0.5); g.lineTo(w - Rp, y + 0.5); g.stroke();
|
| 135 |
+
const major = p.kind === "terminal" || p.turn; // terminals and turn points first
|
| 136 |
+
if (p.kind !== "junction" && (y - lastY >= 11 || major)) {
|
| 137 |
+
g.fillStyle = major ? INK.secondary : INK.muted;
|
| 138 |
+
g.fillText(p.code, L - 6, y);
|
| 139 |
+
lastY = y;
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
+
// hours
|
| 143 |
+
g.textAlign = "center";
|
| 144 |
+
g.textBaseline = "top";
|
| 145 |
+
const step = this.t1 - this.t0 > 300 ? 60 : 30;
|
| 146 |
+
for (let t = Math.ceil(this.t0 / step) * step; t <= this.t1; t += step) {
|
| 147 |
+
const x = f.x(t);
|
| 148 |
+
g.strokeStyle = INK.grid;
|
| 149 |
+
g.beginPath(); g.moveTo(x + 0.5, T); g.lineTo(x + 0.5, T + f.gh); g.stroke();
|
| 150 |
+
g.fillStyle = INK.muted;
|
| 151 |
+
g.fillText(hhmm(t), x, h - B + strip + 6);
|
| 152 |
+
}
|
| 153 |
+
// the window's start and end
|
| 154 |
+
g.strokeStyle = INK.axis;
|
| 155 |
+
for (const t of [this.R.start, this.R.end]) {
|
| 156 |
+
const x = f.x(t);
|
| 157 |
+
g.beginPath(); g.moveTo(x + 0.5, T); g.lineTo(x + 0.5, T + f.gh); g.stroke();
|
| 158 |
+
}
|
| 159 |
+
// train paths
|
| 160 |
+
const focus = this.sel ?? (this.hover && this.hover.unit);
|
| 161 |
+
for (const p of this.paths) {
|
| 162 |
+
const on = !focus || p.unit === focus;
|
| 163 |
+
g.strokeStyle = SERIES[p.kind] || SERIES.stop;
|
| 164 |
+
g.globalAlpha = on ? 1 : 0.28;
|
| 165 |
+
g.lineWidth = focus && p.unit === focus ? 3 : 1.5;
|
| 166 |
+
g.lineJoin = "round";
|
| 167 |
+
g.beginPath();
|
| 168 |
+
p.pts.forEach(([t, k], i) => (i ? g.lineTo(f.x(t), f.y(k)) : g.moveTo(f.x(t), f.y(k))));
|
| 169 |
+
g.stroke();
|
| 170 |
+
}
|
| 171 |
+
g.globalAlpha = 1;
|
| 172 |
+
// rule breaks: a cross where and when each happens
|
| 173 |
+
g.strokeStyle = STATUS.critical;
|
| 174 |
+
g.lineWidth = 2;
|
| 175 |
+
for (const v of this.R.violations) {
|
| 176 |
+
if (!v.time || !v.where) continue;
|
| 177 |
+
const [hh, mm] = v.time.split(":").map(Number);
|
| 178 |
+
let t = hh * 60 + mm;
|
| 179 |
+
if (t < this.R.start - 120) t += 1440;
|
| 180 |
+
const ends = v.where.split("-").filter((c) => this.km[c] !== undefined);
|
| 181 |
+
if (!ends.length) continue;
|
| 182 |
+
const k = ends.reduce((a, c) => a + this.km[c], 0) / ends.length;
|
| 183 |
+
const x = f.x(t), y = f.y(k);
|
| 184 |
+
g.beginPath(); g.moveTo(x - 4, y - 4); g.lineTo(x + 4, y + 4); g.moveTo(x + 4, y - 4); g.lineTo(x - 4, y + 4); g.stroke();
|
| 185 |
+
}
|
| 186 |
+
// events strip
|
| 187 |
+
const sy = T + f.gh + 4;
|
| 188 |
+
g.textAlign = "center";
|
| 189 |
+
g.textBaseline = "top";
|
| 190 |
+
g.font = "bold 11px system-ui, -apple-system, 'Segoe UI', sans-serif";
|
| 191 |
+
for (const ev of this.R.events) {
|
| 192 |
+
if (ev.t === null || ev.t === undefined) continue;
|
| 193 |
+
const x = f.x(ev.t);
|
| 194 |
+
g.fillStyle = ev.kind === "violation" || ev.kind === "closure" ? STATUS.critical : ev.kind === "reopen" ? STATUS.good : INK.secondary;
|
| 195 |
+
g.fillText(GLYPH[ev.kind] || "·", x, sy);
|
| 196 |
+
}
|
| 197 |
+
// the cursor
|
| 198 |
+
const x = f.x(this.t);
|
| 199 |
+
g.strokeStyle = INK.primary;
|
| 200 |
+
g.lineWidth = 1.5;
|
| 201 |
+
g.beginPath(); g.moveTo(x, T); g.lineTo(x, T + f.gh + strip); g.stroke();
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
setTime(t) {
|
| 205 |
+
this.t = t;
|
| 206 |
+
this.draw();
|
| 207 |
+
}
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
export class CostChart {
|
| 211 |
+
constructor(canvas, tip, onSeek) {
|
| 212 |
+
this.c = canvas;
|
| 213 |
+
this.tip = tip;
|
| 214 |
+
this.onSeek = onSeek;
|
| 215 |
+
this.t = 0;
|
| 216 |
+
canvas.addEventListener("pointermove", (e) => this.hoverAt(e));
|
| 217 |
+
canvas.addEventListener("pointerleave", () => { this.tip.hidden = true; });
|
| 218 |
+
canvas.addEventListener("pointerdown", (e) => {
|
| 219 |
+
const f = this.frame(), r = canvas.getBoundingClientRect();
|
| 220 |
+
this.onSeek(Math.max(this.t0, Math.min(this.t1, f.tx(e.clientX - r.left))));
|
| 221 |
+
});
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
// series: [{name, costs: [[t, amount], ...], color, dashed}]
|
| 225 |
+
set(series, t0, t1) {
|
| 226 |
+
this.series = series.map((s) => {
|
| 227 |
+
let sum = 0;
|
| 228 |
+
const steps = [[t0, 0]];
|
| 229 |
+
for (const [t, a] of s.costs) { sum += a; steps.push([t, sum]); }
|
| 230 |
+
return { ...s, steps, total: sum };
|
| 231 |
+
});
|
| 232 |
+
this.t0 = t0;
|
| 233 |
+
this.t1 = t1;
|
| 234 |
+
this.max = Math.max(10, ...this.series.map((s) => s.total)) * 1.08;
|
| 235 |
+
this.draw();
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
frame() {
|
| 239 |
+
const [g, w, h] = fit(this.c);
|
| 240 |
+
const L = 44, Rp = 92, T = 22, B = 22;
|
| 241 |
+
return { g, w, h, L, Rp, T, B,
|
| 242 |
+
x: (t) => L + (t - this.t0) / (this.t1 - this.t0) * (w - L - Rp),
|
| 243 |
+
tx: (x) => this.t0 + (x - L) / (w - L - Rp) * (this.t1 - this.t0),
|
| 244 |
+
y: (v) => h - B - v / this.max * (h - T - B) };
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
valueAt(s, t) {
|
| 248 |
+
let v = 0;
|
| 249 |
+
for (const [tt, sum] of s.steps) { if (tt > t) break; v = sum; }
|
| 250 |
+
return v;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
hoverAt(e) {
|
| 254 |
+
if (!this.series) return;
|
| 255 |
+
const f = this.frame(), r = this.c.getBoundingClientRect();
|
| 256 |
+
const t = Math.max(this.t0, Math.min(this.t1, f.tx(e.clientX - r.left)));
|
| 257 |
+
this.tip.innerHTML = `${hhmm(t)}<br>` + this.series.map((s) => `${s.name}: <b>${this.valueAt(s, t).toLocaleString()}</b>`).join("<br>");
|
| 258 |
+
this.tip.style.left = `${Math.min(r.width - 150, e.clientX - r.left + 12)}px`;
|
| 259 |
+
this.tip.style.top = "6px";
|
| 260 |
+
this.tip.hidden = false;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
draw() {
|
| 264 |
+
if (!this.series) return;
|
| 265 |
+
const f = this.frame();
|
| 266 |
+
const { g, w, h, L, Rp, T, B } = f;
|
| 267 |
+
g.clearRect(0, 0, w, h);
|
| 268 |
+
g.font = "11px system-ui, -apple-system, 'Segoe UI', sans-serif";
|
| 269 |
+
g.fillStyle = INK.secondary;
|
| 270 |
+
g.textAlign = "left";
|
| 271 |
+
g.textBaseline = "top";
|
| 272 |
+
g.fillText("Cost so far (stop-minutes)", L, 4);
|
| 273 |
+
// y grid: three hairlines
|
| 274 |
+
g.textAlign = "right";
|
| 275 |
+
g.textBaseline = "middle";
|
| 276 |
+
for (let i = 0; i <= 3; i++) {
|
| 277 |
+
const v = (this.max / 1.08) * i / 3;
|
| 278 |
+
const y = Math.round(f.y(v)) + 0.5;
|
| 279 |
+
g.strokeStyle = i ? INK.grid : INK.axis;
|
| 280 |
+
g.beginPath(); g.moveTo(L, y); g.lineTo(w - Rp, y); g.stroke();
|
| 281 |
+
g.fillStyle = INK.muted;
|
| 282 |
+
g.fillText(Math.round(v).toLocaleString(), L - 6, y);
|
| 283 |
+
}
|
| 284 |
+
// the series as step lines, with the total labelled at the end
|
| 285 |
+
for (const s of this.series) {
|
| 286 |
+
g.strokeStyle = s.color;
|
| 287 |
+
g.lineWidth = 2;
|
| 288 |
+
g.setLineDash(s.dashed ? [5, 4] : []);
|
| 289 |
+
g.beginPath();
|
| 290 |
+
let prev = null;
|
| 291 |
+
for (const [t, v] of s.steps) {
|
| 292 |
+
const x = f.x(Math.min(this.t1, t)), y = f.y(v);
|
| 293 |
+
if (prev === null) g.moveTo(x, y); else { g.lineTo(x, prev); g.lineTo(x, y); }
|
| 294 |
+
prev = y;
|
| 295 |
+
}
|
| 296 |
+
g.lineTo(f.x(this.t1), prev);
|
| 297 |
+
g.stroke();
|
| 298 |
+
g.setLineDash([]);
|
| 299 |
+
g.fillStyle = INK.primary;
|
| 300 |
+
g.textAlign = "left";
|
| 301 |
+
g.fillText(`${s.name} ${s.total.toLocaleString()}`, w - Rp + 6, Math.max(T, Math.min(h - B, f.y(s.total))));
|
| 302 |
+
}
|
| 303 |
+
const x = f.x(this.t);
|
| 304 |
+
g.strokeStyle = INK.primary;
|
| 305 |
+
g.lineWidth = 1.5;
|
| 306 |
+
g.beginPath(); g.moveTo(x, T); g.lineTo(x, h - B); g.stroke();
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
setTime(t) {
|
| 310 |
+
this.t = t;
|
| 311 |
+
this.draw();
|
| 312 |
+
}
|
| 313 |
+
}
|
openenv/rail_openenv/web/twin/index.html
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
+
<title>RailSimEnv line twin</title>
|
| 7 |
+
<link rel="icon" href="data:,">
|
| 8 |
+
<link rel="stylesheet" href="style.css">
|
| 9 |
+
<script type="importmap">
|
| 10 |
+
{
|
| 11 |
+
"imports": {
|
| 12 |
+
"three": "https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js",
|
| 13 |
+
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/"
|
| 14 |
+
}
|
| 15 |
+
}
|
| 16 |
+
</script>
|
| 17 |
+
</head>
|
| 18 |
+
<body>
|
| 19 |
+
<div id="stage"></div>
|
| 20 |
+
|
| 21 |
+
<header id="bar">
|
| 22 |
+
<div class="brand"><b>RailSimEnv</b><span>Leeuwarden – Groningen</span></div>
|
| 23 |
+
<label class="pick"><span>Task</span><select id="task" aria-label="Task"></select></label>
|
| 24 |
+
<label class="pick"><span>Plan</span><select id="plan" aria-label="Plan"></select></label>
|
| 25 |
+
<div class="transport">
|
| 26 |
+
<button id="restart" title="Back to the start (Home)" aria-label="Back to the start">⏮</button>
|
| 27 |
+
<button id="play" title="Play / pause (Space)" aria-label="Play">▶</button>
|
| 28 |
+
<select id="speed" title="Playback speed" aria-label="Playback speed">
|
| 29 |
+
<option value="1">1×</option><option value="10">10×</option><option value="30">30×</option>
|
| 30 |
+
<option value="60" selected>60×</option><option value="120">120×</option><option value="300">300×</option>
|
| 31 |
+
</select>
|
| 32 |
+
<div id="clock" aria-live="off"><b>--:--</b><span></span></div>
|
| 33 |
+
</div>
|
| 34 |
+
<div class="cams" role="group" aria-label="Camera">
|
| 35 |
+
<button id="cam-map" class="on" title="The whole line (M)">Line</button>
|
| 36 |
+
<button id="cam-follow" title="Follow the selected train (F)">Follow</button>
|
| 37 |
+
<button id="cam-cab" title="Driver's view of the selected train (C)">Cab</button>
|
| 38 |
+
<select id="station" aria-label="Go to a station"><option value="">Station…</option></select>
|
| 39 |
+
</div>
|
| 40 |
+
<button id="panels" title="Hide or show the panels (H)">Panels</button>
|
| 41 |
+
<a class="graphlink" id="graphlink" href="/viewer/" title="This plan on the train graph">Train graph ↗</a>
|
| 42 |
+
<a class="graphlink" href="/explorer/" title="Every model's plan on every eval task">Eval explorer ↗</a>
|
| 43 |
+
</header>
|
| 44 |
+
|
| 45 |
+
<aside id="left" class="panel">
|
| 46 |
+
<section id="scenario"></section>
|
| 47 |
+
<section id="result"></section>
|
| 48 |
+
<section class="legend">
|
| 49 |
+
<div><i class="chip" style="background:#3987e5"></i>Stopping train <i class="chip" style="background:#d95926"></i>Fast train</div>
|
| 50 |
+
<div><i class="bar closed"></i>Closed <i class="bar slow"></i>Slow <i class="bar track"></i>Track out of use</div>
|
| 51 |
+
<div><i class="bar single"></i>Single track <i class="bar double"></i>Double track (from far away)</div>
|
| 52 |
+
<div class="muted">Signals guard each single-track section: red while a train is in it or it is closed.</div>
|
| 53 |
+
</section>
|
| 54 |
+
</aside>
|
| 55 |
+
|
| 56 |
+
<aside id="right" class="panel">
|
| 57 |
+
<h2>Trains <span id="ntrains" class="muted"></span></h2>
|
| 58 |
+
<ol id="trains"></ol>
|
| 59 |
+
<h3 id="cxl-h" hidden>Cancelled</h3>
|
| 60 |
+
<ol id="cancelled"></ol>
|
| 61 |
+
</aside>
|
| 62 |
+
|
| 63 |
+
<footer id="bottom" class="panel">
|
| 64 |
+
<div class="graphwrap">
|
| 65 |
+
<div class="chart-h"><b>Train graph</b><span class="muted">time across, distance down · click to jump, hover to read</span></div>
|
| 66 |
+
<canvas id="graph"></canvas>
|
| 67 |
+
<div id="gtip" class="tip" hidden></div>
|
| 68 |
+
</div>
|
| 69 |
+
<div class="costwrap">
|
| 70 |
+
<canvas id="cost"></canvas>
|
| 71 |
+
<div id="ctip" class="tip" hidden></div>
|
| 72 |
+
</div>
|
| 73 |
+
</footer>
|
| 74 |
+
|
| 75 |
+
<div id="toasts" aria-live="polite"></div>
|
| 76 |
+
<div id="loading"><div><b>Building the line</b><p id="loadmsg">OpenStreetMap: track, stations, buildings…</p></div></div>
|
| 77 |
+
<noscript>The line twin needs JavaScript and WebGL.</noscript>
|
| 78 |
+
<script type="module" src="app.js"></script>
|
| 79 |
+
</body>
|
| 80 |
+
</html>
|
openenv/rail_openenv/web/twin/motion.js
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Trains in time: where every train set is at any moment under a plan, from the replay's simulated times
|
| 2 |
+
// (rail_core.replay, which uses the grader's own simulate). Nothing here changes a time: positions between the
|
| 3 |
+
// simulated minutes are a smooth monotone curve (stopped trains have zero speed, passing trains keep theirs).
|
| 4 |
+
//
|
| 5 |
+
// A train set's window is a chain of segments: it stands (at a station, a turn point, a terminal), moves (one run, or
|
| 6 |
+
// the part of it the plan keeps), stands again. Track allocation is for the picture only:
|
| 7 |
+
// - terminals: the record's usual tracks (Leeuwarden 6b/8, Groningen 5a/6a by train type), a second train on the
|
| 8 |
+
// same track stands behind the first;
|
| 9 |
+
// - two-track stations: each direction's own platform (as in the record); a train that finds it taken, or a turned
|
| 10 |
+
// train, uses the other track.
|
| 11 |
+
//
|
| 12 |
+
// No DOM and no three.js here (node-testable). Times are minutes after midnight of the service day; km as the network.
|
| 13 |
+
|
| 14 |
+
export const SLOT_KM = 0.065; // a second train on a terminal track stands this far behind the first
|
| 15 |
+
export const TRAIN_M = 55; // a two-car unit, metres
|
| 16 |
+
const PAD = 0.5; // minutes of margin when sharing a station track
|
| 17 |
+
|
| 18 |
+
export function lwTrack(run, arriving) {
|
| 19 |
+
// record (Jan-Jun 2025): westbound arrivals fast -> 8, stopping -> 6b; eastbound departures fast -> 6b, stop -> 8
|
| 20 |
+
if (arriving) return run.kind === "fast" ? "8" : "6b";
|
| 21 |
+
return run.kind === "fast" ? "6b" : "8";
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
export function gnTrack(run) {
|
| 25 |
+
return run.kind === "fast" ? "6a" : "5a"; // record: fast trains mostly 6a, stopping trains 5a
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
export function hhmm(m) {
|
| 29 |
+
if (m === null || m === undefined || !Number.isFinite(m)) return "--:--";
|
| 30 |
+
const t = Math.floor(m + 1e-9);
|
| 31 |
+
return `${String(Math.floor(t / 60) % 24).padStart(2, "0")}:${String(t % 60).padStart(2, "0")}`;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
// --- monotone cubic Hermite through (t, x) knots; stop knots have zero slope ---------------------------------
|
| 35 |
+
|
| 36 |
+
export function hermite(knots) {
|
| 37 |
+
const n = knots.length;
|
| 38 |
+
const t = knots.map((k) => k.t), x = knots.map((k) => k.x), m = new Array(n).fill(0);
|
| 39 |
+
for (let i = 0; i < n; i++) {
|
| 40 |
+
if (knots[i].stop) continue;
|
| 41 |
+
const d0 = i > 0 ? (x[i] - x[i - 1]) / (t[i] - t[i - 1]) : null;
|
| 42 |
+
const d1 = i < n - 1 ? (x[i + 1] - x[i]) / (t[i + 1] - t[i]) : null;
|
| 43 |
+
if (d0 === null) m[i] = d1;
|
| 44 |
+
else if (d1 === null) m[i] = d0;
|
| 45 |
+
else if (d0 * d1 <= 0) m[i] = 0;
|
| 46 |
+
else {
|
| 47 |
+
const h0 = t[i] - t[i - 1], h1 = t[i + 1] - t[i];
|
| 48 |
+
const w1 = 2 * h1 + h0, w2 = h1 + 2 * h0;
|
| 49 |
+
m[i] = (w1 + w2) / (w1 / d0 + w2 / d1);
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
return { t, x, m };
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
export function evalHermite(H, tq) {
|
| 56 |
+
const { t, x, m } = H;
|
| 57 |
+
const n = t.length;
|
| 58 |
+
if (n === 1 || tq <= t[0]) return [x[0], 0];
|
| 59 |
+
if (tq >= t[n - 1]) return [x[n - 1], 0];
|
| 60 |
+
let lo = 0, hi = n - 1;
|
| 61 |
+
while (hi - lo > 1) {
|
| 62 |
+
const mid = (lo + hi) >> 1;
|
| 63 |
+
if (t[mid] <= tq) lo = mid; else hi = mid;
|
| 64 |
+
}
|
| 65 |
+
const h = t[hi] - t[lo];
|
| 66 |
+
const u = (tq - t[lo]) / h, u2 = u * u, u3 = u2 * u;
|
| 67 |
+
const pos = (2 * u3 - 3 * u2 + 1) * x[lo] + (u3 - 2 * u2 + u) * h * m[lo] + (-2 * u3 + 3 * u2) * x[hi]
|
| 68 |
+
+ (u3 - u2) * h * m[hi];
|
| 69 |
+
const vel = ((6 * u2 - 6 * u) * x[lo] + (3 * u2 - 4 * u + 1) * h * m[lo] + (-6 * u2 + 6 * u) * x[hi]
|
| 70 |
+
+ (3 * u2 - 2 * u) * h * m[hi]) / h;
|
| 71 |
+
return [pos, vel];
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
// --- the simulation picture ------------------------------------------------------------------------------------
|
| 75 |
+
|
| 76 |
+
export function buildSim(R) {
|
| 77 |
+
const kmOf = Object.fromEntries(R.points.map((p) => [p.code, p.km]));
|
| 78 |
+
const point = Object.fromEntries(R.points.map((p) => [p.code, p]));
|
| 79 |
+
const codes = R.points.map((p) => p.code);
|
| 80 |
+
const west = codes[0], east = codes[codes.length - 1];
|
| 81 |
+
const runOf = new Map(R.runs.map((r) => [r.train, r]));
|
| 82 |
+
const units = [];
|
| 83 |
+
|
| 84 |
+
for (const u of R.units) {
|
| 85 |
+
const runs = u.runs.map((id) => runOf.get(id)).filter(Boolean);
|
| 86 |
+
const segs = [];
|
| 87 |
+
let at = u.at, since = R.start, prev = null;
|
| 88 |
+
for (const r of runs) {
|
| 89 |
+
if (r.cancelled) continue;
|
| 90 |
+
const s = r.first, e = r.last;
|
| 91 |
+
const k = r.started ? r.fixed_to : s;
|
| 92 |
+
let t0;
|
| 93 |
+
if (r.started && r.fixed_from !== null && r.fixed_from !== undefined) {
|
| 94 |
+
t0 = r.fixed_from; // already running when the window starts
|
| 95 |
+
} else {
|
| 96 |
+
const where = r.route[k];
|
| 97 |
+
const dep = r.dep[k];
|
| 98 |
+
// an illegal plan can start a run where its train set is not (a continuity break): the set waits where it
|
| 99 |
+
// is and jumps when the run leaves, as the grader's simulation has it
|
| 100 |
+
const standAt = prev && at && at !== where ? at : where;
|
| 101 |
+
segs.push({ type: "stand", code: standAt, t0: Math.min(since, dep), t1: dep, prev, next: r });
|
| 102 |
+
t0 = dep;
|
| 103 |
+
}
|
| 104 |
+
segs.push({ type: "move", run: r, dir: r.dir, t0, t1: r.arr[e], k, s, e, lanes: {} });
|
| 105 |
+
at = r.route[e];
|
| 106 |
+
since = r.arr[e];
|
| 107 |
+
prev = r;
|
| 108 |
+
}
|
| 109 |
+
if (at) segs.push({ type: "stand", code: at, t0: since, t1: Infinity, prev, next: null });
|
| 110 |
+
units.push({ id: u.id, runs, segs });
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
// terminals: which track, and which slot on it
|
| 114 |
+
const slotUse = {};
|
| 115 |
+
const standsAtTerminals = [];
|
| 116 |
+
for (const un of units) {
|
| 117 |
+
un.segs.forEach((sg, i) => {
|
| 118 |
+
if (sg.type !== "stand" || (sg.code !== west && sg.code !== east)) return;
|
| 119 |
+
let track;
|
| 120 |
+
if (sg.code === west) track = sg.prev && sg.prev.dir === "W" ? lwTrack(sg.prev, true) : sg.next ? lwTrack(sg.next, false) : "8";
|
| 121 |
+
else track = sg.prev && sg.prev.dir === "E" ? gnTrack(sg.prev) : sg.next ? gnTrack(sg.next) : "5a";
|
| 122 |
+
sg.track = track;
|
| 123 |
+
standsAtTerminals.push([sg, i, un]);
|
| 124 |
+
});
|
| 125 |
+
}
|
| 126 |
+
standsAtTerminals.sort((a, b) => a[0].t0 - b[0].t0);
|
| 127 |
+
for (const [sg] of standsAtTerminals) {
|
| 128 |
+
const key = `${sg.code}:${sg.track}`;
|
| 129 |
+
const used = (slotUse[key] ||= []);
|
| 130 |
+
let slot = 0;
|
| 131 |
+
while (used.some(([k, a, b]) => k === slot && a < sg.t1 && sg.t0 < b)) slot++;
|
| 132 |
+
used.push([slot, sg.t0, sg.t1]);
|
| 133 |
+
sg.slot = slot;
|
| 134 |
+
sg.km = kmOf[sg.code] + (sg.code === west ? 1 : -1) * slot * SLOT_KM;
|
| 135 |
+
}
|
| 136 |
+
// moves inherit the terminal tracks of the stands around them
|
| 137 |
+
for (const un of units) {
|
| 138 |
+
un.segs.forEach((sg, i) => {
|
| 139 |
+
if (sg.type !== "move") return;
|
| 140 |
+
const r = sg.run, before = un.segs[i - 1], after = un.segs[i + 1];
|
| 141 |
+
sg.lw = r.dir === "E" ? lwTrack(r, false) : lwTrack(r, true);
|
| 142 |
+
sg.gn = gnTrack(r);
|
| 143 |
+
sg.startKm = null; sg.endKm = null;
|
| 144 |
+
if (before && before.type === "stand" && before.track) {
|
| 145 |
+
if (before.code === west) sg.lw = before.track; else sg.gn = before.track;
|
| 146 |
+
sg.startKm = before.km;
|
| 147 |
+
}
|
| 148 |
+
if (after && after.type === "stand" && after.track) {
|
| 149 |
+
if (after.code === west) sg.lw = after.track; else sg.gn = after.track;
|
| 150 |
+
sg.endKm = after.km;
|
| 151 |
+
}
|
| 152 |
+
});
|
| 153 |
+
for (const sg of un.segs) {
|
| 154 |
+
if (sg.type === "stand" && sg.km === undefined) sg.km = kmOf[sg.code];
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
// two-track stations: each direction's platform, or the other one when it is taken
|
| 159 |
+
const occ = {}; // code -> [{t0, t1, prefer, apply(lane)}]
|
| 160 |
+
const twoTrack = (c) => point[c] && point[c].tracks === 2 && !point[c].directional;
|
| 161 |
+
for (const un of units) {
|
| 162 |
+
un.segs.forEach((sg, i) => {
|
| 163 |
+
if (sg.type === "stand" && twoTrack(sg.code)) {
|
| 164 |
+
const prefer = sg.prev ? sg.prev.dir : sg.next ? sg.next.dir : "E";
|
| 165 |
+
const before = un.segs[i - 1], after = un.segs[i + 1];
|
| 166 |
+
(occ[sg.code] ||= []).push({
|
| 167 |
+
t0: sg.t0, t1: Math.min(sg.t1, sg.t0 + 24 * 60), prefer,
|
| 168 |
+
apply: (lane) => {
|
| 169 |
+
sg.lane = lane;
|
| 170 |
+
if (before && before.type === "move") before.lanes[sg.code] = lane;
|
| 171 |
+
if (after && after.type === "move") after.lanes[sg.code] = lane;
|
| 172 |
+
},
|
| 173 |
+
});
|
| 174 |
+
} else if (sg.type === "move") {
|
| 175 |
+
const r = sg.run;
|
| 176 |
+
const moving = r.started && r.fixed_from !== null && r.fixed_from !== undefined;
|
| 177 |
+
const from = moving ? sg.k - 1 : r.started ? sg.k : sg.s; // the first point it leaves in the window
|
| 178 |
+
for (let j = from + 1; j < sg.e; j++) {
|
| 179 |
+
const c = r.route[j];
|
| 180 |
+
if (!twoTrack(c) || r.arr[j] === null) continue;
|
| 181 |
+
(occ[c] ||= []).push({ t0: r.arr[j], t1: r.dep[j] ?? r.arr[j], prefer: r.dir,
|
| 182 |
+
apply: (lane) => { sg.lanes[c] = lane; } });
|
| 183 |
+
}
|
| 184 |
+
}
|
| 185 |
+
});
|
| 186 |
+
}
|
| 187 |
+
for (const list of Object.values(occ)) {
|
| 188 |
+
list.sort((a, b) => a.t0 - b.t0);
|
| 189 |
+
const taken = { E: [], W: [] };
|
| 190 |
+
for (const o of list) {
|
| 191 |
+
const free = (lane) => !taken[lane].some(([a, b]) => a - PAD < o.t1 && o.t0 < b + PAD);
|
| 192 |
+
const lane = free(o.prefer) ? o.prefer : free(o.prefer === "E" ? "W" : "E") ? (o.prefer === "E" ? "W" : "E")
|
| 193 |
+
: o.prefer;
|
| 194 |
+
taken[lane].push([o.t0, o.t1]);
|
| 195 |
+
o.apply(lane);
|
| 196 |
+
}
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
// knots for every move
|
| 200 |
+
for (const un of units) {
|
| 201 |
+
for (const sg of un.segs) {
|
| 202 |
+
if (sg.type !== "move") continue;
|
| 203 |
+
const r = sg.run, kn = [];
|
| 204 |
+
const push = (t, x, stop) => {
|
| 205 |
+
if (t === null || t === undefined) return;
|
| 206 |
+
const last = kn[kn.length - 1];
|
| 207 |
+
if (last && t <= last.t + 1e-9) {
|
| 208 |
+
if (Math.abs(x - last.x) < 1e-9) { last.stop = last.stop || stop; return; }
|
| 209 |
+
t = last.t + 0.05; // never two places at one instant
|
| 210 |
+
}
|
| 211 |
+
kn.push({ t, x, stop });
|
| 212 |
+
};
|
| 213 |
+
const kmAt = (j) => {
|
| 214 |
+
if (j === sg.e && sg.endKm !== null) return sg.endKm;
|
| 215 |
+
if (j === (r.started ? sg.k : sg.s) && !r.started && sg.startKm !== null) return sg.startKm;
|
| 216 |
+
return kmOf[r.route[j]];
|
| 217 |
+
};
|
| 218 |
+
if (r.started && r.fixed_from !== null && r.fixed_from !== undefined) {
|
| 219 |
+
push(r.fixed_from, kmOf[r.route[sg.k - 1]], !!r.stops[sg.k - 1]);
|
| 220 |
+
}
|
| 221 |
+
const first = r.started ? sg.k : sg.s;
|
| 222 |
+
for (let j = first; j <= sg.e; j++) {
|
| 223 |
+
const x = kmAt(j);
|
| 224 |
+
if (j === first && !(r.started && r.fixed_from !== null && r.fixed_from !== undefined)) {
|
| 225 |
+
if (r.started) push(r.arr[j], x, true);
|
| 226 |
+
push(r.dep[j], x, true);
|
| 227 |
+
} else if (j === sg.e) {
|
| 228 |
+
push(r.arr[j], x, true);
|
| 229 |
+
} else if (r.stopped[j]) {
|
| 230 |
+
push(r.arr[j], x, true);
|
| 231 |
+
push(r.dep[j], x, true);
|
| 232 |
+
} else {
|
| 233 |
+
push(r.arr[j], x, false);
|
| 234 |
+
}
|
| 235 |
+
}
|
| 236 |
+
sg.H = hermite(kn);
|
| 237 |
+
sg.knots = kn;
|
| 238 |
+
sg.t0 = kn[0].t;
|
| 239 |
+
sg.t1 = kn[kn.length - 1].t;
|
| 240 |
+
}
|
| 241 |
+
// keep stands contiguous with the moves around them
|
| 242 |
+
un.segs.forEach((sg, i) => {
|
| 243 |
+
if (sg.type !== "stand") return;
|
| 244 |
+
const before = un.segs[i - 1], after = un.segs[i + 1];
|
| 245 |
+
if (before && before.type === "move") sg.t0 = before.t1;
|
| 246 |
+
if (after && after.type === "move") sg.t1 = after.t0;
|
| 247 |
+
});
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
const tEnd = Math.max(R.end, ...units.flatMap((u) => u.segs.filter((s) => s.type === "move").map((s) => s.t1)));
|
| 251 |
+
return new Sim(R, units, kmOf, point, tEnd);
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
export class Sim {
|
| 255 |
+
constructor(R, units, kmOf, point, tEnd) {
|
| 256 |
+
this.R = R;
|
| 257 |
+
this.units = units;
|
| 258 |
+
this.kmOf = kmOf;
|
| 259 |
+
this.point = point;
|
| 260 |
+
this.t0 = R.start;
|
| 261 |
+
this.t1 = tEnd;
|
| 262 |
+
this.codes = R.points.map((p) => p.code);
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
segAt(un, t) {
|
| 266 |
+
const segs = un.segs;
|
| 267 |
+
if (!segs.length) return null;
|
| 268 |
+
let i = 0;
|
| 269 |
+
while (i < segs.length - 1 && t >= segs[i].t1) i++;
|
| 270 |
+
return segs[i];
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
// where a train set is and what it is doing at minute t
|
| 274 |
+
stateAt(un, t) {
|
| 275 |
+
const sg = this.segAt(un, t);
|
| 276 |
+
if (!sg) return null;
|
| 277 |
+
if (sg.type === "stand") {
|
| 278 |
+
const facing = sg.prev ? sg.prev.dir : sg.next ? sg.next.dir : "E";
|
| 279 |
+
const ref = sg.prev || sg.next;
|
| 280 |
+
return {
|
| 281 |
+
unit: un.id, km: sg.km, dir: facing, moving: false, speed: 0, code: sg.code,
|
| 282 |
+
lw: sg.code === this.codes[0] && sg.track ? sg.track : ref ? lwTrack(ref, facing === "W") : "8",
|
| 283 |
+
gn: sg.code === this.codes[this.codes.length - 1] && sg.track ? sg.track : ref ? gnTrack(ref) : "5a",
|
| 284 |
+
lanes: sg.lane ? { [sg.code]: sg.lane } : null, train: sg.next ? sg.next.train : sg.prev ? sg.prev.train : "",
|
| 285 |
+
kind: ref ? ref.kind : "stop", status: this.standStatus(sg, t), delay: this.standDelay(sg),
|
| 286 |
+
run: sg.next || sg.prev,
|
| 287 |
+
};
|
| 288 |
+
}
|
| 289 |
+
const [km, v] = evalHermite(sg.H, t);
|
| 290 |
+
return {
|
| 291 |
+
unit: un.id, km, dir: sg.dir, moving: Math.abs(v) > 1e-4, speed: Math.abs(v) * 60, code: this.atPoint(sg, t),
|
| 292 |
+
lw: sg.lw, gn: sg.gn, lanes: Object.keys(sg.lanes).length ? sg.lanes : null, train: sg.run.train,
|
| 293 |
+
kind: sg.run.kind, status: this.moveStatus(sg, t, km), delay: this.moveDelay(sg, t), run: sg.run,
|
| 294 |
+
};
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
atPoint(sg, t) {
|
| 298 |
+
const kn = sg.knots;
|
| 299 |
+
for (let i = 0; i < kn.length - 1; i++) {
|
| 300 |
+
if (kn[i].stop && kn[i + 1].stop && Math.abs(kn[i].x - kn[i + 1].x) < 1e-9 && kn[i].t <= t && t <= kn[i + 1].t) {
|
| 301 |
+
return this.codeAtKm(kn[i].x);
|
| 302 |
+
}
|
| 303 |
+
}
|
| 304 |
+
return null;
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
codeAtKm(km) {
|
| 308 |
+
let best = null, d = Infinity;
|
| 309 |
+
for (const [c, k] of Object.entries(this.kmOf)) {
|
| 310 |
+
if (Math.abs(k - km) < d) { d = Math.abs(k - km); best = c; }
|
| 311 |
+
}
|
| 312 |
+
return d < 0.3 ? best : null;
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
moveDelay(sg, t) {
|
| 316 |
+
const r = sg.run;
|
| 317 |
+
let late = 0;
|
| 318 |
+
for (let j = 0; j < r.route.length; j++) {
|
| 319 |
+
const a = r.arr[j] ?? r.dep[j];
|
| 320 |
+
const sch = r.sched_arr[j] ?? r.sched_dep[j];
|
| 321 |
+
if (a === null || a === undefined || sch === null || sch === undefined) continue;
|
| 322 |
+
if (a <= t) late = a - sch;
|
| 323 |
+
}
|
| 324 |
+
return Math.max(0, late);
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
standDelay(sg) {
|
| 328 |
+
const r = sg.next;
|
| 329 |
+
if (!r) return 0;
|
| 330 |
+
const j = r.started ? r.fixed_to : r.first;
|
| 331 |
+
return Math.max(0, (r.dep[j] ?? 0) - (r.sched_dep[j] ?? r.dep[j] ?? 0));
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
moveStatus(sg, t, km) {
|
| 335 |
+
const r = sg.run;
|
| 336 |
+
const code = this.atPoint(sg, t);
|
| 337 |
+
if (code) {
|
| 338 |
+
const j = r.route.indexOf(code);
|
| 339 |
+
const hold = r.order && r.order.hold && r.order.hold[code];
|
| 340 |
+
const dep = r.dep[j];
|
| 341 |
+
if (hold && dep !== null && dep > (r.sched_dep[j] ?? dep)) return `held at ${code} until ${hold} (plan)`;
|
| 342 |
+
if (r.stops[j]) return `at ${code}, leaves ${hhmm(dep)}`;
|
| 343 |
+
return `waiting at ${code}, leaves ${hhmm(dep)}`;
|
| 344 |
+
}
|
| 345 |
+
let j = 0;
|
| 346 |
+
while (j < r.route.length - 1 && (r.arr[j + 1] === null || r.arr[j + 1] <= t)) j++;
|
| 347 |
+
const nextStop = r.route.slice(j + 1, sg.e + 1).find((c, i) => r.stops[j + 1 + i]) || r.route[sg.e];
|
| 348 |
+
const to = r.route[sg.e];
|
| 349 |
+
return `to ${to}${to !== r.route[r.route.length - 1] ? " (turned short)" : ""}, next ${nextStop}`;
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
standStatus(sg, t) {
|
| 353 |
+
if (!sg.next) return `at ${sg.code}: done for this window`;
|
| 354 |
+
const r = sg.next;
|
| 355 |
+
const j = r.started ? r.fixed_to : r.first;
|
| 356 |
+
const hold = r.order && r.order.hold && r.order.hold[sg.code];
|
| 357 |
+
if (hold && r.dep[j] > (r.sched_dep[j] ?? r.dep[j])) return `held at ${sg.code} until ${hold} (plan), then ${r.train}`;
|
| 358 |
+
if (sg.prev && sg.prev.last < sg.prev.route.length - 1 && sg.prev.route[sg.prev.last] === sg.code) {
|
| 359 |
+
return `turned back at ${sg.code}; runs ${r.train} at ${hhmm(r.dep[j])}`;
|
| 360 |
+
}
|
| 361 |
+
return `at ${sg.code}; runs ${r.train} at ${hhmm(r.dep[j])}`;
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
// every train set's state at t (those with a window)
|
| 365 |
+
statesAt(t) {
|
| 366 |
+
return this.units.map((u) => this.stateAt(u, t)).filter(Boolean);
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
// cancelled runs (shown in the list, never on the map)
|
| 370 |
+
cancelled() {
|
| 371 |
+
return this.R.runs.filter((r) => r.cancelled);
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
// which sections hold a train between their end points at t: index -> [unit ids]
|
| 375 |
+
occupancy(states) {
|
| 376 |
+
const occ = {};
|
| 377 |
+
const pts = this.R.points;
|
| 378 |
+
for (const s of states) {
|
| 379 |
+
for (let i = 0; i < pts.length - 1; i++) {
|
| 380 |
+
if (s.km > pts[i].km + 0.03 && s.km < pts[i + 1].km - 0.03) {
|
| 381 |
+
(occ[i] ||= []).push(s.unit);
|
| 382 |
+
break;
|
| 383 |
+
}
|
| 384 |
+
}
|
| 385 |
+
}
|
| 386 |
+
return occ;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
// the cost accrued by minute t (the replay's cost events, which add up to the grader's cost)
|
| 390 |
+
costAt(t) {
|
| 391 |
+
let sum = 0;
|
| 392 |
+
for (const c of this.R.costs) {
|
| 393 |
+
if (c[0] > t) break;
|
| 394 |
+
sum += c[1];
|
| 395 |
+
}
|
| 396 |
+
return sum;
|
| 397 |
+
}
|
| 398 |
+
}
|
openenv/rail_openenv/web/twin/scene.js
ADDED
|
@@ -0,0 +1,765 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// The live part of the line twin: sky and sun for the task's date and time, the trains, the signals that guard each
|
| 2 |
+
// single-track section, the level crossings, the disruptions, labels, and the cameras. The scenery is world.js.
|
| 3 |
+
//
|
| 4 |
+
// It only draws: positions come from motion.js (the simulated times), places from geo.js (the OSM track).
|
| 5 |
+
|
| 6 |
+
import * as THREE from "three";
|
| 7 |
+
import { MapControls } from "three/addons/controls/MapControls.js";
|
| 8 |
+
import { Line2 } from "three/addons/lines/Line2.js";
|
| 9 |
+
import { LineGeometry } from "three/addons/lines/LineGeometry.js";
|
| 10 |
+
import { LineMaterial } from "three/addons/lines/LineMaterial.js";
|
| 11 |
+
import { CSS2DObject, CSS2DRenderer } from "three/addons/renderers/CSS2DRenderer.js";
|
| 12 |
+
|
| 13 |
+
import { CANONICAL } from "./geo.js";
|
| 14 |
+
import { animateWorld, buildWorld } from "./world.js";
|
| 15 |
+
|
| 16 |
+
// series colours, validated for the dark interface (dataviz reference palette, dark slots 1 and 2)
|
| 17 |
+
export const KIND_COLOR = { stop: "#3987e5", fast: "#d95926" };
|
| 18 |
+
export const STATUS = { critical: "#d03b3b", warning: "#fab219", serious: "#ec835a", good: "#0ca30c" };
|
| 19 |
+
const CAR = 26.5, GAP = 1.0, BOGIE = 9.5;
|
| 20 |
+
const LAT = 53.22, LON = 6.19;
|
| 21 |
+
|
| 22 |
+
// --- sun ---------------------------------------------------------------------------------------------------------
|
| 23 |
+
|
| 24 |
+
function amsterdamOffset(day) {
|
| 25 |
+
// CET (UTC+1), CEST (UTC+2) from the last Sunday of March to the last Sunday of October
|
| 26 |
+
const [y, m, d] = day.split("-").map(Number);
|
| 27 |
+
const lastSunday = (month) => { const t = new Date(Date.UTC(y, month, 0)); return t.getUTCDate() - t.getUTCDay(); };
|
| 28 |
+
const date = Date.UTC(y, m - 1, d);
|
| 29 |
+
const start = Date.UTC(y, 2, lastSunday(3)), end = Date.UTC(y, 9, lastSunday(10));
|
| 30 |
+
return date >= start && date < end ? 2 : 1;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
export function sunPosition(day, minute) {
|
| 34 |
+
// NOAA's simplified solar position: elevation and azimuth (from north, clockwise), degrees
|
| 35 |
+
const [y, m, d] = day.split("-").map(Number);
|
| 36 |
+
const n = Math.round((Date.UTC(y, m - 1, d) - Date.UTC(y, 0, 0)) / 864e5);
|
| 37 |
+
const utcMin = minute - amsterdamOffset(day) * 60;
|
| 38 |
+
const g = (2 * Math.PI / 365) * (n - 1 + (utcMin / 60 - 12) / 24);
|
| 39 |
+
const eq = 229.18 * (0.000075 + 0.001868 * Math.cos(g) - 0.032077 * Math.sin(g) - 0.014615 * Math.cos(2 * g)
|
| 40 |
+
- 0.040849 * Math.sin(2 * g));
|
| 41 |
+
const decl = 0.006918 - 0.399912 * Math.cos(g) + 0.070257 * Math.sin(g) - 0.006758 * Math.cos(2 * g)
|
| 42 |
+
+ 0.000907 * Math.sin(2 * g) - 0.002697 * Math.cos(3 * g) + 0.00148 * Math.sin(3 * g);
|
| 43 |
+
const tst = utcMin + eq + 4 * LON;
|
| 44 |
+
const ha = (tst / 4 - 180) * Math.PI / 180;
|
| 45 |
+
const lat = LAT * Math.PI / 180;
|
| 46 |
+
const cosZ = Math.sin(lat) * Math.sin(decl) + Math.cos(lat) * Math.cos(decl) * Math.cos(ha);
|
| 47 |
+
const zen = Math.acos(Math.max(-1, Math.min(1, cosZ)));
|
| 48 |
+
let az = Math.atan2(Math.sin(ha), Math.cos(ha) * Math.sin(lat) - Math.tan(decl) * Math.cos(lat)) + Math.PI;
|
| 49 |
+
az = (az + 2 * Math.PI) % (2 * Math.PI);
|
| 50 |
+
return { elevation: 90 - zen * 180 / Math.PI, azimuth: az * 180 / Math.PI };
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
// --- trains --------------------------------------------------------------------------------------------------
|
| 54 |
+
|
| 55 |
+
function carGeometries() {
|
| 56 |
+
const s = new THREE.Shape();
|
| 57 |
+
s.moveTo(-1.42, 1.15); s.lineTo(1.42, 1.15); s.lineTo(1.42, 3.25);
|
| 58 |
+
s.quadraticCurveTo(1.4, 3.92, 0.85, 3.98); s.lineTo(-0.85, 3.98); s.quadraticCurveTo(-1.4, 3.92, -1.42, 3.25);
|
| 59 |
+
s.closePath();
|
| 60 |
+
const body = new THREE.ExtrudeGeometry(s, { depth: CAR - 2.2, bevelEnabled: false });
|
| 61 |
+
body.rotateY(Math.PI / 2);
|
| 62 |
+
body.translate(-(CAR - 2.2) / 2 - 1.1, 0, 0); // spans -CAR/2 .. CAR/2 - 2.2 (the nose adds 2.2)
|
| 63 |
+
const side = new THREE.Shape();
|
| 64 |
+
side.moveTo(0, 1.15); side.lineTo(0, 3.98); side.lineTo(0.7, 3.92); side.lineTo(1.75, 2.35); side.lineTo(2.2, 1.6);
|
| 65 |
+
side.lineTo(2.2, 1.15); side.closePath();
|
| 66 |
+
const nose = new THREE.ExtrudeGeometry(side, { depth: 2.84, bevelEnabled: false });
|
| 67 |
+
nose.translate(CAR / 2 - 2.2, 0, -1.42);
|
| 68 |
+
const screen = new THREE.PlaneGeometry(1.95, 2.4);
|
| 69 |
+
screen.rotateY(Math.PI / 2);
|
| 70 |
+
screen.rotateZ(-0.98);
|
| 71 |
+
screen.translate(CAR / 2 - 2.2 + 1.24, 3.12, 0);
|
| 72 |
+
const band = new THREE.BoxGeometry(CAR - 4.4, 0.8, 2.88);
|
| 73 |
+
band.translate(-1.1, 2.6, 0);
|
| 74 |
+
const stripe = new THREE.BoxGeometry(CAR - 2.6, 0.26, 2.87);
|
| 75 |
+
stripe.translate(-1.0, 1.62, 0);
|
| 76 |
+
const bogie = new THREE.BoxGeometry(2.7, 0.75, 2.2);
|
| 77 |
+
const lamp = new THREE.CircleGeometry(0.16, 10);
|
| 78 |
+
lamp.rotateY(Math.PI / 2);
|
| 79 |
+
return { body, nose, screen, band, stripe, bogie, lamp };
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
class TrainMesh {
|
| 83 |
+
constructor(shared, kind) {
|
| 84 |
+
const { geo, mats } = shared;
|
| 85 |
+
this.group = new THREE.Group();
|
| 86 |
+
this.cars = [];
|
| 87 |
+
for (let c = 0; c < 2; c++) {
|
| 88 |
+
const car = new THREE.Group();
|
| 89 |
+
const inner = new THREE.Group(); // car 1 is car 0 turned round: both ends are cabs
|
| 90 |
+
if (c === 1) inner.rotation.y = Math.PI;
|
| 91 |
+
for (const [g, m] of [[geo.body, mats.body], [geo.nose, mats.body], [geo.screen, mats.glass],
|
| 92 |
+
[geo.band, mats.glass], [geo.stripe, mats.stripe[kind]]]) {
|
| 93 |
+
const mesh = new THREE.Mesh(g, m);
|
| 94 |
+
mesh.castShadow = true;
|
| 95 |
+
inner.add(mesh);
|
| 96 |
+
}
|
| 97 |
+
for (const x of [-BOGIE, BOGIE]) {
|
| 98 |
+
const b = new THREE.Mesh(geo.bogie, mats.bogie);
|
| 99 |
+
b.position.set(x, 0.62, 0);
|
| 100 |
+
inner.add(b);
|
| 101 |
+
}
|
| 102 |
+
const lamps = [];
|
| 103 |
+
for (const z of [-0.95, 0.95]) {
|
| 104 |
+
const l = new THREE.Mesh(geo.lamp, mats.head);
|
| 105 |
+
l.position.set(CAR / 2 + 0.01, 1.55, z);
|
| 106 |
+
inner.add(l);
|
| 107 |
+
lamps.push(l);
|
| 108 |
+
}
|
| 109 |
+
car.add(inner);
|
| 110 |
+
car.userData.lamps = lamps;
|
| 111 |
+
this.group.add(car);
|
| 112 |
+
this.cars.push(car);
|
| 113 |
+
}
|
| 114 |
+
this.halo = new THREE.Mesh(new THREE.RingGeometry(31, 32.5, 64), mats.halo);
|
| 115 |
+
this.halo.rotation.x = -Math.PI / 2;
|
| 116 |
+
this.halo.position.y = 0.4;
|
| 117 |
+
this.halo.visible = false;
|
| 118 |
+
this.group.add(this.halo);
|
| 119 |
+
this.glow = new THREE.Sprite(mats.glow);
|
| 120 |
+
this.glow.scale.set(24, 24, 1);
|
| 121 |
+
this.group.add(this.glow);
|
| 122 |
+
const el = document.createElement("div");
|
| 123 |
+
el.className = "tlabel";
|
| 124 |
+
this.label = new CSS2DObject(el);
|
| 125 |
+
this.label.position.set(0, 9, 0);
|
| 126 |
+
this.group.add(this.label);
|
| 127 |
+
this.kind = kind;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
setKind(shared, kind) {
|
| 131 |
+
if (kind === this.kind) return;
|
| 132 |
+
this.kind = kind;
|
| 133 |
+
for (const car of this.cars) car.children[0].children[4].material = shared.mats.stripe[kind];
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
// --- the scene -----------------------------------------------------------------------------------------------
|
| 138 |
+
|
| 139 |
+
export class TwinScene {
|
| 140 |
+
constructor(el, twin, line) {
|
| 141 |
+
this.el = el;
|
| 142 |
+
this.twin = twin;
|
| 143 |
+
this.line = line;
|
| 144 |
+
this.renderer = new THREE.WebGLRenderer({ antialias: true, logarithmicDepthBuffer: true,
|
| 145 |
+
preserveDrawingBuffer: true });
|
| 146 |
+
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
| 147 |
+
this.renderer.shadowMap.enabled = true;
|
| 148 |
+
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
| 149 |
+
this.renderer.toneMapping = THREE.NeutralToneMapping;
|
| 150 |
+
this.renderer.toneMappingExposure = 1.0;
|
| 151 |
+
el.appendChild(this.renderer.domElement);
|
| 152 |
+
this.labels = new CSS2DRenderer();
|
| 153 |
+
this.labels.domElement.className = "labels";
|
| 154 |
+
el.appendChild(this.labels.domElement);
|
| 155 |
+
|
| 156 |
+
this.scene = new THREE.Scene();
|
| 157 |
+
this.camera = new THREE.PerspectiveCamera(45, 1, 0.5, 300000);
|
| 158 |
+
this.controls = new MapControls(this.camera, this.labels.domElement);
|
| 159 |
+
this.controls.enableDamping = true;
|
| 160 |
+
this.controls.dampingFactor = 0.12;
|
| 161 |
+
this.controls.maxPolarAngle = Math.PI * 0.47;
|
| 162 |
+
this.controls.minDistance = 12;
|
| 163 |
+
this.controls.maxDistance = 140000;
|
| 164 |
+
this.controls.zoomToCursor = true;
|
| 165 |
+
|
| 166 |
+
this.hemi = new THREE.HemisphereLight(0xeef3ff, 0x7c7a62, 2.0);
|
| 167 |
+
this.ambient = new THREE.AmbientLight(0xffffff, 0.25);
|
| 168 |
+
this.sun = new THREE.DirectionalLight(0xffffff, 3.0);
|
| 169 |
+
this.sun.castShadow = true;
|
| 170 |
+
this.sun.shadow.mapSize.set(2048, 2048);
|
| 171 |
+
const sc = this.sun.shadow.camera;
|
| 172 |
+
sc.left = sc.bottom = -350; sc.right = sc.top = 350; sc.near = 10; sc.far = 3000;
|
| 173 |
+
this.sun.shadow.bias = -0.0004;
|
| 174 |
+
this.scene.add(this.hemi, this.ambient, this.sun, this.sun.target);
|
| 175 |
+
this.scene.fog = new THREE.Fog(0xbcd3e6, 4000, 60000);
|
| 176 |
+
this.scene.background = new THREE.Color(0xbcd3e6);
|
| 177 |
+
|
| 178 |
+
this.world = buildWorld(twin, this.scene);
|
| 179 |
+
this.shared = this.sharedAssets();
|
| 180 |
+
this.trains = new Map();
|
| 181 |
+
this.signals = [];
|
| 182 |
+
this.crossings = this.buildCrossings();
|
| 183 |
+
this.closures = [];
|
| 184 |
+
this.stationLabels = this.buildStationLabels();
|
| 185 |
+
this.placeLabels = this.buildPlaceLabels();
|
| 186 |
+
this.followId = null;
|
| 187 |
+
this.mode = "map";
|
| 188 |
+
this.lastFollow = null;
|
| 189 |
+
this.selected = null;
|
| 190 |
+
this.night = 0;
|
| 191 |
+
this.resize();
|
| 192 |
+
window.addEventListener("resize", () => this.resize());
|
| 193 |
+
this.viewAll();
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
sharedAssets() {
|
| 197 |
+
const geo = carGeometries();
|
| 198 |
+
const glowTex = (() => {
|
| 199 |
+
const cv = document.createElement("canvas");
|
| 200 |
+
cv.width = cv.height = 64;
|
| 201 |
+
const g = cv.getContext("2d");
|
| 202 |
+
const grd = g.createRadialGradient(32, 32, 0, 32, 32, 32);
|
| 203 |
+
grd.addColorStop(0, "rgba(255,244,214,1)"); grd.addColorStop(0.25, "rgba(255,230,170,0.5)");
|
| 204 |
+
grd.addColorStop(1, "rgba(255,220,150,0)");
|
| 205 |
+
g.fillStyle = grd; g.fillRect(0, 0, 64, 64);
|
| 206 |
+
return new THREE.CanvasTexture(cv);
|
| 207 |
+
})();
|
| 208 |
+
const mats = {
|
| 209 |
+
body: new THREE.MeshStandardMaterial({ color: 0xeef0f2, roughness: 0.45, metalness: 0.15 }),
|
| 210 |
+
glass: new THREE.MeshStandardMaterial({ color: 0x18222c, roughness: 0.15, metalness: 0.6 }),
|
| 211 |
+
bogie: new THREE.MeshLambertMaterial({ color: 0x2a2a2a }),
|
| 212 |
+
stripe: { stop: new THREE.MeshStandardMaterial({ color: KIND_COLOR.stop, roughness: 0.5 }),
|
| 213 |
+
fast: new THREE.MeshStandardMaterial({ color: KIND_COLOR.fast, roughness: 0.5 }) },
|
| 214 |
+
head: new THREE.MeshBasicMaterial({ color: 0xfff6dc }),
|
| 215 |
+
tail: new THREE.MeshBasicMaterial({ color: 0xff2a1a }),
|
| 216 |
+
halo: new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.85, depthWrite: false }),
|
| 217 |
+
glow: new THREE.SpriteMaterial({ map: glowTex, color: 0xffffff, transparent: true, opacity: 0,
|
| 218 |
+
depthWrite: false, blending: THREE.AdditiveBlending }),
|
| 219 |
+
};
|
| 220 |
+
return { geo, mats };
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
resize() {
|
| 224 |
+
const w = this.el.clientWidth || window.innerWidth, h = this.el.clientHeight || window.innerHeight;
|
| 225 |
+
this.renderer.setSize(w, h);
|
| 226 |
+
this.labels.setSize(w, h);
|
| 227 |
+
for (const m of [...(this.lineMats || []), ...(this.closureMats || [])]) m.resolution.set(w, h);
|
| 228 |
+
this.camera.aspect = w / h;
|
| 229 |
+
this.camera.updateProjectionMatrix();
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
// --- static labels -----------------------------------------------------------------------------------------
|
| 233 |
+
|
| 234 |
+
buildStationLabels() {
|
| 235 |
+
const out = [];
|
| 236 |
+
for (const st of this.twin.stations) {
|
| 237 |
+
const el = document.createElement("div");
|
| 238 |
+
el.className = "slabel";
|
| 239 |
+
el.textContent = st.name;
|
| 240 |
+
const o = new CSS2DObject(el);
|
| 241 |
+
o.position.set(st.x, 14, -st.y);
|
| 242 |
+
this.scene.add(o);
|
| 243 |
+
out.push({ code: st.code, obj: o, x: st.x, y: st.y });
|
| 244 |
+
}
|
| 245 |
+
return out;
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
buildPlaceLabels() {
|
| 249 |
+
const out = [];
|
| 250 |
+
const stations = new Set(this.twin.stations.map((s) => s.name.toLowerCase()));
|
| 251 |
+
for (const p of this.twin.places) {
|
| 252 |
+
if (p.r > 3) continue;
|
| 253 |
+
const el = document.createElement("div");
|
| 254 |
+
el.className = `plabel r${p.r}`;
|
| 255 |
+
el.textContent = p.n;
|
| 256 |
+
const o = new CSS2DObject(el);
|
| 257 |
+
o.position.set(p.x, 30, -p.y);
|
| 258 |
+
this.scene.add(o);
|
| 259 |
+
out.push({ obj: o, r: p.r, x: p.x, y: p.y, dup: stations.has(p.n.toLowerCase()) });
|
| 260 |
+
}
|
| 261 |
+
return out;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
// --- crossings, signals, closures ------------------------------------------------------------------------
|
| 265 |
+
|
| 266 |
+
buildCrossings() {
|
| 267 |
+
const out = [];
|
| 268 |
+
const cv = document.createElement("canvas");
|
| 269 |
+
cv.width = 64; cv.height = 8;
|
| 270 |
+
const g = cv.getContext("2d");
|
| 271 |
+
for (let i = 0; i < 8; i++) { g.fillStyle = i % 2 ? "#ffffff" : "#d42a2a"; g.fillRect(i * 8, 0, 8, 8); }
|
| 272 |
+
const stripes = new THREE.CanvasTexture(cv);
|
| 273 |
+
const armMat = new THREE.MeshLambertMaterial({ map: stripes });
|
| 274 |
+
const postMat = new THREE.MeshLambertMaterial({ color: 0xdddddd });
|
| 275 |
+
const plateMat = new THREE.MeshLambertMaterial({ color: 0x3e3f42, depthTest: false, depthWrite: false });
|
| 276 |
+
const offMat = new THREE.MeshBasicMaterial({ color: 0x3a1010 });
|
| 277 |
+
this.lampOn = new THREE.MeshBasicMaterial({ color: 0xff3020 });
|
| 278 |
+
const armGeo = new THREE.BoxGeometry(5.6, 0.14, 0.14);
|
| 279 |
+
armGeo.translate(2.8, 0, 0);
|
| 280 |
+
const postGeo = new THREE.CylinderGeometry(0.08, 0.1, 1.3, 6);
|
| 281 |
+
const lampGeo = new THREE.CircleGeometry(0.13, 10);
|
| 282 |
+
for (const c of this.twin.crossings) {
|
| 283 |
+
const grp = new THREE.Group();
|
| 284 |
+
grp.position.set(c.x, 0, -c.y);
|
| 285 |
+
const half = c.n * 2.4 + 2.2;
|
| 286 |
+
const plate = new THREE.Mesh(new THREE.BoxGeometry(half * 2, 0.05, 5.5), plateMat);
|
| 287 |
+
plate.rotation.y = c.r;
|
| 288 |
+
plate.position.y = 0.2;
|
| 289 |
+
plate.renderOrder = -4; // painted with the flat layers (see world.js)
|
| 290 |
+
grp.add(plate);
|
| 291 |
+
const arms = [], lamps = [];
|
| 292 |
+
for (const side of [1, -1]) {
|
| 293 |
+
const ux = Math.cos(c.r) * side, uy = Math.sin(c.r) * side; // along the road, away from the track
|
| 294 |
+
const vx = -uy, vy = ux; // across the road
|
| 295 |
+
const bx = ux * (half + 1.2) + vx * 3.1, by = uy * (half + 1.2) + vy * 3.1;
|
| 296 |
+
const post = new THREE.Mesh(postGeo, postMat);
|
| 297 |
+
post.position.set(bx, 0.65, -by);
|
| 298 |
+
const pivot = new THREE.Group();
|
| 299 |
+
pivot.position.set(bx, 1.05, -by);
|
| 300 |
+
pivot.rotation.y = Math.atan2(-vy, -vx); // arm points back across the road
|
| 301 |
+
const tilt = new THREE.Group();
|
| 302 |
+
tilt.rotation.z = Math.PI / 2; // open: arm up
|
| 303 |
+
const arm = new THREE.Mesh(armGeo, armMat);
|
| 304 |
+
tilt.add(arm);
|
| 305 |
+
pivot.add(tilt);
|
| 306 |
+
for (const dz of [-0.2, 0.2]) {
|
| 307 |
+
const l = new THREE.Mesh(lampGeo, offMat);
|
| 308 |
+
l.position.set(bx + ux * 0.01, 1.25, -by + dz);
|
| 309 |
+
l.rotation.y = Math.atan2(-uy, ux) + Math.PI / 2;
|
| 310 |
+
grp.add(l);
|
| 311 |
+
lamps.push(l);
|
| 312 |
+
}
|
| 313 |
+
grp.add(post, pivot);
|
| 314 |
+
arms.push(tilt);
|
| 315 |
+
}
|
| 316 |
+
this.scene.add(grp);
|
| 317 |
+
out.push({ ...c, arms, lamps, offMat, angle: Math.PI / 2 });
|
| 318 |
+
}
|
| 319 |
+
return out;
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
clearReplayObjects() {
|
| 323 |
+
for (const s of this.signals) this.scene.remove(s.group);
|
| 324 |
+
for (const c of this.closures) { this.scene.remove(c.mesh); this.scene.remove(c.label); if (c.line) this.scene.remove(c.line); }
|
| 325 |
+
this.closureMats = [];
|
| 326 |
+
for (const v of this.breaks || []) { this.scene.remove(v.mesh); this.scene.remove(v.label); }
|
| 327 |
+
this.signals = [];
|
| 328 |
+
this.closures = [];
|
| 329 |
+
this.breaks = [];
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
buildBreaks(R) {
|
| 333 |
+
// where and when the plan breaks a rule: a red ring on the spot for a few minutes around the moment
|
| 334 |
+
const km = Object.fromEntries(R.points.map((p) => [p.code, p.km]));
|
| 335 |
+
const ringGeo = new THREE.RingGeometry(40, 48, 48);
|
| 336 |
+
ringGeo.rotateX(-Math.PI / 2);
|
| 337 |
+
const mat = new THREE.MeshBasicMaterial({ color: STATUS.critical, transparent: true, opacity: 0.9, depthTest: false,
|
| 338 |
+
depthWrite: false });
|
| 339 |
+
for (const v of R.violations) {
|
| 340 |
+
if (!v.time || !v.where) continue;
|
| 341 |
+
const [h, m] = v.time.split(":").map(Number);
|
| 342 |
+
let t = h * 60 + m;
|
| 343 |
+
if (t < R.start - 120) t += 1440;
|
| 344 |
+
const ends = v.where.split("-").filter((c) => km[c] !== undefined);
|
| 345 |
+
if (!ends.length) continue;
|
| 346 |
+
const k = ends.reduce((a, c) => a + km[c], 0) / ends.length;
|
| 347 |
+
const p = this.pose("E", k);
|
| 348 |
+
const mesh = new THREE.Mesh(ringGeo, mat);
|
| 349 |
+
mesh.position.set(p.x, 1.5, -p.y);
|
| 350 |
+
mesh.renderOrder = 6;
|
| 351 |
+
const el = document.createElement("div");
|
| 352 |
+
el.className = "vlabel";
|
| 353 |
+
el.innerHTML = `<b>${v.rule.replace("_", " ")}</b> ${v.trains.join(" · ")} at ${v.where}`;
|
| 354 |
+
const label = new CSS2DObject(el);
|
| 355 |
+
label.position.set(p.x, 30, -p.y);
|
| 356 |
+
this.scene.add(mesh, label);
|
| 357 |
+
this.breaks.push({ t, mesh, label });
|
| 358 |
+
}
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
pose(dir, km, lw = "8", gn = "5a", lanes = null) {
|
| 362 |
+
const p = this.line.place(dir, lw, gn, km, lanes);
|
| 363 |
+
const q = this.line.place(dir, lw, gn, km + (dir === "E" ? 0.01 : -0.01), lanes);
|
| 364 |
+
return { x: p[0], y: p[1], h: Math.atan2(q[1] - p[1], q[0] - p[0]) };
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
buildSignals(R) {
|
| 368 |
+
// one signal at each entry of every single-track section, on the right of the track; red while a train is
|
| 369 |
+
// inside or the section is closed
|
| 370 |
+
const km = Object.fromEntries(R.points.map((p) => [p.code, p.km]));
|
| 371 |
+
const kind = Object.fromEntries(R.points.map((p) => [p.code, p.kind]));
|
| 372 |
+
const mastGeo = new THREE.CylinderGeometry(0.09, 0.11, 4.2, 6);
|
| 373 |
+
const headGeo = new THREE.BoxGeometry(0.35, 0.95, 0.5);
|
| 374 |
+
const lampGeo = new THREE.CircleGeometry(0.12, 12);
|
| 375 |
+
const grey = new THREE.MeshLambertMaterial({ color: 0x2b2b2b });
|
| 376 |
+
this.sigRed = new THREE.MeshBasicMaterial({ color: 0xff2a1a });
|
| 377 |
+
this.sigGreen = new THREE.MeshBasicMaterial({ color: 0x28e070 });
|
| 378 |
+
this.sigOff = new THREE.MeshBasicMaterial({ color: 0x1a1a1a });
|
| 379 |
+
R.sections.forEach((sec, i) => {
|
| 380 |
+
if (!sec.single) return;
|
| 381 |
+
for (const dir of ["E", "W"]) {
|
| 382 |
+
const at = dir === "E" ? sec.a : sec.b;
|
| 383 |
+
const sign = dir === "E" ? 1 : -1;
|
| 384 |
+
const k = kind[at] === "junction" ? km[at] - sign * 0.18 : km[at] + sign * 0.12;
|
| 385 |
+
const p = this.pose(dir, k);
|
| 386 |
+
const rx = Math.sin(p.h), ry = -Math.cos(p.h); // right of the direction of travel
|
| 387 |
+
const group = new THREE.Group();
|
| 388 |
+
group.position.set(p.x + rx * 2.6, 0, -(p.y + ry * 2.6));
|
| 389 |
+
group.rotation.y = p.h + Math.PI; // the lamps face oncoming trains
|
| 390 |
+
const mast = new THREE.Mesh(mastGeo, grey);
|
| 391 |
+
mast.position.y = 2.1;
|
| 392 |
+
const head = new THREE.Mesh(headGeo, grey);
|
| 393 |
+
head.position.y = 4.3;
|
| 394 |
+
const red = new THREE.Mesh(lampGeo, this.sigOff), green = new THREE.Mesh(lampGeo, this.sigOff);
|
| 395 |
+
red.rotation.y = green.rotation.y = Math.PI / 2;
|
| 396 |
+
red.position.set(0.18, 4.52, 0); green.position.set(0.18, 4.12, 0);
|
| 397 |
+
group.add(mast, head, red, green);
|
| 398 |
+
this.scene.add(group);
|
| 399 |
+
this.signals.push({ section: i, dir, group, red, green });
|
| 400 |
+
}
|
| 401 |
+
});
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
buildClosures(R) {
|
| 405 |
+
const km = Object.fromEntries(R.points.map((p) => [p.code, p.km]));
|
| 406 |
+
const single = R.sections.map((s) => s.single);
|
| 407 |
+
const idx = Object.fromEntries(R.points.map((p, i) => [p.code, i]));
|
| 408 |
+
for (const c of R.closures) {
|
| 409 |
+
let k0, k1;
|
| 410 |
+
if (c.kind === "track") { k0 = km[c.where[0]] - 0.2; k1 = km[c.where[0]] + 0.2; }
|
| 411 |
+
else { k0 = Math.min(km[c.where[0]], km[c.where[c.where.length - 1]]); k1 = Math.max(km[c.where[0]], km[c.where[c.where.length - 1]]); }
|
| 412 |
+
const pos = [], index = [];
|
| 413 |
+
const lanes = ["E"];
|
| 414 |
+
const i0 = Math.min(idx[c.where[0]], idx[c.where[c.where.length - 1]]);
|
| 415 |
+
const i1 = Math.max(idx[c.where[0]], idx[c.where[c.where.length - 1]]);
|
| 416 |
+
if (c.kind === "track" || single.slice(i0, Math.max(i1, i0 + 1)).some((s) => !s)) lanes.push("W");
|
| 417 |
+
for (const lane of lanes) {
|
| 418 |
+
const base = pos.length / 3;
|
| 419 |
+
let n = 0;
|
| 420 |
+
for (let k = k0; k <= k1 + 1e-9; k += Math.max(0.005, (k1 - k0) / 400)) {
|
| 421 |
+
const p = this.pose(lane, k, "8", "5a");
|
| 422 |
+
const nx = -Math.sin(p.h) * 2.6, ny = Math.cos(p.h) * 2.6;
|
| 423 |
+
pos.push(p.x + nx, 0.45, -(p.y + ny), p.x - nx, 0.45, -(p.y - ny));
|
| 424 |
+
n++;
|
| 425 |
+
}
|
| 426 |
+
for (let j = 0; j < n - 1; j++) {
|
| 427 |
+
const v = base + 2 * j;
|
| 428 |
+
index.push(v, v + 2, v + 1, v + 1, v + 2, v + 3);
|
| 429 |
+
}
|
| 430 |
+
}
|
| 431 |
+
const g = new THREE.BufferGeometry();
|
| 432 |
+
g.setAttribute("position", new THREE.Float32BufferAttribute(pos, 3));
|
| 433 |
+
g.setIndex(index);
|
| 434 |
+
const color = c.kind === "closed" ? STATUS.critical : c.kind === "slow" ? STATUS.warning : STATUS.serious;
|
| 435 |
+
const mat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.7, depthWrite: false,
|
| 436 |
+
side: THREE.DoubleSide });
|
| 437 |
+
const mesh = new THREE.Mesh(g, mat);
|
| 438 |
+
mesh.renderOrder = 5;
|
| 439 |
+
// from high up: a thick line over the overview line while it lasts
|
| 440 |
+
const lpts = [];
|
| 441 |
+
for (let k = k0; k <= k1 + 1e-9; k += Math.max(0.02, (k1 - k0) / 100)) {
|
| 442 |
+
const q = this.pose("E", Math.min(k, k1));
|
| 443 |
+
lpts.push(q.x, 1.2, -q.y);
|
| 444 |
+
}
|
| 445 |
+
const lmat = new LineMaterial({ color, linewidth: 7, depthTest: false, depthWrite: false, transparent: true });
|
| 446 |
+
lmat.resolution.set(this.el.clientWidth || 1, this.el.clientHeight || 1);
|
| 447 |
+
(this.closureMats ||= []).push(lmat);
|
| 448 |
+
const lg = new LineGeometry();
|
| 449 |
+
lg.setPositions(lpts);
|
| 450 |
+
const line = new Line2(lg, lmat);
|
| 451 |
+
line.renderOrder = -1;
|
| 452 |
+
const el = document.createElement("div");
|
| 453 |
+
el.className = `clabel ${c.kind}`;
|
| 454 |
+
const what = c.kind === "closed" ? "Closed" : c.kind === "slow" ? `Slow +${c.extra} min` : "1 track out of use";
|
| 455 |
+
el.innerHTML = `<b>${what}</b> ${c.cause || ""}`;
|
| 456 |
+
const label = new CSS2DObject(el);
|
| 457 |
+
const mid = this.pose("E", (k0 + k1) / 2);
|
| 458 |
+
label.position.set(mid.x, 22, -mid.y);
|
| 459 |
+
this.scene.add(mesh, label, line);
|
| 460 |
+
this.closures.push({ ...c, mesh, label, mat, line });
|
| 461 |
+
}
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
buildOverviewLine(R) {
|
| 465 |
+
// the line as seen from high up, where 3 m of track is under a pixel: white where it is single track (one train
|
| 466 |
+
// at a time between stations), grey where it is double; a dark casing under both
|
| 467 |
+
const km = Object.fromEntries(R.points.map((p) => [p.code, p.km]));
|
| 468 |
+
this.lineMats = [];
|
| 469 |
+
this.overview = new THREE.Group();
|
| 470 |
+
const mk = (color, width, order) => {
|
| 471 |
+
const m = new LineMaterial({ color, linewidth: width, depthTest: false, depthWrite: false, transparent: true });
|
| 472 |
+
m.resolution.set(this.el.clientWidth || 1, this.el.clientHeight || 1);
|
| 473 |
+
this.lineMats.push(m);
|
| 474 |
+
m.userData.order = order;
|
| 475 |
+
return m;
|
| 476 |
+
};
|
| 477 |
+
const casing = mk(0x1a1a19, 7, -3), single = mk(0xffffff, 3, -2), double = mk(0x9a9a96, 3, -2);
|
| 478 |
+
for (const sec of R.sections) {
|
| 479 |
+
const pts = [];
|
| 480 |
+
for (let k = km[sec.a]; k <= km[sec.b] + 1e-9; k += Math.max(0.02, (km[sec.b] - km[sec.a]) / 120)) {
|
| 481 |
+
const p = this.line.place("E", "8", "5a", Math.min(k, km[sec.b]));
|
| 482 |
+
pts.push(p[0], 1, -p[1]);
|
| 483 |
+
}
|
| 484 |
+
for (const mat of [casing, sec.single ? single : double]) {
|
| 485 |
+
const g = new LineGeometry();
|
| 486 |
+
g.setPositions(pts);
|
| 487 |
+
const l = new Line2(g, mat);
|
| 488 |
+
l.computeLineDistances();
|
| 489 |
+
l.renderOrder = mat.userData.order;
|
| 490 |
+
this.overview.add(l);
|
| 491 |
+
}
|
| 492 |
+
}
|
| 493 |
+
this.scene.add(this.overview);
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
setReplay(R, sim) {
|
| 497 |
+
this.R = R;
|
| 498 |
+
this.sim = sim;
|
| 499 |
+
if (!this.overview) this.buildOverviewLine(R);
|
| 500 |
+
this.clearReplayObjects();
|
| 501 |
+
this.buildSignals(R);
|
| 502 |
+
this.buildClosures(R);
|
| 503 |
+
this.buildBreaks(R);
|
| 504 |
+
const keep = new Set(sim.units.map((u) => u.id));
|
| 505 |
+
for (const [id, tm] of this.trains) {
|
| 506 |
+
if (!keep.has(id)) { this.scene.remove(tm.group); this.trains.delete(id); }
|
| 507 |
+
}
|
| 508 |
+
for (const u of sim.units) {
|
| 509 |
+
if (!this.trains.has(u.id)) {
|
| 510 |
+
const tm = new TrainMesh(this.shared, (u.runs[0] && u.runs[0].kind) || "stop");
|
| 511 |
+
tm.unitId = u.id;
|
| 512 |
+
tm.group.traverse((o) => { o.userData.unit = u.id; });
|
| 513 |
+
this.scene.add(tm.group);
|
| 514 |
+
this.trains.set(u.id, tm);
|
| 515 |
+
}
|
| 516 |
+
}
|
| 517 |
+
this.sunT = null;
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
setSun({ elevation, azimuth }) {
|
| 521 |
+
const el = THREE.MathUtils.degToRad(Math.max(elevation, -8));
|
| 522 |
+
const az = THREE.MathUtils.degToRad(azimuth);
|
| 523 |
+
this.sunDir = new THREE.Vector3(Math.sin(az) * Math.cos(el), Math.sin(Math.max(el, 0.03)), -Math.cos(az) * Math.cos(el));
|
| 524 |
+
const day = THREE.MathUtils.smoothstep(elevation, -6, 10); // 0 night .. 1 day
|
| 525 |
+
const dusk = Math.max(0, 1 - Math.abs(elevation - 1) / 9) * (elevation > -8 ? 1 : 0);
|
| 526 |
+
this.night = 1 - day;
|
| 527 |
+
const sky = new THREE.Color(0x0d1830).lerp(new THREE.Color(0xa9c8e6), day);
|
| 528 |
+
sky.lerp(new THREE.Color(0xe39a62), dusk * 0.45);
|
| 529 |
+
this.scene.background = sky;
|
| 530 |
+
this.scene.fog.color.copy(sky);
|
| 531 |
+
this.hemi.intensity = 0.5 + 2.6 * day;
|
| 532 |
+
this.hemi.color.set(0xeef3ff).lerp(new THREE.Color(0x5a6a9a), 1 - day);
|
| 533 |
+
this.sun.intensity = 3.6 * day + 0.8 * dusk;
|
| 534 |
+
this.sun.color.set(0xffffff).lerp(new THREE.Color(0xffa060), dusk);
|
| 535 |
+
this.renderer.toneMappingExposure = 0.8 + 0.35 * day;
|
| 536 |
+
this.shared.mats.glow.opacity = 0.9 * this.night;
|
| 537 |
+
}
|
| 538 |
+
|
| 539 |
+
// --- cameras ---------------------------------------------------------------------------------------------
|
| 540 |
+
|
| 541 |
+
viewAll() {
|
| 542 |
+
const [x0, y0, x1, y1] = this.twin.bounds;
|
| 543 |
+
const cx = (x0 + x1) / 2, cy = (y0 + y1) / 2;
|
| 544 |
+
this.mode = "map";
|
| 545 |
+
this.followId = null;
|
| 546 |
+
this.controls.enabled = true;
|
| 547 |
+
// high enough that the whole line fits the width of the view, whatever its shape
|
| 548 |
+
const half = (x1 - x0) / 2 - 2000;
|
| 549 |
+
const tanH = Math.tan(THREE.MathUtils.degToRad(this.camera.fov / 2)) * this.camera.aspect;
|
| 550 |
+
const h = Math.max(20000, (half / tanH) * 0.95);
|
| 551 |
+
this.controls.target.set(cx, 0, -cy);
|
| 552 |
+
this.camera.position.set(cx, h, -cy + h * 0.44);
|
| 553 |
+
this.controls.update();
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
viewAt(x, y, dist = 700, pitch = 0.9) {
|
| 557 |
+
this.mode = "map";
|
| 558 |
+
this.followId = null;
|
| 559 |
+
this.controls.enabled = true;
|
| 560 |
+
this.controls.target.set(x, 0, -y);
|
| 561 |
+
this.camera.position.set(x - dist * 0.35, dist * Math.sin(pitch), -y + dist * Math.cos(pitch));
|
| 562 |
+
this.controls.update();
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
viewStation(code) {
|
| 566 |
+
const st = this.twin.stations.find((s) => s.code === code);
|
| 567 |
+
if (st) this.viewAt(st.x, st.y, 650, 0.75);
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
follow(unitId, mode = "chase") {
|
| 571 |
+
this.followId = unitId;
|
| 572 |
+
this.mode = mode;
|
| 573 |
+
this.lastFollow = null;
|
| 574 |
+
this.controls.enabled = mode !== "cab";
|
| 575 |
+
if (mode === "chase") {
|
| 576 |
+
const tm = this.trains.get(unitId);
|
| 577 |
+
if (tm) {
|
| 578 |
+
const p = tm.group.position.clone();
|
| 579 |
+
this.controls.target.copy(p);
|
| 580 |
+
this.camera.position.set(p.x - 140, 95, p.z + 160);
|
| 581 |
+
this.controls.update();
|
| 582 |
+
}
|
| 583 |
+
}
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
+
// --- per frame ---------------------------------------------------------------------------------------------
|
| 587 |
+
|
| 588 |
+
update(t, states, dtReal) {
|
| 589 |
+
const sim = this.sim;
|
| 590 |
+
if (!sim) return;
|
| 591 |
+
if (this.sunT === null || Math.abs(t - this.sunT) > 0.5) { // the sun moves with the clock
|
| 592 |
+
this.setSun(sunPosition(this.R.day, t));
|
| 593 |
+
this.sunT = t;
|
| 594 |
+
}
|
| 595 |
+
// trains
|
| 596 |
+
const seen = new Set();
|
| 597 |
+
for (const s of states) {
|
| 598 |
+
const tm = this.trains.get(s.unit);
|
| 599 |
+
if (!tm) continue;
|
| 600 |
+
seen.add(s.unit);
|
| 601 |
+
tm.group.visible = true;
|
| 602 |
+
tm.setKind(this.shared, s.kind);
|
| 603 |
+
const offs = [CAR / 2 + GAP / 2 + BOGIE, CAR / 2 + GAP / 2 - BOGIE, -(CAR / 2 + GAP / 2) + BOGIE,
|
| 604 |
+
-(CAR / 2 + GAP / 2) - BOGIE];
|
| 605 |
+
const pts = this.line.body(s.dir, s.lw, s.gn, s.km, offs, s.lanes);
|
| 606 |
+
const mid = this.line.place(s.dir, s.lw, s.gn, s.km, s.lanes);
|
| 607 |
+
tm.group.position.set(mid[0], 0, -mid[1]);
|
| 608 |
+
for (let c = 0; c < 2; c++) {
|
| 609 |
+
const [a, b] = [pts[2 * c], pts[2 * c + 1]]; // its front and rear bogie, so cars follow curves
|
| 610 |
+
const car = tm.cars[c];
|
| 611 |
+
car.position.set((a[0] + b[0]) / 2 - mid[0], 0, -((a[1] + b[1]) / 2 - mid[1]));
|
| 612 |
+
car.rotation.y = Math.atan2(a[1] - b[1], a[0] - b[0]);
|
| 613 |
+
// head lamps on the leading cab, tail lamps behind
|
| 614 |
+
for (const l of car.userData.lamps) l.material = c === 0 ? this.shared.mats.head : this.shared.mats.tail;
|
| 615 |
+
}
|
| 616 |
+
const head = pts[0];
|
| 617 |
+
tm.glow.position.set(head[0] - mid[0], 2.0, -(head[1] - mid[1]));
|
| 618 |
+
tm.halo.visible = this.selected === s.unit;
|
| 619 |
+
tm.state = s;
|
| 620 |
+
const delay = Math.round(s.delay);
|
| 621 |
+
tm.label.element.innerHTML = `<i style="background:${KIND_COLOR[s.kind]}"></i>${s.train}`
|
| 622 |
+
+ (delay > 0 ? `<span class="late">+${delay}</span>` : "");
|
| 623 |
+
tm.label.element.classList.toggle("sel", this.selected === s.unit);
|
| 624 |
+
}
|
| 625 |
+
for (const [id, tm] of this.trains) if (!seen.has(id)) tm.group.visible = false;
|
| 626 |
+
|
| 627 |
+
// signals
|
| 628 |
+
const occ = sim.occupancy(states);
|
| 629 |
+
for (const sg of this.signals) {
|
| 630 |
+
const sec = this.R.sections[sg.section];
|
| 631 |
+
const closed = this.closures.some((c) => c.kind === "closed" && t >= c.start && t < c.end
|
| 632 |
+
&& this.covers(c, sec));
|
| 633 |
+
const red = closed || (occ[sg.section] && occ[sg.section].length > 0);
|
| 634 |
+
sg.red.material = red ? this.sigRed : this.sigOff;
|
| 635 |
+
sg.green.material = red ? this.sigOff : this.sigGreen;
|
| 636 |
+
}
|
| 637 |
+
// level crossings: down while a train is within ~35 s of reaching it, until it has passed
|
| 638 |
+
const soon = states.map((s) => {
|
| 639 |
+
const un = sim.units.find((u) => u.id === s.unit);
|
| 640 |
+
const f = un ? sim.stateAt(un, t + 0.6) : s;
|
| 641 |
+
return [Math.min(s.km, f.km) - 0.035, Math.max(s.km, f.km) + 0.035];
|
| 642 |
+
});
|
| 643 |
+
const blink = Math.floor(performance.now() / 450) % 2;
|
| 644 |
+
for (const c of this.crossings) {
|
| 645 |
+
const down = soon.some(([a, b]) => c.km >= a && c.km <= b);
|
| 646 |
+
const target = down ? 0 : Math.PI / 2;
|
| 647 |
+
c.angle += (target - c.angle) * Math.min(1, dtReal * 2.5);
|
| 648 |
+
for (const a of c.arms) a.rotation.z = c.angle;
|
| 649 |
+
c.lamps.forEach((l, i) => { l.material = down && (i + blink) % 2 ? this.lampOn : c.offMat; });
|
| 650 |
+
}
|
| 651 |
+
// disruptions: visible while they last, pulsing
|
| 652 |
+
const pulse = 0.45 + 0.25 * Math.sin(performance.now() / 300);
|
| 653 |
+
for (const c of this.closures) {
|
| 654 |
+
const on = t >= c.start && t < c.end;
|
| 655 |
+
c.mesh.visible = on;
|
| 656 |
+
c.label.visible = on;
|
| 657 |
+
c.mat.opacity = pulse;
|
| 658 |
+
c.line.visible = on && this.overview && this.overview.visible;
|
| 659 |
+
}
|
| 660 |
+
for (const v of this.breaks) {
|
| 661 |
+
const on = t >= v.t - 1 && t <= v.t + 3;
|
| 662 |
+
v.mesh.visible = v.label.visible = on;
|
| 663 |
+
if (on) v.mesh.scale.setScalar(1 + 0.15 * Math.sin(performance.now() / 160));
|
| 664 |
+
}
|
| 665 |
+
animateWorld(this.world, dtReal);
|
| 666 |
+
this.updateCamera();
|
| 667 |
+
this.updateLabels();
|
| 668 |
+
}
|
| 669 |
+
|
| 670 |
+
covers(c, sec) {
|
| 671 |
+
const idx = Object.fromEntries(this.R.points.map((p, i) => [p.code, i]));
|
| 672 |
+
const lo = Math.min(idx[c.where[0]], idx[c.where[c.where.length - 1]]);
|
| 673 |
+
const hi = Math.max(idx[c.where[0]], idx[c.where[c.where.length - 1]]);
|
| 674 |
+
const i = idx[sec.a];
|
| 675 |
+
return i >= lo && i < hi;
|
| 676 |
+
}
|
| 677 |
+
|
| 678 |
+
updateCamera() {
|
| 679 |
+
const tm = this.followId ? this.trains.get(this.followId) : null;
|
| 680 |
+
if (tm && tm.group.visible && tm.state) {
|
| 681 |
+
const s = tm.state;
|
| 682 |
+
if (this.mode === "cab") {
|
| 683 |
+
const pts = this.line.body(s.dir, s.lw, s.gn, s.km, [CAR + 0.5, CAR + 160], s.lanes);
|
| 684 |
+
this.camera.position.set(pts[0][0], 3.4, -pts[0][1]);
|
| 685 |
+
this.camera.lookAt(pts[1][0], 2.2, -pts[1][1]);
|
| 686 |
+
} else {
|
| 687 |
+
const p = tm.group.position;
|
| 688 |
+
if (this.lastFollow) {
|
| 689 |
+
const d = p.clone().sub(this.lastFollow);
|
| 690 |
+
this.camera.position.add(d);
|
| 691 |
+
this.controls.target.add(d);
|
| 692 |
+
}
|
| 693 |
+
this.lastFollow = p.clone();
|
| 694 |
+
}
|
| 695 |
+
}
|
| 696 |
+
if (this.mode !== "cab") this.controls.update();
|
| 697 |
+
// shadows follow the view
|
| 698 |
+
const tgt = this.mode === "cab" ? this.camera.position : this.controls.target;
|
| 699 |
+
const h = this.camera.position.y;
|
| 700 |
+
this.sun.castShadow = h < 2500;
|
| 701 |
+
this.sun.target.position.copy(tgt);
|
| 702 |
+
this.sun.position.copy(tgt).addScaledVector(this.sunDir || new THREE.Vector3(0.3, 0.8, 0.3), 1500);
|
| 703 |
+
this.scene.fog.near = Math.max(1500, h * 2);
|
| 704 |
+
this.scene.fog.far = Math.max(20000, h * 7);
|
| 705 |
+
if (this.overview) { // the overview line fades in as the track gets too thin to see
|
| 706 |
+
const a = THREE.MathUtils.smoothstep(h, 1200, 4000);
|
| 707 |
+
this.overview.visible = a > 0.01;
|
| 708 |
+
for (const m of this.lineMats) m.opacity = a;
|
| 709 |
+
for (const m of this.closureMats || []) m.opacity = a;
|
| 710 |
+
}
|
| 711 |
+
const trees = this.world.getObjectByName("trees");
|
| 712 |
+
if (trees) { // trees only near the view: there are tens of thousands
|
| 713 |
+
trees.visible = h < 2500;
|
| 714 |
+
if (trees.visible) for (const im of trees.children) im.visible = im.userData.centre.distanceTo(tgt) < 3500 + h;
|
| 715 |
+
}
|
| 716 |
+
}
|
| 717 |
+
|
| 718 |
+
updateLabels() {
|
| 719 |
+
const cam = this.camera.position;
|
| 720 |
+
const h = cam.y;
|
| 721 |
+
const narrow = window.innerWidth < 760 && h > 20000; // a phone's overview: terminals and turn points only
|
| 722 |
+
for (const l of this.placeLabels) {
|
| 723 |
+
const show = !l.dup && !narrow && ((l.r <= 1 && h > 2500) || (l.r === 2 && h > 900 && h < 20000) || (l.r === 3 && h < 6000 && h > 300));
|
| 724 |
+
l.obj.visible = show;
|
| 725 |
+
}
|
| 726 |
+
for (const l of this.stationLabels) { // far stations only from high up
|
| 727 |
+
l.obj.visible = (h > 2000 || Math.hypot(l.x - cam.x, -l.y - cam.z) < 12000)
|
| 728 |
+
&& (!narrow || ["LW", "HDG", "BP", "ZH", "GN"].includes(l.code));
|
| 729 |
+
}
|
| 730 |
+
for (const tm of this.trains.values()) {
|
| 731 |
+
const d = tm.group.position.distanceTo(cam);
|
| 732 |
+
tm.label.visible = tm.group.visible;
|
| 733 |
+
tm.label.element.classList.toggle("far", d > 9000);
|
| 734 |
+
}
|
| 735 |
+
for (const c of this.closures) if (c.label.visible) c.label.element.classList.toggle("far", h > 15000);
|
| 736 |
+
}
|
| 737 |
+
|
| 738 |
+
render() {
|
| 739 |
+
this.renderer.render(this.scene, this.camera);
|
| 740 |
+
this.labels.render(this.scene, this.camera);
|
| 741 |
+
}
|
| 742 |
+
|
| 743 |
+
pick(clientX, clientY) {
|
| 744 |
+
const r = this.renderer.domElement.getBoundingClientRect();
|
| 745 |
+
const v = new THREE.Vector2(((clientX - r.left) / r.width) * 2 - 1, -((clientY - r.top) / r.height) * 2 + 1);
|
| 746 |
+
const ray = new THREE.Raycaster();
|
| 747 |
+
ray.setFromCamera(v, this.camera);
|
| 748 |
+
const objs = [];
|
| 749 |
+
for (const tm of this.trains.values()) if (tm.group.visible) objs.push(tm.group);
|
| 750 |
+
const hit = ray.intersectObjects(objs, true).find((h) => h.object.userData.unit);
|
| 751 |
+
if (hit) return hit.object.userData.unit;
|
| 752 |
+
// generous fallback: the train nearest the click on screen
|
| 753 |
+
let best = null, bd = 28;
|
| 754 |
+
for (const tm of this.trains.values()) {
|
| 755 |
+
if (!tm.group.visible) continue;
|
| 756 |
+
const p = tm.group.position.clone().project(this.camera);
|
| 757 |
+
const sx = (p.x + 1) / 2 * r.width + r.left, sy = (1 - p.y) / 2 * r.height + r.top;
|
| 758 |
+
const d = Math.hypot(sx - clientX, sy - clientY);
|
| 759 |
+
if (d < bd && p.z < 1) { bd = d; best = tm.unitId; }
|
| 760 |
+
}
|
| 761 |
+
return best;
|
| 762 |
+
}
|
| 763 |
+
}
|
| 764 |
+
|
| 765 |
+
export { CANONICAL };
|
openenv/rail_openenv/web/twin/style.css
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* The line twin's interface: dark glass panels over the 3D view (one theme: the map is the light source). */
|
| 2 |
+
:root {
|
| 3 |
+
color-scheme: dark;
|
| 4 |
+
--surface-1: rgba(26, 26, 25, 0.86);
|
| 5 |
+
--surface-2: rgba(44, 44, 42, 0.92);
|
| 6 |
+
--page: #0d0d0d;
|
| 7 |
+
--text-primary: #ffffff;
|
| 8 |
+
--text-secondary: #c3c2b7;
|
| 9 |
+
--muted: #898781;
|
| 10 |
+
--grid: #2c2c2a;
|
| 11 |
+
--border: rgba(255, 255, 255, 0.10);
|
| 12 |
+
--stop: #3987e5;
|
| 13 |
+
--fast: #d95926;
|
| 14 |
+
--critical: #d03b3b;
|
| 15 |
+
--warning: #fab219;
|
| 16 |
+
--serious: #ec835a;
|
| 17 |
+
--good: #0ca30c;
|
| 18 |
+
--accent: #199e70;
|
| 19 |
+
--bottom-h: 196px;
|
| 20 |
+
--side-w: 300px;
|
| 21 |
+
--bar-h: 52px;
|
| 22 |
+
}
|
| 23 |
+
* { box-sizing: border-box; }
|
| 24 |
+
html, body { margin: 0; height: 100%; overflow: hidden; background: var(--page); color: var(--text-primary);
|
| 25 |
+
font: 13px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif; }
|
| 26 |
+
#stage { position: fixed; inset: 0; }
|
| 27 |
+
#stage canvas { display: block; }
|
| 28 |
+
.labels { position: absolute; inset: 0; pointer-events: auto; }
|
| 29 |
+
.muted { color: var(--muted); }
|
| 30 |
+
button, select { font: inherit; color: var(--text-primary); background: var(--surface-2); border: 1px solid var(--border);
|
| 31 |
+
border-radius: 6px; padding: 4px 9px; cursor: pointer; }
|
| 32 |
+
button:hover, select:hover { border-color: rgba(255, 255, 255, 0.28); }
|
| 33 |
+
button.on { background: #f2f1ec; color: #0b0b0b; }
|
| 34 |
+
select { max-width: 100%; }
|
| 35 |
+
|
| 36 |
+
#bar { position: fixed; top: 0; left: 0; right: 0; height: var(--bar-h); display: flex; align-items: center; gap: 14px;
|
| 37 |
+
padding: 0 14px; background: var(--surface-1); border-bottom: 1px solid var(--border); backdrop-filter: blur(8px);
|
| 38 |
+
z-index: 5; overflow-x: auto; }
|
| 39 |
+
.brand { display: flex; flex-direction: column; line-height: 1.15; white-space: nowrap; }
|
| 40 |
+
.brand span { color: var(--muted); font-size: 11px; }
|
| 41 |
+
.pick { display: flex; align-items: center; gap: 6px; min-width: 0; }
|
| 42 |
+
.pick span { color: var(--muted); font-size: 11px; }
|
| 43 |
+
#task { width: 250px; }
|
| 44 |
+
#plan { width: 230px; }
|
| 45 |
+
#station { width: 120px; }
|
| 46 |
+
.transport { display: flex; align-items: center; gap: 6px; }
|
| 47 |
+
#play { width: 38px; }
|
| 48 |
+
#clock { display: flex; flex-direction: column; line-height: 1.1; min-width: 118px; font-variant-numeric: tabular-nums; }
|
| 49 |
+
#clock b { font-size: 17px; }
|
| 50 |
+
#clock span { color: var(--muted); font-size: 11px; }
|
| 51 |
+
.cams { display: flex; gap: 4px; }
|
| 52 |
+
.graphlink { color: var(--text-secondary); text-decoration: none; white-space: nowrap; }
|
| 53 |
+
#panels { margin-left: auto; }
|
| 54 |
+
|
| 55 |
+
.panel { position: fixed; background: var(--surface-1); border: 1px solid var(--border); border-radius: 10px;
|
| 56 |
+
backdrop-filter: blur(8px); z-index: 4; }
|
| 57 |
+
#left { top: calc(var(--bar-h) + 10px); left: 10px; width: var(--side-w); max-height: calc(100% - var(--bar-h) - var(--bottom-h) - 30px);
|
| 58 |
+
overflow-y: auto; padding: 12px 14px; }
|
| 59 |
+
#right { top: calc(var(--bar-h) + 10px); right: 10px; width: var(--side-w); max-height: calc(100% - var(--bar-h) - var(--bottom-h) - 30px);
|
| 60 |
+
overflow-y: auto; padding: 10px 12px; }
|
| 61 |
+
#bottom { left: 10px; right: 10px; bottom: 10px; height: var(--bottom-h); display: flex; gap: 10px; padding: 8px 10px; }
|
| 62 |
+
h2, h3 { margin: 4px 0 8px; font-size: 13px; font-weight: 600; }
|
| 63 |
+
h3 { color: var(--text-secondary); margin-top: 12px; }
|
| 64 |
+
|
| 65 |
+
#scenario .day { font-size: 15px; font-weight: 600; }
|
| 66 |
+
#scenario .tier { display: inline-block; padding: 1px 7px; border-radius: 9px; background: var(--surface-2);
|
| 67 |
+
color: var(--text-secondary); font-size: 11px; margin-left: 6px; }
|
| 68 |
+
#scenario ul { margin: 6px 0 0; padding-left: 16px; }
|
| 69 |
+
#scenario li { margin: 2px 0; }
|
| 70 |
+
#scenario .note { color: var(--text-secondary); font-size: 12px; }
|
| 71 |
+
#result { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--border); }
|
| 72 |
+
#result .plan-name { color: var(--text-secondary); }
|
| 73 |
+
#result .hero { font-size: 30px; font-weight: 600; line-height: 1.1; margin: 4px 0; }
|
| 74 |
+
#result .hero small { font-size: 12px; font-weight: 400; color: var(--muted); margin-left: 4px; }
|
| 75 |
+
#result .badge { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; }
|
| 76 |
+
#result .badge.ok::before { content: "✓"; color: var(--good); font-weight: 700; }
|
| 77 |
+
#result .badge.bad::before { content: "✕"; color: var(--critical); font-weight: 700; }
|
| 78 |
+
#result dl { display: grid; grid-template-columns: auto 1fr; gap: 2px 12px; margin: 8px 0 0; }
|
| 79 |
+
#result dt { color: var(--muted); }
|
| 80 |
+
#result dd { margin: 0; font-variant-numeric: tabular-nums; }
|
| 81 |
+
#result .viol { margin: 8px 0 0; padding: 0; list-style: none; max-height: 140px; overflow-y: auto; }
|
| 82 |
+
#result .viol li { cursor: pointer; padding: 3px 0; border-top: 1px solid var(--grid); font-size: 12px; color: var(--text-secondary); }
|
| 83 |
+
#result .viol li::before { content: "! "; color: var(--critical); font-weight: 700; }
|
| 84 |
+
.legend { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--border); font-size: 12px; color: var(--text-secondary); }
|
| 85 |
+
.legend div { margin: 3px 0; }
|
| 86 |
+
.chip { display: inline-block; width: 10px; height: 10px; border-radius: 3px; margin: 0 5px 0 0; vertical-align: -1px; }
|
| 87 |
+
.legend .chip + .chip, .legend i + i { margin-left: 10px; }
|
| 88 |
+
.bar { display: inline-block; width: 16px; height: 6px; border-radius: 2px; margin-right: 5px; vertical-align: 1px; }
|
| 89 |
+
.bar.closed { background: var(--critical); } .bar.slow { background: var(--warning); } .bar.track { background: var(--serious); }
|
| 90 |
+
.bar.single { background: #ffffff; outline: 2px solid #1a1a19; } .bar.double { background: #9a9a96; outline: 2px solid #1a1a19; }
|
| 91 |
+
body.clean #left, body.clean #right { display: none; }
|
| 92 |
+
|
| 93 |
+
#trains, #cancelled { list-style: none; margin: 0; padding: 0; }
|
| 94 |
+
#trains li, #cancelled li { display: grid; grid-template-columns: 12px auto 1fr auto; align-items: center; gap: 2px 8px;
|
| 95 |
+
padding: 6px 4px; border-top: 1px solid var(--grid); cursor: pointer; border-radius: 4px; }
|
| 96 |
+
#trains li:hover { background: rgba(255, 255, 255, 0.05); }
|
| 97 |
+
#trains li.sel { background: rgba(255, 255, 255, 0.10); }
|
| 98 |
+
#trains .num { font-weight: 600; font-variant-numeric: tabular-nums; }
|
| 99 |
+
#trains .st { grid-column: 2 / 5; color: var(--text-secondary); font-size: 12px; }
|
| 100 |
+
#trains .dl { font-variant-numeric: tabular-nums; color: var(--text-secondary); }
|
| 101 |
+
#trains .dl.late { color: var(--warning); }
|
| 102 |
+
#cancelled li { cursor: default; color: var(--muted); }
|
| 103 |
+
|
| 104 |
+
.graphwrap { position: relative; flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
|
| 105 |
+
.costwrap { position: relative; flex: 0 0 340px; }
|
| 106 |
+
.chart-h { display: flex; gap: 10px; align-items: baseline; font-size: 12px; padding-left: 44px; }
|
| 107 |
+
#graph { flex: 1; width: 100%; min-height: 0; cursor: crosshair; touch-action: none; }
|
| 108 |
+
#cost { width: 100%; height: 100%; cursor: crosshair; }
|
| 109 |
+
.tip { position: absolute; pointer-events: none; background: #f2f1ec; color: #0b0b0b; padding: 4px 8px; border-radius: 6px;
|
| 110 |
+
font-size: 12px; white-space: nowrap; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); z-index: 3; }
|
| 111 |
+
|
| 112 |
+
/* labels in the 3D view */
|
| 113 |
+
.tlabel { font-size: 11px; font-weight: 600; padding: 1px 6px 1px 4px; border-radius: 9px; background: rgba(13, 13, 13, 0.78);
|
| 114 |
+
color: #fff; white-space: nowrap; display: flex; align-items: center; gap: 4px; pointer-events: none; }
|
| 115 |
+
.tlabel i { width: 7px; height: 7px; border-radius: 50%; display: inline-block; }
|
| 116 |
+
.tlabel .late { color: var(--warning); font-weight: 600; }
|
| 117 |
+
.tlabel.far { font-size: 10px; padding: 0 5px 0 3px; }
|
| 118 |
+
.tlabel.far .late { display: none; }
|
| 119 |
+
.tlabel.sel { outline: 2px solid #fff; }
|
| 120 |
+
.slabel { font-size: 12px; font-weight: 600; color: #fff; background: #23408f; padding: 1px 7px; border-radius: 3px;
|
| 121 |
+
white-space: nowrap; pointer-events: none; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); }
|
| 122 |
+
.plabel { color: rgba(255, 255, 255, 0.82); text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9); pointer-events: none; white-space: nowrap; }
|
| 123 |
+
.plabel.r0, .plabel.r1 { font-size: 15px; font-weight: 600; letter-spacing: 0.04em; }
|
| 124 |
+
.plabel.r2 { font-size: 12px; }
|
| 125 |
+
.plabel.r3 { font-size: 11px; font-style: italic; }
|
| 126 |
+
.clabel { font-size: 11px; color: #fff; padding: 2px 7px; border-radius: 4px; white-space: nowrap; pointer-events: none;
|
| 127 |
+
border-left: 3px solid var(--critical); background: rgba(13, 13, 13, 0.82); }
|
| 128 |
+
.clabel.slow { border-color: var(--warning); } .clabel.track { border-color: var(--serious); }
|
| 129 |
+
.clabel.far { font-size: 10px; }
|
| 130 |
+
.vlabel { font-size: 11px; color: #fff; padding: 2px 7px; border-radius: 4px; white-space: nowrap; pointer-events: none;
|
| 131 |
+
background: var(--critical); }
|
| 132 |
+
.vlabel b::before { content: "! "; }
|
| 133 |
+
|
| 134 |
+
#toasts { position: fixed; left: 50%; transform: translateX(-50%); top: calc(var(--bar-h) + 12px); z-index: 6;
|
| 135 |
+
display: flex; flex-direction: column; gap: 6px; align-items: center; pointer-events: none; }
|
| 136 |
+
.toast { background: var(--surface-2); border: 1px solid var(--border); border-left: 3px solid var(--text-secondary);
|
| 137 |
+
padding: 5px 10px; border-radius: 6px; font-size: 12px; max-width: min(560px, 90vw); transition: opacity 0.6s; }
|
| 138 |
+
.toast.closure, .toast.violation { border-left-color: var(--critical); }
|
| 139 |
+
.toast.reopen { border-left-color: var(--good); }
|
| 140 |
+
.toast time { color: var(--muted); margin-right: 6px; font-variant-numeric: tabular-nums; }
|
| 141 |
+
|
| 142 |
+
#loading { position: fixed; inset: 0; display: grid; place-items: center; background: var(--page); z-index: 10;
|
| 143 |
+
transition: opacity 0.5s; text-align: center; }
|
| 144 |
+
#loading p { color: var(--muted); margin: 6px 0 0; }
|
| 145 |
+
#loading.done { opacity: 0; pointer-events: none; }
|
| 146 |
+
|
| 147 |
+
@media (max-width: 1100px) {
|
| 148 |
+
:root { --side-w: 250px; }
|
| 149 |
+
#task { width: 200px; } #plan { width: 180px; }
|
| 150 |
+
.graphlink { display: none; }
|
| 151 |
+
}
|
| 152 |
+
@media (max-width: 760px) {
|
| 153 |
+
:root { --bottom-h: 150px; --bar-h: 96px; }
|
| 154 |
+
#bar { flex-wrap: wrap; height: auto; min-height: var(--bar-h); padding: 6px 16px; gap: 6px 10px; }
|
| 155 |
+
.brand span { display: none; }
|
| 156 |
+
#task, #plan { width: 150px; }
|
| 157 |
+
#left, #right { display: none; }
|
| 158 |
+
#bottom { left: 6px; right: 6px; bottom: 6px; }
|
| 159 |
+
.costwrap { display: none; }
|
| 160 |
+
.chart-h .muted { display: none; }
|
| 161 |
+
}
|
openenv/rail_openenv/web/twin/twin.json.gz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:11f10c84c37afd98a081fe1ea9494f1f0421a6a09c52ea24589b835cea00db31
|
| 3 |
+
size 3548310
|
openenv/rail_openenv/web/twin/world.js
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// The static scenery of the line twin, built once from twin.json (OpenStreetMap, see SOURCES.md): ground, land cover,
|
| 2 |
+
// water, roads, buildings, trees, landmarks, track and platforms. Everything is merged into a few large meshes,
|
| 3 |
+
// tiled so the renderer can skip what is off screen.
|
| 4 |
+
//
|
| 5 |
+
// World frame: x east, z south (-north), y up, metres; the twin's (x, y) maps to (x, -y).
|
| 6 |
+
|
| 7 |
+
import * as THREE from "three";
|
| 8 |
+
import { mergeGeometries } from "three/addons/utils/BufferGeometryUtils.js";
|
| 9 |
+
|
| 10 |
+
export const TILE = 2000; // metres: tile size for culling
|
| 11 |
+
const Y = { landcover: 0.02, farmland: 0.01, water: 0.05, waterway: 0.06, road: 0.09, platform: 0, track: 0.12 };
|
| 12 |
+
|
| 13 |
+
export const COLORS = {
|
| 14 |
+
ground: 0x789a58,
|
| 15 |
+
landcover: {
|
| 16 |
+
0: 0xbfb6a4, 1: 0x82a85c, 2: 0x7fae5a, 3: 0xb4aebb, 4: 0xc8b9aa, 5: 0xa39d92, 6: 0xb3a789, 7: 0x3e6a35,
|
| 17 |
+
8: 0x7ea75a, 9: 0x8a9255, 10: 0x6d9a7f, 11: 0x6f9a55, 12: 0x7d9c6a, 13: 0xd2dbd8, 14: 0x7fa257, 15: 0x98a862,
|
| 18 |
+
16: 0xd8c99a,
|
| 19 |
+
},
|
| 20 |
+
water: 0x4d7da6,
|
| 21 |
+
road: [0x4c4d51, 0x55565a, 0x5b5c60, 0x616265, 0x67686b, 0x6e6e70, 0x9b8b6a, 0xa2584b],
|
| 22 |
+
roadWidth: [22, 13, 10, 8, 5.5, 4, 3, 2.6],
|
| 23 |
+
waterwayWidth: [22, 14, 4, 1.8],
|
| 24 |
+
building: [
|
| 25 |
+
[0x9b5a43, 0x4a3a34], [0xa77d63, 0x585654], [0x7b5d4b, 0x3d3a36], [0x9ea3a8, 0x7c8186], [0xb8a998, 0x6f6a66],
|
| 26 |
+
[0x8a4e3b, 0x3f3936], [0xd5e2de, 0xc3d2ce], [0xb7724f, 0x4b4744],
|
| 27 |
+
],
|
| 28 |
+
ballast: 0x8b8379, rail: 0xc4c7cb, sleeper: 0x5a4a3d, platform: 0xbab6ae, platformEdge: 0xe0bf38,
|
| 29 |
+
trunk: 0x5b4636, crown: 0x3f6b33, crown2: 0x4f7a3b,
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
const col = new THREE.Color();
|
| 33 |
+
|
| 34 |
+
function decode(flat) {
|
| 35 |
+
const pts = [];
|
| 36 |
+
for (let i = 0; i < flat.length; i += 2) pts.push([flat[i] / 10, flat[i + 1] / 10]);
|
| 37 |
+
return pts;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
function tileKey(x, y) {
|
| 41 |
+
return `${Math.floor(x / TILE)},${Math.floor(y / TILE)}`;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// growable typed buffers for one mesh
|
| 45 |
+
class Buf {
|
| 46 |
+
constructor() { this.pos = []; this.col = []; this.uv = null; this.idx = []; }
|
| 47 |
+
get n() { return this.pos.length / 3; }
|
| 48 |
+
vert(x, y, z, c) { this.pos.push(x, y, z); this.col.push(c.r, c.g, c.b); return this.n - 1; }
|
| 49 |
+
// a triangle that faces up (+y), whichever way its vertices were given
|
| 50 |
+
up(a, b, c) {
|
| 51 |
+
const P = this.pos;
|
| 52 |
+
const ax = P[3 * a], az = P[3 * a + 2], bx = P[3 * b], bz = P[3 * b + 2], cx = P[3 * c], cz = P[3 * c + 2];
|
| 53 |
+
const ny = (bz - az) * (cx - ax) - (bx - ax) * (cz - az);
|
| 54 |
+
if (ny >= 0) this.idx.push(a, b, c); else this.idx.push(a, c, b);
|
| 55 |
+
}
|
| 56 |
+
geometry() {
|
| 57 |
+
const g = new THREE.BufferGeometry();
|
| 58 |
+
g.setAttribute("position", new THREE.Float32BufferAttribute(this.pos, 3));
|
| 59 |
+
g.setAttribute("color", new THREE.Float32BufferAttribute(this.col, 3));
|
| 60 |
+
if (this.uv) g.setAttribute("uv", new THREE.Float32BufferAttribute(this.uv, 2));
|
| 61 |
+
g.setIndex(this.n > 65535 ? new THREE.Uint32BufferAttribute(this.idx, 1) : new THREE.Uint16BufferAttribute(this.idx, 1));
|
| 62 |
+
g.computeVertexNormals();
|
| 63 |
+
g.computeBoundingSphere();
|
| 64 |
+
return g;
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
function tiled(map, key) {
|
| 69 |
+
let b = map.get(key);
|
| 70 |
+
if (!b) { b = new Buf(); map.set(key, b); }
|
| 71 |
+
return b;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
// Flat layers (ground, land cover, water, roads, track) are painted in a fixed order before anything 3D, without
|
| 75 |
+
// depth: they all lie on the ground, and centimetre offsets cannot win a depth test from kilometres away.
|
| 76 |
+
function flat(material, order) {
|
| 77 |
+
material.side = THREE.DoubleSide;
|
| 78 |
+
material.depthTest = false;
|
| 79 |
+
material.depthWrite = false;
|
| 80 |
+
material.userData.order = order;
|
| 81 |
+
return material;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
function meshes(map, material, opts = {}) {
|
| 85 |
+
const group = new THREE.Group();
|
| 86 |
+
for (const b of map.values()) {
|
| 87 |
+
if (!b.n) continue;
|
| 88 |
+
const m = new THREE.Mesh(b.geometry(), material);
|
| 89 |
+
m.receiveShadow = !!opts.receive;
|
| 90 |
+
m.castShadow = !!opts.cast;
|
| 91 |
+
if (material.userData.order !== undefined) m.renderOrder = material.userData.order;
|
| 92 |
+
group.add(m);
|
| 93 |
+
}
|
| 94 |
+
return group;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
// flat polygons (outer ring + holes), triangulated, at height y, into tiled buffers
|
| 98 |
+
function addPolygon(map, outer, holes, y, c) {
|
| 99 |
+
if (outer.length < 3) return;
|
| 100 |
+
const contour = outer.map(([x, yy]) => new THREE.Vector2(x, yy));
|
| 101 |
+
const hs = holes.map((h) => h.map(([x, yy]) => new THREE.Vector2(x, yy)));
|
| 102 |
+
let tris;
|
| 103 |
+
try {
|
| 104 |
+
tris = THREE.ShapeUtils.triangulateShape(contour, hs);
|
| 105 |
+
} catch {
|
| 106 |
+
return;
|
| 107 |
+
}
|
| 108 |
+
const all = [...outer, ...holes.flat()];
|
| 109 |
+
let cx = 0, cy = 0;
|
| 110 |
+
for (const [x, yy] of outer) { cx += x; cy += yy; }
|
| 111 |
+
const b = tiled(map, tileKey(cx / outer.length, cy / outer.length));
|
| 112 |
+
const base = b.n;
|
| 113 |
+
for (const [x, yy] of all) b.vert(x, y, -yy, c);
|
| 114 |
+
for (const [a, bb, cc] of tris) b.up(base + a, base + bb, base + cc);
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
// ribbons along polylines (roads, rivers, ditches) with mitred joins
|
| 118 |
+
function addRibbon(map, pts, width, y, c, uvScale = 0) {
|
| 119 |
+
if (pts.length < 2) return;
|
| 120 |
+
const h = width / 2;
|
| 121 |
+
const n = pts.length;
|
| 122 |
+
const mid = pts[n >> 1];
|
| 123 |
+
const b = tiled(map, tileKey(mid[0], mid[1]));
|
| 124 |
+
if (uvScale && !b.uv) b.uv = [];
|
| 125 |
+
const base = b.n;
|
| 126 |
+
let along = 0;
|
| 127 |
+
for (let i = 0; i < n; i++) {
|
| 128 |
+
const p = pts[i], a = pts[Math.max(0, i - 1)], q = pts[Math.min(n - 1, i + 1)];
|
| 129 |
+
let dx = q[0] - a[0], dy = q[1] - a[1];
|
| 130 |
+
const L = Math.hypot(dx, dy) || 1;
|
| 131 |
+
dx /= L; dy /= L;
|
| 132 |
+
let nx = -dy, ny = dx, k = 1;
|
| 133 |
+
if (i > 0 && i < n - 1) { // mitre: keep the width constant around the corner
|
| 134 |
+
const ex = p[0] - a[0], ey = p[1] - a[1], el = Math.hypot(ex, ey) || 1;
|
| 135 |
+
const sx = -ey / el, sy = ex / el;
|
| 136 |
+
k = 1 / Math.max(0.35, nx * sx + ny * sy);
|
| 137 |
+
}
|
| 138 |
+
if (i > 0) along += Math.hypot(p[0] - pts[i - 1][0], p[1] - pts[i - 1][1]);
|
| 139 |
+
b.vert(p[0] + nx * h * k, y, -(p[1] + ny * h * k), c);
|
| 140 |
+
b.vert(p[0] - nx * h * k, y, -(p[1] - ny * h * k), c);
|
| 141 |
+
if (uvScale) b.uv.push(0, along / uvScale, 1, along / uvScale);
|
| 142 |
+
}
|
| 143 |
+
for (let i = 0; i < n - 1; i++) {
|
| 144 |
+
const v = base + 2 * i;
|
| 145 |
+
b.up(v, v + 1, v + 2);
|
| 146 |
+
b.up(v + 1, v + 3, v + 2);
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
function sleeperTexture() {
|
| 151 |
+
const cv = document.createElement("canvas");
|
| 152 |
+
cv.width = 64; cv.height = 64;
|
| 153 |
+
const g = cv.getContext("2d");
|
| 154 |
+
g.fillStyle = "#8b8379"; g.fillRect(0, 0, 64, 64);
|
| 155 |
+
for (let i = 0; i < 400; i++) { // ballast grain
|
| 156 |
+
const v = 110 + Math.floor(Math.random() * 60);
|
| 157 |
+
g.fillStyle = `rgb(${v},${v - 6},${v - 14})`;
|
| 158 |
+
g.fillRect(Math.random() * 64, Math.random() * 64, 2, 2);
|
| 159 |
+
}
|
| 160 |
+
g.fillStyle = "#5a4a3d";
|
| 161 |
+
g.fillRect(6, 4, 52, 22); // one sleeper per 0.6 m (two per texture repeat of 1.2 m)
|
| 162 |
+
g.fillRect(6, 36, 52, 22);
|
| 163 |
+
const t = new THREE.CanvasTexture(cv);
|
| 164 |
+
t.wrapS = t.wrapT = THREE.RepeatWrapping;
|
| 165 |
+
t.anisotropy = 8;
|
| 166 |
+
t.colorSpace = THREE.SRGBColorSpace;
|
| 167 |
+
return t;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
export function buildWorld(twin, scene, opts = {}) {
|
| 171 |
+
const root = new THREE.Group();
|
| 172 |
+
root.name = "world";
|
| 173 |
+
const [x0, y0, x1, y1] = twin.bounds;
|
| 174 |
+
const vc = (o = {}) => new THREE.MeshLambertMaterial({ vertexColors: true, ...o });
|
| 175 |
+
|
| 176 |
+
// ground
|
| 177 |
+
const ground = new THREE.Mesh(new THREE.PlaneGeometry(x1 - x0 + 600000, y1 - y0 + 600000),
|
| 178 |
+
flat(new THREE.MeshLambertMaterial({ color: COLORS.ground }), -10));
|
| 179 |
+
ground.renderOrder = -10;
|
| 180 |
+
ground.rotation.x = -Math.PI / 2;
|
| 181 |
+
ground.position.set((x0 + x1) / 2, 0, -(y0 + y1) / 2);
|
| 182 |
+
ground.receiveShadow = true;
|
| 183 |
+
root.add(ground);
|
| 184 |
+
|
| 185 |
+
// land cover: farmland and meadows get a slight per-field tint, so the parcel pattern shows
|
| 186 |
+
const lc = new Map();
|
| 187 |
+
let i = 0;
|
| 188 |
+
for (const p of twin.landcover) {
|
| 189 |
+
const base = COLORS.landcover[p.c] ?? COLORS.ground;
|
| 190 |
+
col.setHex(base);
|
| 191 |
+
if (p.c === 14 || p.c === 15 || p.c === 1) col.offsetHSL(((i * 37) % 11 - 5) * 0.004, 0, ((i * 53) % 9 - 4) * 0.008);
|
| 192 |
+
addPolygon(lc, decode(p.o), p.i.map(decode), p.c >= 14 ? Y.farmland : Y.landcover, col.clone());
|
| 193 |
+
i++;
|
| 194 |
+
}
|
| 195 |
+
root.add(meshes(lc, flat(vc(), -9), { receive: true }));
|
| 196 |
+
|
| 197 |
+
// water: lakes, canals and rivers as areas, ditches and drains as ribbons
|
| 198 |
+
const wat = new Map();
|
| 199 |
+
col.setHex(COLORS.water);
|
| 200 |
+
for (const p of twin.water) addPolygon(wat, decode(p.o), p.i.map(decode), Y.water, col);
|
| 201 |
+
for (const w of twin.waterways) addRibbon(wat, decode(w.p), COLORS.waterwayWidth[w.c] ?? 2, Y.waterway, col);
|
| 202 |
+
const waterMat = flat(new THREE.MeshPhongMaterial({ vertexColors: true, shininess: 80, specular: 0x334455 }), -8);
|
| 203 |
+
root.add(meshes(wat, waterMat));
|
| 204 |
+
|
| 205 |
+
// roads (Dutch cycle paths in their red asphalt)
|
| 206 |
+
const rd = new Map();
|
| 207 |
+
for (const r of [...twin.roads].sort((a, b) => b.c - a.c)) {
|
| 208 |
+
col.setHex(COLORS.road[r.c] ?? COLORS.road[4]);
|
| 209 |
+
addRibbon(rd, decode(r.p), COLORS.roadWidth[r.c] ?? 4, Y.road + (7 - r.c) * 0.004, col);
|
| 210 |
+
}
|
| 211 |
+
root.add(meshes(rd, flat(vc(), -7), { receive: true }));
|
| 212 |
+
|
| 213 |
+
// buildings: walls and flat roofs, coloured by kind
|
| 214 |
+
const bl = new Map();
|
| 215 |
+
const B = twin.buildings;
|
| 216 |
+
const wall = new THREE.Color(), roof = new THREE.Color();
|
| 217 |
+
for (let k = 0; k < B.p.length; k++) {
|
| 218 |
+
const ring = decode(B.p[k]);
|
| 219 |
+
if (ring.length < 3) continue;
|
| 220 |
+
const h = B.h[k];
|
| 221 |
+
const [wc, rc] = COLORS.building[B.k[k]] ?? COLORS.building[0];
|
| 222 |
+
wall.setHex(wc).offsetHSL(0, 0, ((k * 29) % 7 - 3) * 0.012);
|
| 223 |
+
roof.setHex(rc);
|
| 224 |
+
let cx = 0, cy = 0;
|
| 225 |
+
for (const [x, y] of ring) { cx += x; cy += y; }
|
| 226 |
+
const b = tiled(bl, tileKey(cx / ring.length, cy / ring.length));
|
| 227 |
+
// orientation: walls face outwards whichever way the ring runs
|
| 228 |
+
let area = 0;
|
| 229 |
+
for (let j = 0; j < ring.length; j++) {
|
| 230 |
+
const [ax, ay] = ring[j], [bx, by] = ring[(j + 1) % ring.length];
|
| 231 |
+
area += ax * by - bx * ay;
|
| 232 |
+
}
|
| 233 |
+
const ccw = area > 0;
|
| 234 |
+
for (let j = 0; j < ring.length; j++) {
|
| 235 |
+
const [ax, ay] = ring[j], [bx, by] = ring[(j + 1) % ring.length];
|
| 236 |
+
const v = b.n;
|
| 237 |
+
b.vert(ax, 0, -ay, wall); b.vert(bx, 0, -by, wall); b.vert(bx, h, -by, wall); b.vert(ax, h, -ay, wall);
|
| 238 |
+
if (ccw) b.idx.push(v, v + 1, v + 2, v, v + 2, v + 3);
|
| 239 |
+
else b.idx.push(v, v + 2, v + 1, v, v + 3, v + 2);
|
| 240 |
+
}
|
| 241 |
+
let tris;
|
| 242 |
+
try { tris = THREE.ShapeUtils.triangulateShape(ring.map(([x, y]) => new THREE.Vector2(x, y)), []); }
|
| 243 |
+
catch { tris = []; }
|
| 244 |
+
const v0 = b.n;
|
| 245 |
+
for (const [x, y] of ring) b.vert(x, h, -y, roof);
|
| 246 |
+
for (const [a, bb, c] of tris) b.up(v0 + a, v0 + bb, v0 + c);
|
| 247 |
+
}
|
| 248 |
+
const buildings = meshes(bl, vc({ flatShading: true }), { cast: true, receive: true });
|
| 249 |
+
buildings.name = "buildings";
|
| 250 |
+
root.add(buildings);
|
| 251 |
+
|
| 252 |
+
// track: ballast with sleepers, then the two rails (1435 mm gauge)
|
| 253 |
+
const tex = sleeperTexture();
|
| 254 |
+
const bal = new Map(), rails = new Map();
|
| 255 |
+
const white = new THREE.Color(0xffffff), steel = new THREE.Color(COLORS.rail);
|
| 256 |
+
for (const t of twin.tracks) {
|
| 257 |
+
const pts = decode(t.p);
|
| 258 |
+
addRibbon(bal, pts, t.k ? 2.8 : 3.2, Y.track, white, 1.2);
|
| 259 |
+
for (const off of [-0.7175, 0.7175]) {
|
| 260 |
+
const shifted = offsetLine(pts, off);
|
| 261 |
+
addRibbon(rails, shifted, 0.075, Y.track + 0.17, steel);
|
| 262 |
+
}
|
| 263 |
+
}
|
| 264 |
+
root.add(meshes(bal, flat(new THREE.MeshLambertMaterial({ map: tex, vertexColors: true }), -6), { receive: true }));
|
| 265 |
+
root.add(meshes(rails, flat(new THREE.MeshPhongMaterial({ vertexColors: true, shininess: 90, specular: 0x999999 }), -5)));
|
| 266 |
+
|
| 267 |
+
// platforms: 76 cm high, concrete, with the yellow safety line
|
| 268 |
+
const pl = new Map(), edge = new Map();
|
| 269 |
+
for (const p of twin.platforms) {
|
| 270 |
+
const ring = decode(p.p);
|
| 271 |
+
if (ring.length < 3) continue;
|
| 272 |
+
extrude(pl, ring, 0.76, new THREE.Color(COLORS.platform));
|
| 273 |
+
addRibbon(edge, offsetRing(ring, -0.6), 0.1, 0.78, new THREE.Color(COLORS.platformEdge));
|
| 274 |
+
}
|
| 275 |
+
root.add(meshes(pl, vc(), { receive: true, cast: true }));
|
| 276 |
+
root.add(meshes(edge, vc()));
|
| 277 |
+
|
| 278 |
+
// trees: rows (Frisian alder rows and hedgerows), single trees, woods
|
| 279 |
+
root.add(buildTrees(twin, opts.treeCap ?? 60000));
|
| 280 |
+
root.add(buildLandmarks(twin, opts));
|
| 281 |
+
scene.add(root);
|
| 282 |
+
return root;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
function offsetLine(pts, d) {
|
| 286 |
+
const out = [];
|
| 287 |
+
for (let i = 0; i < pts.length; i++) {
|
| 288 |
+
const a = pts[Math.max(0, i - 1)], b = pts[Math.min(pts.length - 1, i + 1)];
|
| 289 |
+
const dx = b[0] - a[0], dy = b[1] - a[1], L = Math.hypot(dx, dy) || 1;
|
| 290 |
+
out.push([pts[i][0] - dy / L * d, pts[i][1] + dx / L * d]);
|
| 291 |
+
}
|
| 292 |
+
return out;
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
function offsetRing(ring, d) {
|
| 296 |
+
const closed = [...ring, ring[0]];
|
| 297 |
+
return offsetLine(closed, d);
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
function extrude(map, ring, h, c) {
|
| 301 |
+
let cx = 0, cy = 0;
|
| 302 |
+
for (const [x, y] of ring) { cx += x; cy += y; }
|
| 303 |
+
const b = tiled(map, tileKey(cx / ring.length, cy / ring.length));
|
| 304 |
+
let area = 0;
|
| 305 |
+
for (let j = 0; j < ring.length; j++) {
|
| 306 |
+
const [ax, ay] = ring[j], [bx, by] = ring[(j + 1) % ring.length];
|
| 307 |
+
area += ax * by - bx * ay;
|
| 308 |
+
}
|
| 309 |
+
for (let j = 0; j < ring.length; j++) {
|
| 310 |
+
const [ax, ay] = ring[j], [bx, by] = ring[(j + 1) % ring.length];
|
| 311 |
+
const v = b.n;
|
| 312 |
+
b.vert(ax, 0, -ay, c); b.vert(bx, 0, -by, c); b.vert(bx, h, -by, c); b.vert(ax, h, -ay, c);
|
| 313 |
+
if (area > 0) b.idx.push(v, v + 1, v + 2, v, v + 2, v + 3); else b.idx.push(v, v + 2, v + 1, v, v + 3, v + 2);
|
| 314 |
+
}
|
| 315 |
+
let tris;
|
| 316 |
+
try { tris = THREE.ShapeUtils.triangulateShape(ring.map(([x, y]) => new THREE.Vector2(x, y)), []); }
|
| 317 |
+
catch { tris = []; }
|
| 318 |
+
const v0 = b.n;
|
| 319 |
+
for (const [x, y] of ring) b.vert(x, h, -y, c);
|
| 320 |
+
for (const [a, bb, cc] of tris) b.up(v0 + a, v0 + bb, v0 + cc);
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
// deterministic pseudo-random (no Math.random in the scenery: the same twin always looks the same)
|
| 324 |
+
function rng(seed) {
|
| 325 |
+
let s = seed >>> 0;
|
| 326 |
+
return () => ((s = (s * 1664525 + 1013904223) >>> 0) / 4294967296);
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
function treeGeometry() {
|
| 330 |
+
const trunk = new THREE.CylinderGeometry(0.18, 0.25, 3, 4);
|
| 331 |
+
trunk.translate(0, 1.5, 0);
|
| 332 |
+
const crown = new THREE.IcosahedronGeometry(2.6, 0);
|
| 333 |
+
crown.scale(1, 1.25, 1);
|
| 334 |
+
crown.translate(0, 5.2, 0);
|
| 335 |
+
const paint = (g, c) => {
|
| 336 |
+
const n = g.attributes.position.count, arr = new Float32Array(n * 3);
|
| 337 |
+
col.setHex(c);
|
| 338 |
+
for (let i = 0; i < n; i++) arr.set([col.r, col.g, col.b], i * 3);
|
| 339 |
+
g.setAttribute("color", new THREE.BufferAttribute(arr, 3));
|
| 340 |
+
return g;
|
| 341 |
+
};
|
| 342 |
+
const flatGeo = (g) => (g.index ? g.toNonIndexed() : g);
|
| 343 |
+
const a = paint(flatGeo(trunk), COLORS.trunk), b = paint(flatGeo(crown), COLORS.crown);
|
| 344 |
+
const g = new THREE.BufferGeometry();
|
| 345 |
+
for (const name of ["position", "normal", "color"]) {
|
| 346 |
+
const x = a.attributes[name], y = b.attributes[name];
|
| 347 |
+
const arr = new Float32Array(x.array.length + y.array.length);
|
| 348 |
+
arr.set(x.array); arr.set(y.array, x.array.length);
|
| 349 |
+
g.setAttribute(name, new THREE.BufferAttribute(arr, x.itemSize));
|
| 350 |
+
}
|
| 351 |
+
g.computeVertexNormals();
|
| 352 |
+
return g;
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
function buildTrees(twin, cap) {
|
| 356 |
+
const spots = [];
|
| 357 |
+
const r = rng(7);
|
| 358 |
+
for (const row of twin.trees.rows) {
|
| 359 |
+
const pts = decode(row);
|
| 360 |
+
for (let i = 1; i < pts.length; i++) {
|
| 361 |
+
const [ax, ay] = pts[i - 1], [bx, by] = pts[i];
|
| 362 |
+
const L = Math.hypot(bx - ax, by - ay);
|
| 363 |
+
for (let d = r() * 7; d < L; d += 6.5 + r() * 2) spots.push([ax + (bx - ax) * d / L, ay + (by - ay) * d / L]);
|
| 364 |
+
}
|
| 365 |
+
}
|
| 366 |
+
const P = twin.trees.points;
|
| 367 |
+
for (let i = 0; i < P.length; i += 2) spots.push([P[i] / 10, P[i + 1] / 10]);
|
| 368 |
+
for (const p of twin.landcover) { // woods: scattered trees, up to one per 120 m2
|
| 369 |
+
if (p.c !== 7) continue;
|
| 370 |
+
const ring = decode(p.o);
|
| 371 |
+
let minx = Infinity, miny = Infinity, maxx = -Infinity, maxy = -Infinity;
|
| 372 |
+
for (const [x, y] of ring) { minx = Math.min(minx, x); maxx = Math.max(maxx, x); miny = Math.min(miny, y); maxy = Math.max(maxy, y); }
|
| 373 |
+
const step = 11;
|
| 374 |
+
for (let x = minx; x < maxx; x += step) {
|
| 375 |
+
for (let y = miny; y < maxy; y += step) {
|
| 376 |
+
const px = x + r() * step, py = y + r() * step;
|
| 377 |
+
if (inside(px, py, ring)) spots.push([px, py]);
|
| 378 |
+
if (spots.length > cap) break;
|
| 379 |
+
}
|
| 380 |
+
}
|
| 381 |
+
}
|
| 382 |
+
const group = new THREE.Group();
|
| 383 |
+
group.name = "trees";
|
| 384 |
+
const byTile = new Map();
|
| 385 |
+
for (const s of spots.slice(0, cap)) {
|
| 386 |
+
const k = tileKey(s[0], s[1]);
|
| 387 |
+
if (!byTile.has(k)) byTile.set(k, []);
|
| 388 |
+
byTile.get(k).push(s);
|
| 389 |
+
}
|
| 390 |
+
const geo = treeGeometry();
|
| 391 |
+
const mat = new THREE.MeshLambertMaterial({ vertexColors: true, flatShading: true });
|
| 392 |
+
const m4 = new THREE.Matrix4(), q = new THREE.Quaternion(), sc = new THREE.Vector3(), pos = new THREE.Vector3();
|
| 393 |
+
const up = new THREE.Vector3(0, 1, 0);
|
| 394 |
+
for (const list of byTile.values()) {
|
| 395 |
+
const im = new THREE.InstancedMesh(geo, mat, list.length);
|
| 396 |
+
list.forEach(([x, y], j) => {
|
| 397 |
+
const s = 0.7 + r() * 0.6;
|
| 398 |
+
q.setFromAxisAngle(up, r() * Math.PI * 2);
|
| 399 |
+
m4.compose(pos.set(x, 0, -y), q, sc.set(s, s * (0.85 + r() * 0.4), s));
|
| 400 |
+
im.setMatrixAt(j, m4);
|
| 401 |
+
const v = 0.82 + r() * 0.3; // brightness only: keeps green crowns and brown trunks
|
| 402 |
+
im.setColorAt(j, col.setRGB(v, v * (0.98 + r() * 0.06), v));
|
| 403 |
+
});
|
| 404 |
+
im.computeBoundingSphere();
|
| 405 |
+
im.castShadow = true;
|
| 406 |
+
im.userData.centre = new THREE.Vector3(list[0][0], 0, -list[0][1]);
|
| 407 |
+
group.add(im);
|
| 408 |
+
}
|
| 409 |
+
return group;
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
function inside(x, y, ring) {
|
| 413 |
+
let c = false;
|
| 414 |
+
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
| 415 |
+
const [xi, yi] = ring[i], [xj, yj] = ring[j];
|
| 416 |
+
if ((yi > y) !== (yj > y) && x < (xj - xi) * (y - yi) / (yj - yi) + xi) c = !c;
|
| 417 |
+
}
|
| 418 |
+
return c;
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
// windmills (the Frisian polder mills), wind turbines, water towers. Static parts are merged into one mesh per
|
| 422 |
+
// material; blades and sails are instanced and turn in animateWorld().
|
| 423 |
+
function buildLandmarks(twin) {
|
| 424 |
+
const group = new THREE.Group();
|
| 425 |
+
group.name = "landmarks";
|
| 426 |
+
const parts = { white: [], body: [], cap: [], brick: [] };
|
| 427 |
+
const rotors = [];
|
| 428 |
+
const m4 = new THREE.Matrix4(), q = new THREE.Quaternion(), up = new THREE.Vector3(0, 1, 0);
|
| 429 |
+
const put = (kind, geo, x, y, z, yaw = 0) => {
|
| 430 |
+
m4.compose(new THREE.Vector3(x, y, z), q.setFromAxisAngle(up, yaw), new THREE.Vector3(1, 1, 1));
|
| 431 |
+
parts[kind].push((geo.index ? geo.toNonIndexed() : geo).applyMatrix4(m4));
|
| 432 |
+
};
|
| 433 |
+
const YAW = 0.6; // facing the prevailing south-westerly
|
| 434 |
+
for (const m of twin.landmarks) {
|
| 435 |
+
const x = m.x, z = -m.y;
|
| 436 |
+
const dx = Math.cos(YAW), dz = -Math.sin(YAW); // the rotor side, in the world frame
|
| 437 |
+
if (m.k === "turbine") {
|
| 438 |
+
const h = Math.max(40, Math.min(m.h, 140));
|
| 439 |
+
put("white", new THREE.CylinderGeometry(1.2, 2.2, h, 10), x, h / 2, z);
|
| 440 |
+
put("white", new THREE.BoxGeometry(6, 3, 3), x, h + 1, z, YAW);
|
| 441 |
+
rotors.push({ kind: "blade", n: 3, len: h * 0.45, hub: new THREE.Vector3(x + dx * 3.2, h + 1, z + dz * 3.2), speed: 1.6,
|
| 442 |
+
phase: (x * 0.013) % 6.28 });
|
| 443 |
+
} else if (m.k === "mill") {
|
| 444 |
+
const h = Math.max(8, Math.min(m.h, 30));
|
| 445 |
+
put("body", new THREE.CylinderGeometry(2.2, 4.2, h * 0.7, 8), x, h * 0.35, z);
|
| 446 |
+
put("cap", new THREE.ConeGeometry(2.8, h * 0.18, 8), x, h * 0.79, z);
|
| 447 |
+
rotors.push({ kind: "sail", n: 4, len: h * 0.55, hub: new THREE.Vector3(x + dx * 2.8, h * 0.72, z + dz * 2.8), speed: 0.5,
|
| 448 |
+
phase: (x * 0.017) % 6.28 });
|
| 449 |
+
} else if (m.k === "water_tower") {
|
| 450 |
+
const h = Math.max(15, Math.min(m.h, 50));
|
| 451 |
+
put("brick", new THREE.CylinderGeometry(3, 3.5, h * 0.75, 12), x, h * 0.375, z);
|
| 452 |
+
put("brick", new THREE.CylinderGeometry(7, 5, h * 0.25, 16), x, h * 0.875, z);
|
| 453 |
+
} else {
|
| 454 |
+
const h = Math.max(10, Math.min(m.h, 120));
|
| 455 |
+
put("white", new THREE.CylinderGeometry(1.5, 2.5, h, 8), x, h / 2, z);
|
| 456 |
+
}
|
| 457 |
+
}
|
| 458 |
+
const mats = {
|
| 459 |
+
white: new THREE.MeshLambertMaterial({ color: 0xf2f2f0 }),
|
| 460 |
+
body: new THREE.MeshLambertMaterial({ color: 0x4a4038, flatShading: true }),
|
| 461 |
+
cap: new THREE.MeshLambertMaterial({ color: 0x2f3a30, flatShading: true }),
|
| 462 |
+
brick: new THREE.MeshLambertMaterial({ color: 0x8c5a45, flatShading: true }),
|
| 463 |
+
};
|
| 464 |
+
for (const [k, list] of Object.entries(parts)) {
|
| 465 |
+
if (!list.length) continue;
|
| 466 |
+
const mesh = new THREE.Mesh(mergeGeometries(list), mats[k]);
|
| 467 |
+
mesh.castShadow = true;
|
| 468 |
+
group.add(mesh);
|
| 469 |
+
}
|
| 470 |
+
// one instanced mesh per rotor part; unit length along +y from the hub
|
| 471 |
+
const bladeGeo = new THREE.BoxGeometry(0.012, 1, 0.035);
|
| 472 |
+
bladeGeo.translate(0, 0.5, 0);
|
| 473 |
+
const sailGeo = mergeGeometries([new THREE.PlaneGeometry(0.004, 1).translate(0, 0.5, 0).toNonIndexed(),
|
| 474 |
+
new THREE.PlaneGeometry(0.075, 0.78).translate(0, 0.56, 0.035).toNonIndexed()]);
|
| 475 |
+
sailGeo.rotateY(Math.PI / 2);
|
| 476 |
+
const inst = {};
|
| 477 |
+
for (const [kind, geo, mat] of [["blade", bladeGeo, mats.white],
|
| 478 |
+
["sail", sailGeo, new THREE.MeshLambertMaterial({ color: 0xe8e0d0, side: THREE.DoubleSide })]]) {
|
| 479 |
+
const count = rotors.filter((r) => r.kind === kind).reduce((a, r) => a + r.n, 0);
|
| 480 |
+
if (!count) continue;
|
| 481 |
+
const im = new THREE.InstancedMesh(geo, mat, count);
|
| 482 |
+
im.frustumCulled = false;
|
| 483 |
+
group.add(im);
|
| 484 |
+
inst[kind] = im;
|
| 485 |
+
}
|
| 486 |
+
group.userData = { rotors, inst, t: 0 };
|
| 487 |
+
setRotors(group, 0);
|
| 488 |
+
return group;
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
const _m = new THREE.Matrix4(), _r = new THREE.Matrix4(), _y = new THREE.Matrix4(), _s = new THREE.Matrix4();
|
| 492 |
+
|
| 493 |
+
function setRotors(group, t) {
|
| 494 |
+
const { rotors, inst } = group.userData;
|
| 495 |
+
const idx = { blade: 0, sail: 0 };
|
| 496 |
+
_y.makeRotationY(0.6);
|
| 497 |
+
for (const r of rotors) {
|
| 498 |
+
const im = inst[r.kind];
|
| 499 |
+
for (let k = 0; k < r.n; k++) {
|
| 500 |
+
_r.makeRotationX(r.phase + t * r.speed + (k * 2 * Math.PI) / r.n);
|
| 501 |
+
_s.makeScale(r.len, r.len, r.len);
|
| 502 |
+
_m.makeTranslation(r.hub.x, r.hub.y, r.hub.z).multiply(_y).multiply(_r).multiply(_s);
|
| 503 |
+
im.setMatrixAt(idx[r.kind]++, _m);
|
| 504 |
+
}
|
| 505 |
+
}
|
| 506 |
+
for (const im of Object.values(inst)) im.instanceMatrix.needsUpdate = true;
|
| 507 |
+
}
|
| 508 |
+
|
| 509 |
+
export function animateWorld(root, dtReal) {
|
| 510 |
+
const lm = root.getObjectByName("landmarks");
|
| 511 |
+
if (!lm) return;
|
| 512 |
+
lm.userData.t += dtReal;
|
| 513 |
+
setRotors(lm, lm.userData.t);
|
| 514 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
openenv==0.7.0
|
| 2 |
+
fastmcp>=3,<4
|
runs/2026-10-07-sonnet-5-5-eval45/index.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
runs/2026-10-08-deepseek-v4-1-flash-eval45/index.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
runs/2026-10-08-glm-5-3-flash-eval45/index.json
ADDED
|
@@ -0,0 +1,4831 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"run": "2026-10-08-glm-5-3-flash-eval45",
|
| 3 |
+
"episodes": [
|
| 4 |
+
{
|
| 5 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 6 |
+
"task_id": "gnlw-2025-01-20-1800-180m-compound-0",
|
| 7 |
+
"split": "eval",
|
| 8 |
+
"difficulty": "compound",
|
| 9 |
+
"day": "2025-01-20",
|
| 10 |
+
"runs": 24,
|
| 11 |
+
"reward": 0.066667,
|
| 12 |
+
"submitted": true,
|
| 13 |
+
"feasible": false,
|
| 14 |
+
"cost": null,
|
| 15 |
+
"optimal_cost": 664,
|
| 16 |
+
"floor_cost": 539,
|
| 17 |
+
"greedy_cost": 2501,
|
| 18 |
+
"real_cost": 64,
|
| 19 |
+
"hardness": {
|
| 20 |
+
"runs": 24,
|
| 21 |
+
"sets": 6,
|
| 22 |
+
"stops": 147,
|
| 23 |
+
"conflicts": 11,
|
| 24 |
+
"single_track": 5,
|
| 25 |
+
"headway": 3,
|
| 26 |
+
"platforms": 0,
|
| 27 |
+
"closed": 3,
|
| 28 |
+
"decisions": 43,
|
| 29 |
+
"holds": 36,
|
| 30 |
+
"short_turns": 7,
|
| 31 |
+
"cancels": 0,
|
| 32 |
+
"turn_points": 2,
|
| 33 |
+
"avoidable": 125,
|
| 34 |
+
"optimal": 664,
|
| 35 |
+
"greedy_gap": 0.7999,
|
| 36 |
+
"closed_min": 46,
|
| 37 |
+
"slow_min": 0,
|
| 38 |
+
"track_min": 0,
|
| 39 |
+
"single_closed": true,
|
| 40 |
+
"mid_line": true,
|
| 41 |
+
"late_sets": 1
|
| 42 |
+
},
|
| 43 |
+
"violations": 24,
|
| 44 |
+
"agent_turns": 12,
|
| 45 |
+
"checks": 6,
|
| 46 |
+
"seconds": 3979.1,
|
| 47 |
+
"input_tokens": 3090635,
|
| 48 |
+
"cache_read_tokens": 2529856,
|
| 49 |
+
"cache_write_tokens": 0,
|
| 50 |
+
"output_tokens": 500205,
|
| 51 |
+
"served_by": [
|
| 52 |
+
"zai-org/GLM-5.3-Flash"
|
| 53 |
+
],
|
| 54 |
+
"end_reason": "submitted",
|
| 55 |
+
"errors": 0,
|
| 56 |
+
"plan": [
|
| 57 |
+
{
|
| 58 |
+
"train": "37466",
|
| 59 |
+
"to": "HDG"
|
| 60 |
+
},
|
| 61 |
+
{
|
| 62 |
+
"train": "37357",
|
| 63 |
+
"to": "HDG",
|
| 64 |
+
"hold": {
|
| 65 |
+
"FWD": "18:51"
|
| 66 |
+
}
|
| 67 |
+
},
|
| 68 |
+
{
|
| 69 |
+
"train": "37468",
|
| 70 |
+
"from": "HDG"
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
"train": "37459",
|
| 74 |
+
"hold": {
|
| 75 |
+
"FWD": "19:02"
|
| 76 |
+
}
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
"train": "37461",
|
| 80 |
+
"hold": {
|
| 81 |
+
"FWD": "19:05"
|
| 82 |
+
}
|
| 83 |
+
},
|
| 84 |
+
{
|
| 85 |
+
"train": "37359",
|
| 86 |
+
"to": "HDG",
|
| 87 |
+
"hold": {
|
| 88 |
+
"ZH": "18:40",
|
| 89 |
+
"BP": "19:02"
|
| 90 |
+
}
|
| 91 |
+
},
|
| 92 |
+
{
|
| 93 |
+
"train": "37470",
|
| 94 |
+
"from": "HDG"
|
| 95 |
+
},
|
| 96 |
+
{
|
| 97 |
+
"train": "37361",
|
| 98 |
+
"hold": {
|
| 99 |
+
"BP": "19:05",
|
| 100 |
+
"FWD": "19:15"
|
| 101 |
+
}
|
| 102 |
+
},
|
| 103 |
+
{
|
| 104 |
+
"train": "37472",
|
| 105 |
+
"hold": {
|
| 106 |
+
"LW": "19:31"
|
| 107 |
+
}
|
| 108 |
+
},
|
| 109 |
+
{
|
| 110 |
+
"train": "37463",
|
| 111 |
+
"from": "HDG",
|
| 112 |
+
"hold": {
|
| 113 |
+
"HDG": "19:37"
|
| 114 |
+
}
|
| 115 |
+
},
|
| 116 |
+
{
|
| 117 |
+
"train": "37465",
|
| 118 |
+
"hold": {
|
| 119 |
+
"GN": "19:38"
|
| 120 |
+
}
|
| 121 |
+
},
|
| 122 |
+
{
|
| 123 |
+
"train": "37467",
|
| 124 |
+
"hold": {
|
| 125 |
+
"GN": "20:07"
|
| 126 |
+
}
|
| 127 |
+
},
|
| 128 |
+
{
|
| 129 |
+
"train": "37374",
|
| 130 |
+
"hold": {
|
| 131 |
+
"LW": "20:26"
|
| 132 |
+
}
|
| 133 |
+
},
|
| 134 |
+
{
|
| 135 |
+
"train": "37376",
|
| 136 |
+
"hold": {
|
| 137 |
+
"LW": "20:30"
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
]
|
| 141 |
+
},
|
| 142 |
+
{
|
| 143 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 144 |
+
"task_id": "gnlw-2025-01-21-0626-180m-real-0",
|
| 145 |
+
"split": "eval",
|
| 146 |
+
"difficulty": "real",
|
| 147 |
+
"day": "2025-01-21",
|
| 148 |
+
"runs": 29,
|
| 149 |
+
"reward": 0.0,
|
| 150 |
+
"submitted": false,
|
| 151 |
+
"feasible": false,
|
| 152 |
+
"cost": null,
|
| 153 |
+
"optimal_cost": 445,
|
| 154 |
+
"floor_cost": 356,
|
| 155 |
+
"greedy_cost": 1980,
|
| 156 |
+
"real_cost": 1308,
|
| 157 |
+
"hardness": {
|
| 158 |
+
"runs": 29,
|
| 159 |
+
"sets": 7,
|
| 160 |
+
"stops": 199,
|
| 161 |
+
"conflicts": 18,
|
| 162 |
+
"single_track": 14,
|
| 163 |
+
"headway": 4,
|
| 164 |
+
"platforms": 0,
|
| 165 |
+
"closed": 0,
|
| 166 |
+
"decisions": 21,
|
| 167 |
+
"holds": 17,
|
| 168 |
+
"short_turns": 4,
|
| 169 |
+
"cancels": 0,
|
| 170 |
+
"turn_points": 1,
|
| 171 |
+
"avoidable": 89,
|
| 172 |
+
"optimal": 445,
|
| 173 |
+
"greedy_gap": 0.8,
|
| 174 |
+
"closed_min": 0,
|
| 175 |
+
"slow_min": 69,
|
| 176 |
+
"track_min": 0,
|
| 177 |
+
"single_closed": false,
|
| 178 |
+
"mid_line": false,
|
| 179 |
+
"late_sets": 0
|
| 180 |
+
},
|
| 181 |
+
"violations": 0,
|
| 182 |
+
"agent_turns": 4,
|
| 183 |
+
"checks": 2,
|
| 184 |
+
"seconds": 1782.2,
|
| 185 |
+
"input_tokens": 297014,
|
| 186 |
+
"cache_read_tokens": 69888,
|
| 187 |
+
"cache_write_tokens": 0,
|
| 188 |
+
"output_tokens": 192349,
|
| 189 |
+
"served_by": [
|
| 190 |
+
"zai-org/GLM-5.3-Flash"
|
| 191 |
+
],
|
| 192 |
+
"end_reason": "no_tool_call",
|
| 193 |
+
"errors": 0,
|
| 194 |
+
"plan": null
|
| 195 |
+
},
|
| 196 |
+
{
|
| 197 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 198 |
+
"task_id": "gnlw-2025-01-22-1600-240m-compound-0",
|
| 199 |
+
"split": "eval",
|
| 200 |
+
"difficulty": "compound",
|
| 201 |
+
"day": "2025-01-22",
|
| 202 |
+
"runs": 38,
|
| 203 |
+
"reward": 0.0,
|
| 204 |
+
"submitted": false,
|
| 205 |
+
"feasible": false,
|
| 206 |
+
"cost": null,
|
| 207 |
+
"optimal_cost": 670,
|
| 208 |
+
"floor_cost": 627,
|
| 209 |
+
"greedy_cost": 5661,
|
| 210 |
+
"real_cost": 112,
|
| 211 |
+
"hardness": {
|
| 212 |
+
"runs": 38,
|
| 213 |
+
"sets": 7,
|
| 214 |
+
"stops": 252,
|
| 215 |
+
"conflicts": 28,
|
| 216 |
+
"single_track": 7,
|
| 217 |
+
"headway": 10,
|
| 218 |
+
"platforms": 0,
|
| 219 |
+
"closed": 11,
|
| 220 |
+
"decisions": 24,
|
| 221 |
+
"holds": 18,
|
| 222 |
+
"short_turns": 6,
|
| 223 |
+
"cancels": 0,
|
| 224 |
+
"turn_points": 3,
|
| 225 |
+
"avoidable": 43,
|
| 226 |
+
"optimal": 670,
|
| 227 |
+
"greedy_gap": 0.8,
|
| 228 |
+
"closed_min": 36,
|
| 229 |
+
"slow_min": 33,
|
| 230 |
+
"track_min": 0,
|
| 231 |
+
"single_closed": true,
|
| 232 |
+
"mid_line": false,
|
| 233 |
+
"late_sets": 1
|
| 234 |
+
},
|
| 235 |
+
"violations": 0,
|
| 236 |
+
"agent_turns": 4,
|
| 237 |
+
"checks": 1,
|
| 238 |
+
"seconds": 2084.4,
|
| 239 |
+
"input_tokens": 301096,
|
| 240 |
+
"cache_read_tokens": 144000,
|
| 241 |
+
"cache_write_tokens": 0,
|
| 242 |
+
"output_tokens": 192680,
|
| 243 |
+
"served_by": [
|
| 244 |
+
"zai-org/GLM-5.3-Flash"
|
| 245 |
+
],
|
| 246 |
+
"end_reason": "no_tool_call",
|
| 247 |
+
"errors": 0,
|
| 248 |
+
"plan": null
|
| 249 |
+
},
|
| 250 |
+
{
|
| 251 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 252 |
+
"task_id": "gnlw-2025-01-22-1800-150m-loop-0",
|
| 253 |
+
"split": "eval",
|
| 254 |
+
"difficulty": "loop",
|
| 255 |
+
"day": "2025-01-22",
|
| 256 |
+
"runs": 25,
|
| 257 |
+
"reward": 0.0,
|
| 258 |
+
"submitted": false,
|
| 259 |
+
"feasible": false,
|
| 260 |
+
"cost": null,
|
| 261 |
+
"optimal_cost": 397,
|
| 262 |
+
"floor_cost": 336,
|
| 263 |
+
"greedy_cost": 562,
|
| 264 |
+
"real_cost": 79,
|
| 265 |
+
"hardness": {
|
| 266 |
+
"runs": 25,
|
| 267 |
+
"sets": 7,
|
| 268 |
+
"stops": 158,
|
| 269 |
+
"conflicts": 18,
|
| 270 |
+
"single_track": 18,
|
| 271 |
+
"headway": 0,
|
| 272 |
+
"platforms": 0,
|
| 273 |
+
"closed": 0,
|
| 274 |
+
"decisions": 19,
|
| 275 |
+
"holds": 19,
|
| 276 |
+
"short_turns": 0,
|
| 277 |
+
"cancels": 0,
|
| 278 |
+
"turn_points": 0,
|
| 279 |
+
"avoidable": 61,
|
| 280 |
+
"optimal": 397,
|
| 281 |
+
"greedy_gap": 0.697,
|
| 282 |
+
"closed_min": 0,
|
| 283 |
+
"slow_min": 138,
|
| 284 |
+
"track_min": 10,
|
| 285 |
+
"single_closed": false,
|
| 286 |
+
"mid_line": false,
|
| 287 |
+
"late_sets": 0
|
| 288 |
+
},
|
| 289 |
+
"violations": 0,
|
| 290 |
+
"agent_turns": 2,
|
| 291 |
+
"checks": 0,
|
| 292 |
+
"seconds": 1286.8,
|
| 293 |
+
"input_tokens": 74757,
|
| 294 |
+
"cache_read_tokens": 1600,
|
| 295 |
+
"cache_write_tokens": 0,
|
| 296 |
+
"output_tokens": 128000,
|
| 297 |
+
"served_by": [
|
| 298 |
+
"zai-org/GLM-5.3-Flash"
|
| 299 |
+
],
|
| 300 |
+
"end_reason": "no_tool_call",
|
| 301 |
+
"errors": 0,
|
| 302 |
+
"plan": null
|
| 303 |
+
},
|
| 304 |
+
{
|
| 305 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 306 |
+
"task_id": "gnlw-2025-01-23-0800-210m-compound-0",
|
| 307 |
+
"split": "eval",
|
| 308 |
+
"difficulty": "compound",
|
| 309 |
+
"day": "2025-01-23",
|
| 310 |
+
"runs": 34,
|
| 311 |
+
"reward": 0.1,
|
| 312 |
+
"submitted": true,
|
| 313 |
+
"feasible": false,
|
| 314 |
+
"cost": null,
|
| 315 |
+
"optimal_cost": 894,
|
| 316 |
+
"floor_cost": 841,
|
| 317 |
+
"greedy_cost": 4449,
|
| 318 |
+
"real_cost": 83,
|
| 319 |
+
"hardness": {
|
| 320 |
+
"runs": 34,
|
| 321 |
+
"sets": 7,
|
| 322 |
+
"stops": 226,
|
| 323 |
+
"conflicts": 51,
|
| 324 |
+
"single_track": 17,
|
| 325 |
+
"headway": 20,
|
| 326 |
+
"platforms": 0,
|
| 327 |
+
"closed": 14,
|
| 328 |
+
"decisions": 29,
|
| 329 |
+
"holds": 18,
|
| 330 |
+
"short_turns": 11,
|
| 331 |
+
"cancels": 0,
|
| 332 |
+
"turn_points": 3,
|
| 333 |
+
"avoidable": 53,
|
| 334 |
+
"optimal": 894,
|
| 335 |
+
"greedy_gap": 0.8,
|
| 336 |
+
"closed_min": 24,
|
| 337 |
+
"slow_min": 69,
|
| 338 |
+
"track_min": 0,
|
| 339 |
+
"single_closed": true,
|
| 340 |
+
"mid_line": false,
|
| 341 |
+
"late_sets": 1
|
| 342 |
+
},
|
| 343 |
+
"violations": 26,
|
| 344 |
+
"agent_turns": 12,
|
| 345 |
+
"checks": 8,
|
| 346 |
+
"seconds": 2844.5,
|
| 347 |
+
"input_tokens": 2416702,
|
| 348 |
+
"cache_read_tokens": 1687296,
|
| 349 |
+
"cache_write_tokens": 0,
|
| 350 |
+
"output_tokens": 350218,
|
| 351 |
+
"served_by": [
|
| 352 |
+
"zai-org/GLM-5.3-Flash"
|
| 353 |
+
],
|
| 354 |
+
"end_reason": "submitted",
|
| 355 |
+
"errors": 0,
|
| 356 |
+
"plan": [
|
| 357 |
+
{
|
| 358 |
+
"hold": {
|
| 359 |
+
"HDG": "08:01"
|
| 360 |
+
},
|
| 361 |
+
"train": "37426"
|
| 362 |
+
},
|
| 363 |
+
{
|
| 364 |
+
"hold": {
|
| 365 |
+
"FWD": "08:04"
|
| 366 |
+
},
|
| 367 |
+
"train": "37317"
|
| 368 |
+
},
|
| 369 |
+
{
|
| 370 |
+
"hold": {
|
| 371 |
+
"LWC": "08:44"
|
| 372 |
+
},
|
| 373 |
+
"to": "BP",
|
| 374 |
+
"train": "37328"
|
| 375 |
+
},
|
| 376 |
+
{
|
| 377 |
+
"hold": {
|
| 378 |
+
"LW": "08:44",
|
| 379 |
+
"LWC": "08:48",
|
| 380 |
+
"GK": "09:14"
|
| 381 |
+
},
|
| 382 |
+
"train": "37428"
|
| 383 |
+
},
|
| 384 |
+
{
|
| 385 |
+
"hold": {
|
| 386 |
+
"BP": "08:44",
|
| 387 |
+
"FWD": "08:55"
|
| 388 |
+
},
|
| 389 |
+
"train": "37419"
|
| 390 |
+
},
|
| 391 |
+
{
|
| 392 |
+
"hold": {
|
| 393 |
+
"BP": "08:48",
|
| 394 |
+
"FWD": "08:58"
|
| 395 |
+
},
|
| 396 |
+
"train": "37421"
|
| 397 |
+
},
|
| 398 |
+
{
|
| 399 |
+
"hold": {
|
| 400 |
+
"HDG": "09:25"
|
| 401 |
+
},
|
| 402 |
+
"train": "37319"
|
| 403 |
+
},
|
| 404 |
+
{
|
| 405 |
+
"hold": {
|
| 406 |
+
"GN": "08:45",
|
| 407 |
+
"FWD": "09:18",
|
| 408 |
+
"HDG": "09:22"
|
| 409 |
+
},
|
| 410 |
+
"train": "37321"
|
| 411 |
+
},
|
| 412 |
+
{
|
| 413 |
+
"hold": {
|
| 414 |
+
"FWD": "09:20"
|
| 415 |
+
},
|
| 416 |
+
"to": "ZH",
|
| 417 |
+
"train": "37330"
|
| 418 |
+
},
|
| 419 |
+
{
|
| 420 |
+
"hold": {
|
| 421 |
+
"ZH": "09:50",
|
| 422 |
+
"HDG": "10:15"
|
| 423 |
+
},
|
| 424 |
+
"train": "37425"
|
| 425 |
+
},
|
| 426 |
+
{
|
| 427 |
+
"hold": {
|
| 428 |
+
"LW": "09:35",
|
| 429 |
+
"GK": "10:01"
|
| 430 |
+
},
|
| 431 |
+
"to": "ZH",
|
| 432 |
+
"train": "37430"
|
| 433 |
+
},
|
| 434 |
+
{
|
| 435 |
+
"hold": {
|
| 436 |
+
"LW": "09:58"
|
| 437 |
+
},
|
| 438 |
+
"train": "37334"
|
| 439 |
+
},
|
| 440 |
+
{
|
| 441 |
+
"hold": {
|
| 442 |
+
"LW": "09:59"
|
| 443 |
+
},
|
| 444 |
+
"train": "37434"
|
| 445 |
+
},
|
| 446 |
+
{
|
| 447 |
+
"from": "ZH",
|
| 448 |
+
"hold": {
|
| 449 |
+
"ZH": "10:10"
|
| 450 |
+
},
|
| 451 |
+
"train": "37427"
|
| 452 |
+
},
|
| 453 |
+
{
|
| 454 |
+
"hold": {
|
| 455 |
+
"HDG": "09:44"
|
| 456 |
+
},
|
| 457 |
+
"train": "37423"
|
| 458 |
+
},
|
| 459 |
+
{
|
| 460 |
+
"from": "BP",
|
| 461 |
+
"hold": {
|
| 462 |
+
"BP": "09:30",
|
| 463 |
+
"HDG": "09:41"
|
| 464 |
+
},
|
| 465 |
+
"train": "37323"
|
| 466 |
+
},
|
| 467 |
+
{
|
| 468 |
+
"hold": {
|
| 469 |
+
"HDG": "09:40",
|
| 470 |
+
"GK": "10:04"
|
| 471 |
+
},
|
| 472 |
+
"train": "37432"
|
| 473 |
+
},
|
| 474 |
+
{
|
| 475 |
+
"hold": {
|
| 476 |
+
"GK": "10:52"
|
| 477 |
+
},
|
| 478 |
+
"train": "37336"
|
| 479 |
+
},
|
| 480 |
+
{
|
| 481 |
+
"hold": {
|
| 482 |
+
"HDG": "10:52"
|
| 483 |
+
},
|
| 484 |
+
"train": "37327"
|
| 485 |
+
},
|
| 486 |
+
{
|
| 487 |
+
"hold": {
|
| 488 |
+
"HDG": "11:11"
|
| 489 |
+
},
|
| 490 |
+
"train": "37429"
|
| 491 |
+
},
|
| 492 |
+
{
|
| 493 |
+
"hold": {
|
| 494 |
+
"FWD": "11:05"
|
| 495 |
+
},
|
| 496 |
+
"train": "37329"
|
| 497 |
+
},
|
| 498 |
+
{
|
| 499 |
+
"hold": {
|
| 500 |
+
"HDG": "11:36"
|
| 501 |
+
},
|
| 502 |
+
"train": "37431"
|
| 503 |
+
},
|
| 504 |
+
{
|
| 505 |
+
"hold": {
|
| 506 |
+
"FWD": "11:41"
|
| 507 |
+
},
|
| 508 |
+
"train": "37331"
|
| 509 |
+
},
|
| 510 |
+
{
|
| 511 |
+
"hold": {
|
| 512 |
+
"GN": "11:27"
|
| 513 |
+
},
|
| 514 |
+
"train": "37433"
|
| 515 |
+
},
|
| 516 |
+
{
|
| 517 |
+
"hold": {
|
| 518 |
+
"LW": "11:27",
|
| 519 |
+
"DWE": "11:44"
|
| 520 |
+
},
|
| 521 |
+
"train": "37440"
|
| 522 |
+
}
|
| 523 |
+
]
|
| 524 |
+
},
|
| 525 |
+
{
|
| 526 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 527 |
+
"task_id": "gnlw-2025-01-23-0900-180m-compound-0",
|
| 528 |
+
"split": "eval",
|
| 529 |
+
"difficulty": "compound",
|
| 530 |
+
"day": "2025-01-23",
|
| 531 |
+
"runs": 30,
|
| 532 |
+
"reward": 0.236904,
|
| 533 |
+
"submitted": true,
|
| 534 |
+
"feasible": true,
|
| 535 |
+
"cost": 1236,
|
| 536 |
+
"optimal_cost": 853,
|
| 537 |
+
"floor_cost": 704,
|
| 538 |
+
"greedy_cost": 3025,
|
| 539 |
+
"real_cost": 61,
|
| 540 |
+
"hardness": {
|
| 541 |
+
"runs": 30,
|
| 542 |
+
"sets": 7,
|
| 543 |
+
"stops": 200,
|
| 544 |
+
"conflicts": 41,
|
| 545 |
+
"single_track": 29,
|
| 546 |
+
"headway": 5,
|
| 547 |
+
"platforms": 0,
|
| 548 |
+
"closed": 7,
|
| 549 |
+
"decisions": 31,
|
| 550 |
+
"holds": 17,
|
| 551 |
+
"short_turns": 14,
|
| 552 |
+
"cancels": 0,
|
| 553 |
+
"turn_points": 2,
|
| 554 |
+
"avoidable": 149,
|
| 555 |
+
"optimal": 853,
|
| 556 |
+
"greedy_gap": 0.8,
|
| 557 |
+
"closed_min": 23,
|
| 558 |
+
"slow_min": 150,
|
| 559 |
+
"track_min": 10,
|
| 560 |
+
"single_closed": true,
|
| 561 |
+
"mid_line": false,
|
| 562 |
+
"late_sets": 1
|
| 563 |
+
},
|
| 564 |
+
"violations": 0,
|
| 565 |
+
"agent_turns": 12,
|
| 566 |
+
"checks": 7,
|
| 567 |
+
"seconds": 3393.6,
|
| 568 |
+
"input_tokens": 2461633,
|
| 569 |
+
"cache_read_tokens": 1489088,
|
| 570 |
+
"cache_write_tokens": 0,
|
| 571 |
+
"output_tokens": 345648,
|
| 572 |
+
"served_by": [
|
| 573 |
+
"zai-org/GLM-5.3-Flash"
|
| 574 |
+
],
|
| 575 |
+
"end_reason": "submitted",
|
| 576 |
+
"errors": 0,
|
| 577 |
+
"plan": [
|
| 578 |
+
{
|
| 579 |
+
"hold": {
|
| 580 |
+
"LWC": "09:28"
|
| 581 |
+
},
|
| 582 |
+
"train": "37421"
|
| 583 |
+
},
|
| 584 |
+
{
|
| 585 |
+
"to": "HDG",
|
| 586 |
+
"train": "37321"
|
| 587 |
+
},
|
| 588 |
+
{
|
| 589 |
+
"from": "HDG",
|
| 590 |
+
"train": "37432"
|
| 591 |
+
},
|
| 592 |
+
{
|
| 593 |
+
"hold": {
|
| 594 |
+
"FWD": "09:49"
|
| 595 |
+
},
|
| 596 |
+
"to": "HDG",
|
| 597 |
+
"train": "37323"
|
| 598 |
+
},
|
| 599 |
+
{
|
| 600 |
+
"hold": {
|
| 601 |
+
"LW": "09:40"
|
| 602 |
+
},
|
| 603 |
+
"to": "BP",
|
| 604 |
+
"train": "37332"
|
| 605 |
+
},
|
| 606 |
+
{
|
| 607 |
+
"from": "HDG",
|
| 608 |
+
"train": "37434"
|
| 609 |
+
},
|
| 610 |
+
{
|
| 611 |
+
"hold": {
|
| 612 |
+
"GN": "09:27"
|
| 613 |
+
},
|
| 614 |
+
"train": "37425"
|
| 615 |
+
},
|
| 616 |
+
{
|
| 617 |
+
"hold": {
|
| 618 |
+
"ZH": "09:57"
|
| 619 |
+
},
|
| 620 |
+
"train": "37325"
|
| 621 |
+
},
|
| 622 |
+
{
|
| 623 |
+
"hold": {
|
| 624 |
+
"GK": "10:47",
|
| 625 |
+
"LW": "10:24"
|
| 626 |
+
},
|
| 627 |
+
"train": "37336"
|
| 628 |
+
},
|
| 629 |
+
{
|
| 630 |
+
"hold": {
|
| 631 |
+
"FWD": "10:32",
|
| 632 |
+
"ZH": "10:11"
|
| 633 |
+
},
|
| 634 |
+
"train": "37427"
|
| 635 |
+
},
|
| 636 |
+
{
|
| 637 |
+
"hold": {
|
| 638 |
+
"HDG": "10:37"
|
| 639 |
+
},
|
| 640 |
+
"train": "37436"
|
| 641 |
+
},
|
| 642 |
+
{
|
| 643 |
+
"from": "BP",
|
| 644 |
+
"hold": {
|
| 645 |
+
"BP": "10:33"
|
| 646 |
+
},
|
| 647 |
+
"train": "37327"
|
| 648 |
+
},
|
| 649 |
+
{
|
| 650 |
+
"hold": {
|
| 651 |
+
"FWD": "11:05",
|
| 652 |
+
"GN": "10:27"
|
| 653 |
+
},
|
| 654 |
+
"train": "37429"
|
| 655 |
+
},
|
| 656 |
+
{
|
| 657 |
+
"hold": {
|
| 658 |
+
"GN": "10:43",
|
| 659 |
+
"ZH": "11:00"
|
| 660 |
+
},
|
| 661 |
+
"train": "37329"
|
| 662 |
+
},
|
| 663 |
+
{
|
| 664 |
+
"hold": {
|
| 665 |
+
"LW": "11:28"
|
| 666 |
+
},
|
| 667 |
+
"train": "37340"
|
| 668 |
+
},
|
| 669 |
+
{
|
| 670 |
+
"hold": {
|
| 671 |
+
"FWD": "11:31",
|
| 672 |
+
"HDG": "11:38",
|
| 673 |
+
"ZH": "11:11"
|
| 674 |
+
},
|
| 675 |
+
"train": "37431"
|
| 676 |
+
},
|
| 677 |
+
{
|
| 678 |
+
"hold": {
|
| 679 |
+
"GK": "12:00",
|
| 680 |
+
"HDG": "11:38",
|
| 681 |
+
"LW": "11:31"
|
| 682 |
+
},
|
| 683 |
+
"train": "37440"
|
| 684 |
+
},
|
| 685 |
+
{
|
| 686 |
+
"hold": {
|
| 687 |
+
"GK": "11:15",
|
| 688 |
+
"LW": "10:48",
|
| 689 |
+
"ZH": "11:20"
|
| 690 |
+
},
|
| 691 |
+
"train": "37338"
|
| 692 |
+
},
|
| 693 |
+
{
|
| 694 |
+
"hold": {
|
| 695 |
+
"GN": "11:16"
|
| 696 |
+
},
|
| 697 |
+
"train": "37331"
|
| 698 |
+
},
|
| 699 |
+
{
|
| 700 |
+
"hold": {
|
| 701 |
+
"FWD": "12:04"
|
| 702 |
+
},
|
| 703 |
+
"train": "37433"
|
| 704 |
+
},
|
| 705 |
+
{
|
| 706 |
+
"hold": {
|
| 707 |
+
"LW": "11:56"
|
| 708 |
+
},
|
| 709 |
+
"train": "37342"
|
| 710 |
+
},
|
| 711 |
+
{
|
| 712 |
+
"hold": {
|
| 713 |
+
"HDG": "12:07"
|
| 714 |
+
},
|
| 715 |
+
"train": "37442"
|
| 716 |
+
},
|
| 717 |
+
{
|
| 718 |
+
"hold": {
|
| 719 |
+
"ZH": "11:36"
|
| 720 |
+
},
|
| 721 |
+
"train": "37438"
|
| 722 |
+
}
|
| 723 |
+
]
|
| 724 |
+
},
|
| 725 |
+
{
|
| 726 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 727 |
+
"task_id": "gnlw-2025-01-25-0630-180m-loop-0",
|
| 728 |
+
"split": "eval",
|
| 729 |
+
"difficulty": "loop",
|
| 730 |
+
"day": "2025-01-25",
|
| 731 |
+
"runs": 20,
|
| 732 |
+
"reward": 1.0,
|
| 733 |
+
"submitted": true,
|
| 734 |
+
"feasible": true,
|
| 735 |
+
"cost": 116,
|
| 736 |
+
"optimal_cost": 116,
|
| 737 |
+
"floor_cost": 102,
|
| 738 |
+
"greedy_cost": 518,
|
| 739 |
+
"real_cost": 62,
|
| 740 |
+
"hardness": {
|
| 741 |
+
"runs": 20,
|
| 742 |
+
"sets": 7,
|
| 743 |
+
"stops": 150,
|
| 744 |
+
"conflicts": 3,
|
| 745 |
+
"single_track": 3,
|
| 746 |
+
"headway": 0,
|
| 747 |
+
"platforms": 0,
|
| 748 |
+
"closed": 0,
|
| 749 |
+
"decisions": 23,
|
| 750 |
+
"holds": 23,
|
| 751 |
+
"short_turns": 0,
|
| 752 |
+
"cancels": 0,
|
| 753 |
+
"turn_points": 0,
|
| 754 |
+
"avoidable": 14,
|
| 755 |
+
"optimal": 116,
|
| 756 |
+
"greedy_gap": 0.7993,
|
| 757 |
+
"closed_min": 0,
|
| 758 |
+
"slow_min": 69,
|
| 759 |
+
"track_min": 10,
|
| 760 |
+
"single_closed": false,
|
| 761 |
+
"mid_line": false,
|
| 762 |
+
"late_sets": 0
|
| 763 |
+
},
|
| 764 |
+
"violations": 0,
|
| 765 |
+
"agent_turns": 6,
|
| 766 |
+
"checks": 3,
|
| 767 |
+
"seconds": 1156.3,
|
| 768 |
+
"input_tokens": 426953,
|
| 769 |
+
"cache_read_tokens": 273536,
|
| 770 |
+
"cache_write_tokens": 0,
|
| 771 |
+
"output_tokens": 123383,
|
| 772 |
+
"served_by": [
|
| 773 |
+
"zai-org/GLM-5.3-Flash"
|
| 774 |
+
],
|
| 775 |
+
"end_reason": "submitted",
|
| 776 |
+
"errors": 0,
|
| 777 |
+
"plan": [
|
| 778 |
+
{
|
| 779 |
+
"hold": {
|
| 780 |
+
"ZH": "08:01"
|
| 781 |
+
},
|
| 782 |
+
"train": "37424"
|
| 783 |
+
},
|
| 784 |
+
{
|
| 785 |
+
"hold": {
|
| 786 |
+
"HDG": "08:03"
|
| 787 |
+
},
|
| 788 |
+
"train": "37426"
|
| 789 |
+
},
|
| 790 |
+
{
|
| 791 |
+
"hold": {
|
| 792 |
+
"HDG": "08:33"
|
| 793 |
+
},
|
| 794 |
+
"train": "37428"
|
| 795 |
+
},
|
| 796 |
+
{
|
| 797 |
+
"hold": {
|
| 798 |
+
"FWD": "08:36"
|
| 799 |
+
},
|
| 800 |
+
"train": "37319"
|
| 801 |
+
}
|
| 802 |
+
]
|
| 803 |
+
},
|
| 804 |
+
{
|
| 805 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 806 |
+
"task_id": "gnlw-2025-01-25-0730-180m-compound-0",
|
| 807 |
+
"split": "eval",
|
| 808 |
+
"difficulty": "compound",
|
| 809 |
+
"day": "2025-01-25",
|
| 810 |
+
"runs": 26,
|
| 811 |
+
"reward": 0.259548,
|
| 812 |
+
"submitted": true,
|
| 813 |
+
"feasible": true,
|
| 814 |
+
"cost": 1541,
|
| 815 |
+
"optimal_cost": 1302,
|
| 816 |
+
"floor_cost": 1218,
|
| 817 |
+
"greedy_cost": null,
|
| 818 |
+
"real_cost": 129,
|
| 819 |
+
"hardness": {
|
| 820 |
+
"runs": 26,
|
| 821 |
+
"sets": 7,
|
| 822 |
+
"stops": 184,
|
| 823 |
+
"conflicts": 55,
|
| 824 |
+
"single_track": 10,
|
| 825 |
+
"headway": 19,
|
| 826 |
+
"platforms": 2,
|
| 827 |
+
"closed": 24,
|
| 828 |
+
"decisions": 26,
|
| 829 |
+
"holds": 11,
|
| 830 |
+
"short_turns": 15,
|
| 831 |
+
"cancels": 0,
|
| 832 |
+
"turn_points": 2,
|
| 833 |
+
"avoidable": 84,
|
| 834 |
+
"optimal": 1302,
|
| 835 |
+
"greedy_gap": 0.8846,
|
| 836 |
+
"closed_min": 111,
|
| 837 |
+
"slow_min": 100,
|
| 838 |
+
"track_min": 0,
|
| 839 |
+
"single_closed": true,
|
| 840 |
+
"mid_line": false,
|
| 841 |
+
"late_sets": 2
|
| 842 |
+
},
|
| 843 |
+
"violations": 0,
|
| 844 |
+
"agent_turns": 9,
|
| 845 |
+
"checks": 6,
|
| 846 |
+
"seconds": 1971.4,
|
| 847 |
+
"input_tokens": 1338672,
|
| 848 |
+
"cache_read_tokens": 812224,
|
| 849 |
+
"cache_write_tokens": 0,
|
| 850 |
+
"output_tokens": 230040,
|
| 851 |
+
"served_by": [
|
| 852 |
+
"zai-org/GLM-5.3-Flash"
|
| 853 |
+
],
|
| 854 |
+
"end_reason": "submitted",
|
| 855 |
+
"errors": 0,
|
| 856 |
+
"plan": [
|
| 857 |
+
{
|
| 858 |
+
"train": "37415",
|
| 859 |
+
"to": "HDG"
|
| 860 |
+
},
|
| 861 |
+
{
|
| 862 |
+
"train": "37417",
|
| 863 |
+
"to": "HDG"
|
| 864 |
+
},
|
| 865 |
+
{
|
| 866 |
+
"train": "37419",
|
| 867 |
+
"to": "HDG"
|
| 868 |
+
},
|
| 869 |
+
{
|
| 870 |
+
"train": "37319",
|
| 871 |
+
"to": "HDG",
|
| 872 |
+
"hold": {
|
| 873 |
+
"ZH": "08:44"
|
| 874 |
+
}
|
| 875 |
+
},
|
| 876 |
+
{
|
| 877 |
+
"train": "37421",
|
| 878 |
+
"to": "HDG",
|
| 879 |
+
"hold": {
|
| 880 |
+
"GN": "08:31",
|
| 881 |
+
"FWD": "09:09"
|
| 882 |
+
}
|
| 883 |
+
},
|
| 884 |
+
{
|
| 885 |
+
"train": "37321",
|
| 886 |
+
"to": "HDG",
|
| 887 |
+
"hold": {
|
| 888 |
+
"GN": "08:43"
|
| 889 |
+
}
|
| 890 |
+
},
|
| 891 |
+
{
|
| 892 |
+
"train": "37426",
|
| 893 |
+
"from": "HDG"
|
| 894 |
+
},
|
| 895 |
+
{
|
| 896 |
+
"train": "37328",
|
| 897 |
+
"from": "HDG"
|
| 898 |
+
},
|
| 899 |
+
{
|
| 900 |
+
"train": "37330",
|
| 901 |
+
"from": "HDG"
|
| 902 |
+
},
|
| 903 |
+
{
|
| 904 |
+
"train": "37430",
|
| 905 |
+
"from": "HDG",
|
| 906 |
+
"hold": {
|
| 907 |
+
"GK": "09:28"
|
| 908 |
+
}
|
| 909 |
+
},
|
| 910 |
+
{
|
| 911 |
+
"train": "37332",
|
| 912 |
+
"from": "HDG"
|
| 913 |
+
},
|
| 914 |
+
{
|
| 915 |
+
"train": "37432",
|
| 916 |
+
"from": "HDG"
|
| 917 |
+
},
|
| 918 |
+
{
|
| 919 |
+
"train": "37428",
|
| 920 |
+
"cancel": true
|
| 921 |
+
},
|
| 922 |
+
{
|
| 923 |
+
"train": "37425",
|
| 924 |
+
"cancel": true
|
| 925 |
+
},
|
| 926 |
+
{
|
| 927 |
+
"train": "37423",
|
| 928 |
+
"hold": {
|
| 929 |
+
"GN": "08:59"
|
| 930 |
+
}
|
| 931 |
+
},
|
| 932 |
+
{
|
| 933 |
+
"train": "37323",
|
| 934 |
+
"hold": {
|
| 935 |
+
"HDG": "09:43"
|
| 936 |
+
}
|
| 937 |
+
},
|
| 938 |
+
{
|
| 939 |
+
"train": "37325",
|
| 940 |
+
"hold": {
|
| 941 |
+
"GN": "09:45",
|
| 942 |
+
"ZH": "09:57"
|
| 943 |
+
}
|
| 944 |
+
},
|
| 945 |
+
{
|
| 946 |
+
"train": "37434",
|
| 947 |
+
"hold": {}
|
| 948 |
+
},
|
| 949 |
+
{
|
| 950 |
+
"train": "37336",
|
| 951 |
+
"hold": {
|
| 952 |
+
"LW": "10:23"
|
| 953 |
+
}
|
| 954 |
+
},
|
| 955 |
+
{
|
| 956 |
+
"train": "37427",
|
| 957 |
+
"hold": {
|
| 958 |
+
"HDG": "10:34"
|
| 959 |
+
}
|
| 960 |
+
},
|
| 961 |
+
{
|
| 962 |
+
"train": "37327",
|
| 963 |
+
"hold": {
|
| 964 |
+
"HDG": "10:38"
|
| 965 |
+
}
|
| 966 |
+
},
|
| 967 |
+
{
|
| 968 |
+
"train": "37436",
|
| 969 |
+
"hold": {
|
| 970 |
+
"HDG": "10:37"
|
| 971 |
+
}
|
| 972 |
+
},
|
| 973 |
+
{
|
| 974 |
+
"train": "37429",
|
| 975 |
+
"hold": {}
|
| 976 |
+
}
|
| 977 |
+
]
|
| 978 |
+
},
|
| 979 |
+
{
|
| 980 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 981 |
+
"task_id": "gnlw-2025-01-25-1700-180m-blockage-0",
|
| 982 |
+
"split": "eval",
|
| 983 |
+
"difficulty": "blockage",
|
| 984 |
+
"day": "2025-01-25",
|
| 985 |
+
"runs": 30,
|
| 986 |
+
"reward": 0.375453,
|
| 987 |
+
"submitted": true,
|
| 988 |
+
"feasible": true,
|
| 989 |
+
"cost": 430,
|
| 990 |
+
"optimal_cost": 320,
|
| 991 |
+
"floor_cost": 275,
|
| 992 |
+
"greedy_cost": 1591,
|
| 993 |
+
"real_cost": 74,
|
| 994 |
+
"hardness": {
|
| 995 |
+
"runs": 30,
|
| 996 |
+
"sets": 7,
|
| 997 |
+
"stops": 200,
|
| 998 |
+
"conflicts": 11,
|
| 999 |
+
"single_track": 0,
|
| 1000 |
+
"headway": 0,
|
| 1001 |
+
"platforms": 0,
|
| 1002 |
+
"closed": 11,
|
| 1003 |
+
"decisions": 51,
|
| 1004 |
+
"holds": 49,
|
| 1005 |
+
"short_turns": 2,
|
| 1006 |
+
"cancels": 0,
|
| 1007 |
+
"turn_points": 1,
|
| 1008 |
+
"avoidable": 45,
|
| 1009 |
+
"optimal": 320,
|
| 1010 |
+
"greedy_gap": 0.8,
|
| 1011 |
+
"closed_min": 15,
|
| 1012 |
+
"slow_min": 0,
|
| 1013 |
+
"track_min": 0,
|
| 1014 |
+
"single_closed": true,
|
| 1015 |
+
"mid_line": false,
|
| 1016 |
+
"late_sets": 0
|
| 1017 |
+
},
|
| 1018 |
+
"violations": 0,
|
| 1019 |
+
"agent_turns": 12,
|
| 1020 |
+
"checks": 8,
|
| 1021 |
+
"seconds": 3447.9,
|
| 1022 |
+
"input_tokens": 2693163,
|
| 1023 |
+
"cache_read_tokens": 2104128,
|
| 1024 |
+
"cache_write_tokens": 0,
|
| 1025 |
+
"output_tokens": 320171,
|
| 1026 |
+
"served_by": [
|
| 1027 |
+
"zai-org/GLM-5.3-Flash"
|
| 1028 |
+
],
|
| 1029 |
+
"end_reason": "submitted",
|
| 1030 |
+
"errors": 0,
|
| 1031 |
+
"plan": [
|
| 1032 |
+
{
|
| 1033 |
+
"train": "37455",
|
| 1034 |
+
"hold": {
|
| 1035 |
+
"HDG": "17:45"
|
| 1036 |
+
}
|
| 1037 |
+
},
|
| 1038 |
+
{
|
| 1039 |
+
"train": "37364",
|
| 1040 |
+
"hold": {
|
| 1041 |
+
"DWE": "17:45"
|
| 1042 |
+
}
|
| 1043 |
+
},
|
| 1044 |
+
{
|
| 1045 |
+
"train": "37464",
|
| 1046 |
+
"hold": {
|
| 1047 |
+
"HDG": "17:45",
|
| 1048 |
+
"GK": "18:05"
|
| 1049 |
+
}
|
| 1050 |
+
},
|
| 1051 |
+
{
|
| 1052 |
+
"train": "37355",
|
| 1053 |
+
"to": "HDG",
|
| 1054 |
+
"hold": {
|
| 1055 |
+
"BP": "17:45"
|
| 1056 |
+
}
|
| 1057 |
+
},
|
| 1058 |
+
{
|
| 1059 |
+
"train": "37466",
|
| 1060 |
+
"from": "HDG"
|
| 1061 |
+
},
|
| 1062 |
+
{
|
| 1063 |
+
"train": "37457",
|
| 1064 |
+
"hold": {
|
| 1065 |
+
"BP": "17:48",
|
| 1066 |
+
"FWD": "18:03"
|
| 1067 |
+
}
|
| 1068 |
+
},
|
| 1069 |
+
{
|
| 1070 |
+
"train": "37357",
|
| 1071 |
+
"hold": {
|
| 1072 |
+
"DWE": "18:02",
|
| 1073 |
+
"FWD": "18:09"
|
| 1074 |
+
}
|
| 1075 |
+
},
|
| 1076 |
+
{
|
| 1077 |
+
"train": "37366",
|
| 1078 |
+
"hold": {
|
| 1079 |
+
"LW": "18:00",
|
| 1080 |
+
"FWD": "18:16"
|
| 1081 |
+
}
|
| 1082 |
+
},
|
| 1083 |
+
{
|
| 1084 |
+
"train": "37368",
|
| 1085 |
+
"hold": {
|
| 1086 |
+
"LW": "18:20",
|
| 1087 |
+
"HDG": "18:29"
|
| 1088 |
+
}
|
| 1089 |
+
},
|
| 1090 |
+
{
|
| 1091 |
+
"train": "37468",
|
| 1092 |
+
"hold": {
|
| 1093 |
+
"LW": "18:25",
|
| 1094 |
+
"FWD": "18:39"
|
| 1095 |
+
}
|
| 1096 |
+
},
|
| 1097 |
+
{
|
| 1098 |
+
"train": "37459",
|
| 1099 |
+
"hold": {
|
| 1100 |
+
"HDG": "18:33"
|
| 1101 |
+
}
|
| 1102 |
+
},
|
| 1103 |
+
{
|
| 1104 |
+
"train": "37359",
|
| 1105 |
+
"hold": {
|
| 1106 |
+
"DWE": "18:36"
|
| 1107 |
+
}
|
| 1108 |
+
},
|
| 1109 |
+
{
|
| 1110 |
+
"train": "37370",
|
| 1111 |
+
"hold": {
|
| 1112 |
+
"LW": "18:48"
|
| 1113 |
+
}
|
| 1114 |
+
},
|
| 1115 |
+
{
|
| 1116 |
+
"train": "37461",
|
| 1117 |
+
"hold": {
|
| 1118 |
+
"FWD": "18:56"
|
| 1119 |
+
}
|
| 1120 |
+
},
|
| 1121 |
+
{
|
| 1122 |
+
"train": "37470",
|
| 1123 |
+
"hold": {
|
| 1124 |
+
"HDG": "19:00"
|
| 1125 |
+
}
|
| 1126 |
+
},
|
| 1127 |
+
{
|
| 1128 |
+
"train": "37463",
|
| 1129 |
+
"hold": {
|
| 1130 |
+
"GN": "18:56"
|
| 1131 |
+
}
|
| 1132 |
+
},
|
| 1133 |
+
{
|
| 1134 |
+
"train": "37363",
|
| 1135 |
+
"hold": {
|
| 1136 |
+
"FWD": "19:39"
|
| 1137 |
+
}
|
| 1138 |
+
},
|
| 1139 |
+
{
|
| 1140 |
+
"train": "37472",
|
| 1141 |
+
"hold": {
|
| 1142 |
+
"HDG": "19:30"
|
| 1143 |
+
}
|
| 1144 |
+
},
|
| 1145 |
+
{
|
| 1146 |
+
"train": "37465",
|
| 1147 |
+
"hold": {
|
| 1148 |
+
"FWD": "20:00"
|
| 1149 |
+
}
|
| 1150 |
+
},
|
| 1151 |
+
{
|
| 1152 |
+
"train": "37474",
|
| 1153 |
+
"hold": {
|
| 1154 |
+
"HDG": "20:03"
|
| 1155 |
+
}
|
| 1156 |
+
},
|
| 1157 |
+
{
|
| 1158 |
+
"train": "37374",
|
| 1159 |
+
"hold": {
|
| 1160 |
+
"LW": "19:47"
|
| 1161 |
+
}
|
| 1162 |
+
},
|
| 1163 |
+
{
|
| 1164 |
+
"train": "37365",
|
| 1165 |
+
"hold": {
|
| 1166 |
+
"FWD": "20:06"
|
| 1167 |
+
}
|
| 1168 |
+
}
|
| 1169 |
+
]
|
| 1170 |
+
},
|
| 1171 |
+
{
|
| 1172 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 1173 |
+
"task_id": "gnlw-2025-01-26-1700-210m-compound-0",
|
| 1174 |
+
"split": "eval",
|
| 1175 |
+
"difficulty": "compound",
|
| 1176 |
+
"day": "2025-01-26",
|
| 1177 |
+
"runs": 16,
|
| 1178 |
+
"reward": 0.559463,
|
| 1179 |
+
"submitted": true,
|
| 1180 |
+
"feasible": true,
|
| 1181 |
+
"cost": 670,
|
| 1182 |
+
"optimal_cost": 618,
|
| 1183 |
+
"floor_cost": 588,
|
| 1184 |
+
"greedy_cost": 928,
|
| 1185 |
+
"real_cost": 58,
|
| 1186 |
+
"hardness": {
|
| 1187 |
+
"runs": 16,
|
| 1188 |
+
"sets": 3,
|
| 1189 |
+
"stops": 104,
|
| 1190 |
+
"conflicts": 10,
|
| 1191 |
+
"single_track": 3,
|
| 1192 |
+
"headway": 0,
|
| 1193 |
+
"platforms": 0,
|
| 1194 |
+
"closed": 7,
|
| 1195 |
+
"decisions": 21,
|
| 1196 |
+
"holds": 12,
|
| 1197 |
+
"short_turns": 9,
|
| 1198 |
+
"cancels": 0,
|
| 1199 |
+
"turn_points": 2,
|
| 1200 |
+
"avoidable": 30,
|
| 1201 |
+
"optimal": 618,
|
| 1202 |
+
"greedy_gap": 0.7932,
|
| 1203 |
+
"closed_min": 104,
|
| 1204 |
+
"slow_min": 100,
|
| 1205 |
+
"track_min": 0,
|
| 1206 |
+
"single_closed": true,
|
| 1207 |
+
"mid_line": true,
|
| 1208 |
+
"late_sets": 0
|
| 1209 |
+
},
|
| 1210 |
+
"violations": 0,
|
| 1211 |
+
"agent_turns": 6,
|
| 1212 |
+
"checks": 4,
|
| 1213 |
+
"seconds": 1264.8,
|
| 1214 |
+
"input_tokens": 495339,
|
| 1215 |
+
"cache_read_tokens": 354624,
|
| 1216 |
+
"cache_write_tokens": 0,
|
| 1217 |
+
"output_tokens": 139962,
|
| 1218 |
+
"served_by": [
|
| 1219 |
+
"zai-org/GLM-5.3-Flash"
|
| 1220 |
+
],
|
| 1221 |
+
"end_reason": "submitted",
|
| 1222 |
+
"errors": 0,
|
| 1223 |
+
"plan": [
|
| 1224 |
+
{
|
| 1225 |
+
"train": "37353",
|
| 1226 |
+
"to": "HDG"
|
| 1227 |
+
},
|
| 1228 |
+
{
|
| 1229 |
+
"train": "37364",
|
| 1230 |
+
"from": "HDG"
|
| 1231 |
+
},
|
| 1232 |
+
{
|
| 1233 |
+
"train": "37455",
|
| 1234 |
+
"to": "HDG"
|
| 1235 |
+
},
|
| 1236 |
+
{
|
| 1237 |
+
"train": "37466",
|
| 1238 |
+
"from": "HDG"
|
| 1239 |
+
},
|
| 1240 |
+
{
|
| 1241 |
+
"train": "37459",
|
| 1242 |
+
"to": "HDG"
|
| 1243 |
+
},
|
| 1244 |
+
{
|
| 1245 |
+
"train": "37470",
|
| 1246 |
+
"from": "HDG"
|
| 1247 |
+
},
|
| 1248 |
+
{
|
| 1249 |
+
"train": "37357",
|
| 1250 |
+
"to": "HDG",
|
| 1251 |
+
"hold": {
|
| 1252 |
+
"GN": "17:46"
|
| 1253 |
+
}
|
| 1254 |
+
},
|
| 1255 |
+
{
|
| 1256 |
+
"train": "37368",
|
| 1257 |
+
"from": "HDG"
|
| 1258 |
+
},
|
| 1259 |
+
{
|
| 1260 |
+
"train": "37361",
|
| 1261 |
+
"hold": {
|
| 1262 |
+
"GN": "18:46"
|
| 1263 |
+
}
|
| 1264 |
+
},
|
| 1265 |
+
{
|
| 1266 |
+
"train": "37463",
|
| 1267 |
+
"hold": {
|
| 1268 |
+
"HDG": "19:36"
|
| 1269 |
+
}
|
| 1270 |
+
}
|
| 1271 |
+
]
|
| 1272 |
+
},
|
| 1273 |
+
{
|
| 1274 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 1275 |
+
"task_id": "gnlw-2025-02-11-0900-150m-blockage-0",
|
| 1276 |
+
"split": "eval",
|
| 1277 |
+
"difficulty": "blockage",
|
| 1278 |
+
"day": "2025-02-11",
|
| 1279 |
+
"runs": 23,
|
| 1280 |
+
"reward": 1.0,
|
| 1281 |
+
"submitted": true,
|
| 1282 |
+
"feasible": true,
|
| 1283 |
+
"cost": 725,
|
| 1284 |
+
"optimal_cost": 725,
|
| 1285 |
+
"floor_cost": 721,
|
| 1286 |
+
"greedy_cost": null,
|
| 1287 |
+
"real_cost": 318,
|
| 1288 |
+
"hardness": {
|
| 1289 |
+
"runs": 23,
|
| 1290 |
+
"sets": 6,
|
| 1291 |
+
"stops": 152,
|
| 1292 |
+
"conflicts": 32,
|
| 1293 |
+
"single_track": 3,
|
| 1294 |
+
"headway": 0,
|
| 1295 |
+
"platforms": 0,
|
| 1296 |
+
"closed": 29,
|
| 1297 |
+
"decisions": 37,
|
| 1298 |
+
"holds": 23,
|
| 1299 |
+
"short_turns": 14,
|
| 1300 |
+
"cancels": 0,
|
| 1301 |
+
"turn_points": 1,
|
| 1302 |
+
"avoidable": 4,
|
| 1303 |
+
"optimal": 725,
|
| 1304 |
+
"greedy_gap": 0.8174,
|
| 1305 |
+
"closed_min": 120,
|
| 1306 |
+
"slow_min": 0,
|
| 1307 |
+
"track_min": 0,
|
| 1308 |
+
"single_closed": true,
|
| 1309 |
+
"mid_line": false,
|
| 1310 |
+
"late_sets": 0
|
| 1311 |
+
},
|
| 1312 |
+
"violations": 0,
|
| 1313 |
+
"agent_turns": 4,
|
| 1314 |
+
"checks": 2,
|
| 1315 |
+
"seconds": 899.7,
|
| 1316 |
+
"input_tokens": 242522,
|
| 1317 |
+
"cache_read_tokens": 147520,
|
| 1318 |
+
"cache_write_tokens": 0,
|
| 1319 |
+
"output_tokens": 92904,
|
| 1320 |
+
"served_by": [
|
| 1321 |
+
"zai-org/GLM-5.3-Flash"
|
| 1322 |
+
],
|
| 1323 |
+
"end_reason": "submitted",
|
| 1324 |
+
"errors": 0,
|
| 1325 |
+
"plan": [
|
| 1326 |
+
{
|
| 1327 |
+
"train": "37421",
|
| 1328 |
+
"to": "HDG"
|
| 1329 |
+
},
|
| 1330 |
+
{
|
| 1331 |
+
"train": "37332",
|
| 1332 |
+
"from": "HDG",
|
| 1333 |
+
"hold": {
|
| 1334 |
+
"HDG": "09:21"
|
| 1335 |
+
}
|
| 1336 |
+
},
|
| 1337 |
+
{
|
| 1338 |
+
"train": "37321",
|
| 1339 |
+
"hold": {
|
| 1340 |
+
"FWD": "09:07"
|
| 1341 |
+
},
|
| 1342 |
+
"to": "HDG"
|
| 1343 |
+
},
|
| 1344 |
+
{
|
| 1345 |
+
"train": "37432",
|
| 1346 |
+
"from": "HDG"
|
| 1347 |
+
},
|
| 1348 |
+
{
|
| 1349 |
+
"train": "37428",
|
| 1350 |
+
"hold": {
|
| 1351 |
+
"ZH": "09:05"
|
| 1352 |
+
}
|
| 1353 |
+
},
|
| 1354 |
+
{
|
| 1355 |
+
"train": "37323",
|
| 1356 |
+
"to": "HDG"
|
| 1357 |
+
},
|
| 1358 |
+
{
|
| 1359 |
+
"train": "37434",
|
| 1360 |
+
"from": "HDG"
|
| 1361 |
+
},
|
| 1362 |
+
{
|
| 1363 |
+
"train": "37425",
|
| 1364 |
+
"to": "HDG"
|
| 1365 |
+
},
|
| 1366 |
+
{
|
| 1367 |
+
"train": "37336",
|
| 1368 |
+
"from": "HDG",
|
| 1369 |
+
"hold": {
|
| 1370 |
+
"HDG": "10:21"
|
| 1371 |
+
}
|
| 1372 |
+
},
|
| 1373 |
+
{
|
| 1374 |
+
"train": "37325",
|
| 1375 |
+
"to": "HDG"
|
| 1376 |
+
},
|
| 1377 |
+
{
|
| 1378 |
+
"train": "37436",
|
| 1379 |
+
"from": "HDG"
|
| 1380 |
+
},
|
| 1381 |
+
{
|
| 1382 |
+
"train": "37427",
|
| 1383 |
+
"to": "HDG"
|
| 1384 |
+
},
|
| 1385 |
+
{
|
| 1386 |
+
"train": "37338",
|
| 1387 |
+
"from": "HDG",
|
| 1388 |
+
"hold": {
|
| 1389 |
+
"HDG": "10:51"
|
| 1390 |
+
}
|
| 1391 |
+
},
|
| 1392 |
+
{
|
| 1393 |
+
"train": "37327",
|
| 1394 |
+
"to": "HDG"
|
| 1395 |
+
},
|
| 1396 |
+
{
|
| 1397 |
+
"train": "37438",
|
| 1398 |
+
"from": "HDG"
|
| 1399 |
+
},
|
| 1400 |
+
{
|
| 1401 |
+
"train": "37429",
|
| 1402 |
+
"hold": {
|
| 1403 |
+
"HDG": "11:10"
|
| 1404 |
+
}
|
| 1405 |
+
},
|
| 1406 |
+
{
|
| 1407 |
+
"train": "37340",
|
| 1408 |
+
"hold": {
|
| 1409 |
+
"LW": "11:23"
|
| 1410 |
+
}
|
| 1411 |
+
}
|
| 1412 |
+
]
|
| 1413 |
+
},
|
| 1414 |
+
{
|
| 1415 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 1416 |
+
"task_id": "gnlw-2025-02-12-0900-180m-real-0",
|
| 1417 |
+
"split": "eval",
|
| 1418 |
+
"difficulty": "real",
|
| 1419 |
+
"day": "2025-02-12",
|
| 1420 |
+
"runs": 31,
|
| 1421 |
+
"reward": 0.19375,
|
| 1422 |
+
"submitted": true,
|
| 1423 |
+
"feasible": false,
|
| 1424 |
+
"cost": null,
|
| 1425 |
+
"optimal_cost": 643,
|
| 1426 |
+
"floor_cost": 529,
|
| 1427 |
+
"greedy_cost": null,
|
| 1428 |
+
"real_cost": 363,
|
| 1429 |
+
"hardness": {
|
| 1430 |
+
"runs": 31,
|
| 1431 |
+
"sets": 7,
|
| 1432 |
+
"stops": 204,
|
| 1433 |
+
"conflicts": 16,
|
| 1434 |
+
"single_track": 8,
|
| 1435 |
+
"headway": 3,
|
| 1436 |
+
"platforms": 0,
|
| 1437 |
+
"closed": 5,
|
| 1438 |
+
"decisions": 38,
|
| 1439 |
+
"holds": 32,
|
| 1440 |
+
"short_turns": 6,
|
| 1441 |
+
"cancels": 0,
|
| 1442 |
+
"turn_points": 3,
|
| 1443 |
+
"avoidable": 114,
|
| 1444 |
+
"optimal": 643,
|
| 1445 |
+
"greedy_gap": 0.8452,
|
| 1446 |
+
"closed_min": 30,
|
| 1447 |
+
"slow_min": 0,
|
| 1448 |
+
"track_min": 0,
|
| 1449 |
+
"single_closed": false,
|
| 1450 |
+
"mid_line": true,
|
| 1451 |
+
"late_sets": 0
|
| 1452 |
+
},
|
| 1453 |
+
"violations": 0,
|
| 1454 |
+
"agent_turns": 12,
|
| 1455 |
+
"checks": 8,
|
| 1456 |
+
"seconds": 2686.2,
|
| 1457 |
+
"input_tokens": 2649912,
|
| 1458 |
+
"cache_read_tokens": 2186880,
|
| 1459 |
+
"cache_write_tokens": 0,
|
| 1460 |
+
"output_tokens": 344321,
|
| 1461 |
+
"served_by": [
|
| 1462 |
+
"zai-org/GLM-5.3-Flash"
|
| 1463 |
+
],
|
| 1464 |
+
"end_reason": "submitted",
|
| 1465 |
+
"errors": 0,
|
| 1466 |
+
"plan": [
|
| 1467 |
+
{
|
| 1468 |
+
"hold": {
|
| 1469 |
+
"HDG": "09:05"
|
| 1470 |
+
},
|
| 1471 |
+
"train": "37421"
|
| 1472 |
+
},
|
| 1473 |
+
{
|
| 1474 |
+
"hold": {
|
| 1475 |
+
"GK": "09:02"
|
| 1476 |
+
},
|
| 1477 |
+
"train": "37328"
|
| 1478 |
+
},
|
| 1479 |
+
{
|
| 1480 |
+
"hold": {
|
| 1481 |
+
"BP": "09:29",
|
| 1482 |
+
"ZH": "09:07"
|
| 1483 |
+
},
|
| 1484 |
+
"train": "37423"
|
| 1485 |
+
},
|
| 1486 |
+
{
|
| 1487 |
+
"hold": {
|
| 1488 |
+
"HDG": "09:44"
|
| 1489 |
+
},
|
| 1490 |
+
"train": "37323"
|
| 1491 |
+
},
|
| 1492 |
+
{
|
| 1493 |
+
"hold": {
|
| 1494 |
+
"DWE": "09:31"
|
| 1495 |
+
},
|
| 1496 |
+
"to": "HDG",
|
| 1497 |
+
"train": "37321"
|
| 1498 |
+
},
|
| 1499 |
+
{
|
| 1500 |
+
"from": "HDG",
|
| 1501 |
+
"hold": {
|
| 1502 |
+
"GK": "10:05",
|
| 1503 |
+
"HDG": "09:44"
|
| 1504 |
+
},
|
| 1505 |
+
"train": "37432"
|
| 1506 |
+
},
|
| 1507 |
+
{
|
| 1508 |
+
"hold": {
|
| 1509 |
+
"FWD": "09:30"
|
| 1510 |
+
},
|
| 1511 |
+
"to": "ZH",
|
| 1512 |
+
"train": "37330"
|
| 1513 |
+
},
|
| 1514 |
+
{
|
| 1515 |
+
"from": "ZH",
|
| 1516 |
+
"hold": {
|
| 1517 |
+
"FWD": "10:11"
|
| 1518 |
+
},
|
| 1519 |
+
"train": "37325"
|
| 1520 |
+
},
|
| 1521 |
+
{
|
| 1522 |
+
"hold": {
|
| 1523 |
+
"FWD": "09:33",
|
| 1524 |
+
"GK": "09:52"
|
| 1525 |
+
},
|
| 1526 |
+
"to": "ZH",
|
| 1527 |
+
"train": "37430"
|
| 1528 |
+
},
|
| 1529 |
+
{
|
| 1530 |
+
"from": "ZH",
|
| 1531 |
+
"hold": {
|
| 1532 |
+
"FWD": "10:29"
|
| 1533 |
+
},
|
| 1534 |
+
"train": "37427"
|
| 1535 |
+
},
|
| 1536 |
+
{
|
| 1537 |
+
"hold": {
|
| 1538 |
+
"BP": "09:47",
|
| 1539 |
+
"FWD": "09:38",
|
| 1540 |
+
"GK": "09:55",
|
| 1541 |
+
"HDG": "09:29"
|
| 1542 |
+
},
|
| 1543 |
+
"train": "37332"
|
| 1544 |
+
},
|
| 1545 |
+
{
|
| 1546 |
+
"hold": {
|
| 1547 |
+
"LW": "09:52"
|
| 1548 |
+
},
|
| 1549 |
+
"train": "37334"
|
| 1550 |
+
},
|
| 1551 |
+
{
|
| 1552 |
+
"hold": {
|
| 1553 |
+
"GK": "10:21",
|
| 1554 |
+
"LW": "09:55"
|
| 1555 |
+
},
|
| 1556 |
+
"train": "37434"
|
| 1557 |
+
},
|
| 1558 |
+
{
|
| 1559 |
+
"hold": {
|
| 1560 |
+
"FWD": "10:05"
|
| 1561 |
+
},
|
| 1562 |
+
"train": "37425"
|
| 1563 |
+
},
|
| 1564 |
+
{
|
| 1565 |
+
"hold": {
|
| 1566 |
+
"FWD": "10:29"
|
| 1567 |
+
},
|
| 1568 |
+
"from": "ZH",
|
| 1569 |
+
"train": "37427"
|
| 1570 |
+
},
|
| 1571 |
+
{
|
| 1572 |
+
"hold": {
|
| 1573 |
+
"HDG": "10:32",
|
| 1574 |
+
"LW": "10:23"
|
| 1575 |
+
},
|
| 1576 |
+
"train": "37436"
|
| 1577 |
+
},
|
| 1578 |
+
{
|
| 1579 |
+
"hold": {
|
| 1580 |
+
"FWD": "10:35"
|
| 1581 |
+
},
|
| 1582 |
+
"train": "37327"
|
| 1583 |
+
}
|
| 1584 |
+
]
|
| 1585 |
+
},
|
| 1586 |
+
{
|
| 1587 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 1588 |
+
"task_id": "gnlw-2025-02-13-0954-180m-real-0",
|
| 1589 |
+
"split": "eval",
|
| 1590 |
+
"difficulty": "real",
|
| 1591 |
+
"day": "2025-02-13",
|
| 1592 |
+
"runs": 30,
|
| 1593 |
+
"reward": 0.484494,
|
| 1594 |
+
"submitted": true,
|
| 1595 |
+
"feasible": true,
|
| 1596 |
+
"cost": 233,
|
| 1597 |
+
"optimal_cost": 172,
|
| 1598 |
+
"floor_cost": 154,
|
| 1599 |
+
"greedy_cost": 1960,
|
| 1600 |
+
"real_cost": 215,
|
| 1601 |
+
"hardness": {
|
| 1602 |
+
"runs": 30,
|
| 1603 |
+
"sets": 7,
|
| 1604 |
+
"stops": 195,
|
| 1605 |
+
"conflicts": 13,
|
| 1606 |
+
"single_track": 3,
|
| 1607 |
+
"headway": 7,
|
| 1608 |
+
"platforms": 3,
|
| 1609 |
+
"closed": 0,
|
| 1610 |
+
"decisions": 45,
|
| 1611 |
+
"holds": 45,
|
| 1612 |
+
"short_turns": 0,
|
| 1613 |
+
"cancels": 0,
|
| 1614 |
+
"turn_points": 0,
|
| 1615 |
+
"avoidable": 18,
|
| 1616 |
+
"optimal": 172,
|
| 1617 |
+
"greedy_gap": 0.8,
|
| 1618 |
+
"closed_min": 0,
|
| 1619 |
+
"slow_min": 25,
|
| 1620 |
+
"track_min": 0,
|
| 1621 |
+
"single_closed": false,
|
| 1622 |
+
"mid_line": false,
|
| 1623 |
+
"late_sets": 0
|
| 1624 |
+
},
|
| 1625 |
+
"violations": 0,
|
| 1626 |
+
"agent_turns": 6,
|
| 1627 |
+
"checks": 3,
|
| 1628 |
+
"seconds": 1213.7,
|
| 1629 |
+
"input_tokens": 599410,
|
| 1630 |
+
"cache_read_tokens": 411200,
|
| 1631 |
+
"cache_write_tokens": 0,
|
| 1632 |
+
"output_tokens": 164361,
|
| 1633 |
+
"served_by": [
|
| 1634 |
+
"zai-org/GLM-5.3-Flash"
|
| 1635 |
+
],
|
| 1636 |
+
"end_reason": "submitted",
|
| 1637 |
+
"errors": 0,
|
| 1638 |
+
"plan": [
|
| 1639 |
+
{
|
| 1640 |
+
"train": "37334",
|
| 1641 |
+
"hold": {
|
| 1642 |
+
"GK": "10:19"
|
| 1643 |
+
}
|
| 1644 |
+
},
|
| 1645 |
+
{
|
| 1646 |
+
"train": "37327",
|
| 1647 |
+
"hold": {
|
| 1648 |
+
"GN": "10:20",
|
| 1649 |
+
"FWD": "10:44"
|
| 1650 |
+
}
|
| 1651 |
+
},
|
| 1652 |
+
{
|
| 1653 |
+
"train": "37434",
|
| 1654 |
+
"hold": {
|
| 1655 |
+
"GK": "10:30"
|
| 1656 |
+
}
|
| 1657 |
+
},
|
| 1658 |
+
{
|
| 1659 |
+
"train": "37429",
|
| 1660 |
+
"hold": {
|
| 1661 |
+
"ZH": "10:35",
|
| 1662 |
+
"HDG": "11:03"
|
| 1663 |
+
}
|
| 1664 |
+
},
|
| 1665 |
+
{
|
| 1666 |
+
"train": "37329",
|
| 1667 |
+
"hold": {
|
| 1668 |
+
"FWD": "11:06"
|
| 1669 |
+
}
|
| 1670 |
+
}
|
| 1671 |
+
]
|
| 1672 |
+
},
|
| 1673 |
+
{
|
| 1674 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 1675 |
+
"task_id": "gnlw-2025-02-13-1530-180m-blockage-0",
|
| 1676 |
+
"split": "eval",
|
| 1677 |
+
"difficulty": "blockage",
|
| 1678 |
+
"day": "2025-02-13",
|
| 1679 |
+
"runs": 30,
|
| 1680 |
+
"reward": 0.0,
|
| 1681 |
+
"submitted": false,
|
| 1682 |
+
"feasible": false,
|
| 1683 |
+
"cost": null,
|
| 1684 |
+
"optimal_cost": 648,
|
| 1685 |
+
"floor_cost": 588,
|
| 1686 |
+
"greedy_cost": 1690,
|
| 1687 |
+
"real_cost": 151,
|
| 1688 |
+
"hardness": {
|
| 1689 |
+
"runs": 30,
|
| 1690 |
+
"sets": 7,
|
| 1691 |
+
"stops": 200,
|
| 1692 |
+
"conflicts": 25,
|
| 1693 |
+
"single_track": 5,
|
| 1694 |
+
"headway": 5,
|
| 1695 |
+
"platforms": 1,
|
| 1696 |
+
"closed": 14,
|
| 1697 |
+
"decisions": 37,
|
| 1698 |
+
"holds": 29,
|
| 1699 |
+
"short_turns": 8,
|
| 1700 |
+
"cancels": 0,
|
| 1701 |
+
"turn_points": 2,
|
| 1702 |
+
"avoidable": 60,
|
| 1703 |
+
"optimal": 648,
|
| 1704 |
+
"greedy_gap": 0.8,
|
| 1705 |
+
"closed_min": 24,
|
| 1706 |
+
"slow_min": 0,
|
| 1707 |
+
"track_min": 0,
|
| 1708 |
+
"single_closed": true,
|
| 1709 |
+
"mid_line": false,
|
| 1710 |
+
"late_sets": 1
|
| 1711 |
+
},
|
| 1712 |
+
"violations": 0,
|
| 1713 |
+
"agent_turns": 4,
|
| 1714 |
+
"checks": 1,
|
| 1715 |
+
"seconds": 1659.6,
|
| 1716 |
+
"input_tokens": 289662,
|
| 1717 |
+
"cache_read_tokens": 140032,
|
| 1718 |
+
"cache_write_tokens": 0,
|
| 1719 |
+
"output_tokens": 192312,
|
| 1720 |
+
"served_by": [
|
| 1721 |
+
"zai-org/GLM-5.3-Flash"
|
| 1722 |
+
],
|
| 1723 |
+
"end_reason": "no_tool_call",
|
| 1724 |
+
"errors": 0,
|
| 1725 |
+
"plan": null
|
| 1726 |
+
},
|
| 1727 |
+
{
|
| 1728 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 1729 |
+
"task_id": "gnlw-2025-02-14-1730-210m-blockage-0",
|
| 1730 |
+
"split": "eval",
|
| 1731 |
+
"difficulty": "blockage",
|
| 1732 |
+
"day": "2025-02-14",
|
| 1733 |
+
"runs": 33,
|
| 1734 |
+
"reward": 0.0,
|
| 1735 |
+
"submitted": false,
|
| 1736 |
+
"feasible": false,
|
| 1737 |
+
"cost": null,
|
| 1738 |
+
"optimal_cost": 875,
|
| 1739 |
+
"floor_cost": 776,
|
| 1740 |
+
"greedy_cost": null,
|
| 1741 |
+
"real_cost": 191,
|
| 1742 |
+
"hardness": {
|
| 1743 |
+
"runs": 33,
|
| 1744 |
+
"sets": 7,
|
| 1745 |
+
"stops": 208,
|
| 1746 |
+
"conflicts": 33,
|
| 1747 |
+
"single_track": 7,
|
| 1748 |
+
"headway": 19,
|
| 1749 |
+
"platforms": 3,
|
| 1750 |
+
"closed": 4,
|
| 1751 |
+
"decisions": 35,
|
| 1752 |
+
"holds": 25,
|
| 1753 |
+
"short_turns": 10,
|
| 1754 |
+
"cancels": 0,
|
| 1755 |
+
"turn_points": 3,
|
| 1756 |
+
"avoidable": 99,
|
| 1757 |
+
"optimal": 875,
|
| 1758 |
+
"greedy_gap": 0.8909,
|
| 1759 |
+
"closed_min": 38,
|
| 1760 |
+
"slow_min": 0,
|
| 1761 |
+
"track_min": 0,
|
| 1762 |
+
"single_closed": false,
|
| 1763 |
+
"mid_line": true,
|
| 1764 |
+
"late_sets": 1
|
| 1765 |
+
},
|
| 1766 |
+
"violations": 0,
|
| 1767 |
+
"agent_turns": 2,
|
| 1768 |
+
"checks": 0,
|
| 1769 |
+
"seconds": 1117.8,
|
| 1770 |
+
"input_tokens": 76350,
|
| 1771 |
+
"cache_read_tokens": 7680,
|
| 1772 |
+
"cache_write_tokens": 0,
|
| 1773 |
+
"output_tokens": 128000,
|
| 1774 |
+
"served_by": [
|
| 1775 |
+
"zai-org/GLM-5.3-Flash"
|
| 1776 |
+
],
|
| 1777 |
+
"end_reason": "no_tool_call",
|
| 1778 |
+
"errors": 0,
|
| 1779 |
+
"plan": null
|
| 1780 |
+
},
|
| 1781 |
+
{
|
| 1782 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 1783 |
+
"task_id": "gnlw-2025-02-15-0630-180m-blockage-0",
|
| 1784 |
+
"split": "eval",
|
| 1785 |
+
"difficulty": "blockage",
|
| 1786 |
+
"day": "2025-02-15",
|
| 1787 |
+
"runs": 20,
|
| 1788 |
+
"reward": 0.542968,
|
| 1789 |
+
"submitted": true,
|
| 1790 |
+
"feasible": true,
|
| 1791 |
+
"cost": 2419,
|
| 1792 |
+
"optimal_cost": 2300,
|
| 1793 |
+
"floor_cost": 2119,
|
| 1794 |
+
"greedy_cost": null,
|
| 1795 |
+
"real_cost": 61,
|
| 1796 |
+
"hardness": {
|
| 1797 |
+
"runs": 20,
|
| 1798 |
+
"sets": 7,
|
| 1799 |
+
"stops": 150,
|
| 1800 |
+
"conflicts": 45,
|
| 1801 |
+
"single_track": 0,
|
| 1802 |
+
"headway": 0,
|
| 1803 |
+
"platforms": 0,
|
| 1804 |
+
"closed": 45,
|
| 1805 |
+
"decisions": 31,
|
| 1806 |
+
"holds": 13,
|
| 1807 |
+
"short_turns": 18,
|
| 1808 |
+
"cancels": 0,
|
| 1809 |
+
"turn_points": 3,
|
| 1810 |
+
"avoidable": 181,
|
| 1811 |
+
"optimal": 2300,
|
| 1812 |
+
"greedy_gap": 0.87,
|
| 1813 |
+
"closed_min": 160,
|
| 1814 |
+
"slow_min": 0,
|
| 1815 |
+
"track_min": 0,
|
| 1816 |
+
"single_closed": true,
|
| 1817 |
+
"mid_line": true,
|
| 1818 |
+
"late_sets": 0
|
| 1819 |
+
},
|
| 1820 |
+
"violations": 0,
|
| 1821 |
+
"agent_turns": 11,
|
| 1822 |
+
"checks": 9,
|
| 1823 |
+
"seconds": 2329.4,
|
| 1824 |
+
"input_tokens": 1812928,
|
| 1825 |
+
"cache_read_tokens": 1286016,
|
| 1826 |
+
"cache_write_tokens": 0,
|
| 1827 |
+
"output_tokens": 231905,
|
| 1828 |
+
"served_by": [
|
| 1829 |
+
"zai-org/GLM-5.3-Flash"
|
| 1830 |
+
],
|
| 1831 |
+
"end_reason": "submitted",
|
| 1832 |
+
"errors": 0,
|
| 1833 |
+
"plan": [
|
| 1834 |
+
{
|
| 1835 |
+
"train": "37420",
|
| 1836 |
+
"to": "BP"
|
| 1837 |
+
},
|
| 1838 |
+
{
|
| 1839 |
+
"train": "37417",
|
| 1840 |
+
"from": "BP"
|
| 1841 |
+
},
|
| 1842 |
+
{
|
| 1843 |
+
"train": "37422",
|
| 1844 |
+
"to": "BP"
|
| 1845 |
+
},
|
| 1846 |
+
{
|
| 1847 |
+
"train": "37419",
|
| 1848 |
+
"from": "BP"
|
| 1849 |
+
},
|
| 1850 |
+
{
|
| 1851 |
+
"train": "37328",
|
| 1852 |
+
"to": "BP"
|
| 1853 |
+
},
|
| 1854 |
+
{
|
| 1855 |
+
"train": "37323",
|
| 1856 |
+
"from": "BP"
|
| 1857 |
+
},
|
| 1858 |
+
{
|
| 1859 |
+
"train": "37413",
|
| 1860 |
+
"to": "ZH"
|
| 1861 |
+
},
|
| 1862 |
+
{
|
| 1863 |
+
"train": "37424",
|
| 1864 |
+
"from": "ZH"
|
| 1865 |
+
},
|
| 1866 |
+
{
|
| 1867 |
+
"train": "37421",
|
| 1868 |
+
"to": "ZH",
|
| 1869 |
+
"hold": {
|
| 1870 |
+
"GN": "09:19"
|
| 1871 |
+
}
|
| 1872 |
+
},
|
| 1873 |
+
{
|
| 1874 |
+
"train": "37332",
|
| 1875 |
+
"from": "ZH"
|
| 1876 |
+
},
|
| 1877 |
+
{
|
| 1878 |
+
"train": "37415",
|
| 1879 |
+
"to": "ZH"
|
| 1880 |
+
},
|
| 1881 |
+
{
|
| 1882 |
+
"train": "37426",
|
| 1883 |
+
"from": "ZH"
|
| 1884 |
+
},
|
| 1885 |
+
{
|
| 1886 |
+
"train": "37423",
|
| 1887 |
+
"hold": {
|
| 1888 |
+
"ZH": "09:10"
|
| 1889 |
+
}
|
| 1890 |
+
},
|
| 1891 |
+
{
|
| 1892 |
+
"train": "37319",
|
| 1893 |
+
"to": "ZH"
|
| 1894 |
+
},
|
| 1895 |
+
{
|
| 1896 |
+
"train": "37430",
|
| 1897 |
+
"from": "ZH",
|
| 1898 |
+
"hold": {
|
| 1899 |
+
"ZH": "09:30"
|
| 1900 |
+
}
|
| 1901 |
+
},
|
| 1902 |
+
{
|
| 1903 |
+
"train": "37428",
|
| 1904 |
+
"hold": {
|
| 1905 |
+
"BP": "09:10"
|
| 1906 |
+
}
|
| 1907 |
+
},
|
| 1908 |
+
{
|
| 1909 |
+
"train": "37330",
|
| 1910 |
+
"hold": {
|
| 1911 |
+
"FWD": "09:10"
|
| 1912 |
+
}
|
| 1913 |
+
},
|
| 1914 |
+
{
|
| 1915 |
+
"train": "37321",
|
| 1916 |
+
"to": "ZH",
|
| 1917 |
+
"hold": {
|
| 1918 |
+
"GN": "09:40"
|
| 1919 |
+
}
|
| 1920 |
+
},
|
| 1921 |
+
{
|
| 1922 |
+
"train": "37432",
|
| 1923 |
+
"from": "ZH"
|
| 1924 |
+
}
|
| 1925 |
+
]
|
| 1926 |
+
},
|
| 1927 |
+
{
|
| 1928 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 1929 |
+
"task_id": "gnlw-2025-02-15-0700-240m-compound-0",
|
| 1930 |
+
"split": "eval",
|
| 1931 |
+
"difficulty": "compound",
|
| 1932 |
+
"day": "2025-02-15",
|
| 1933 |
+
"runs": 32,
|
| 1934 |
+
"reward": 0.0,
|
| 1935 |
+
"submitted": false,
|
| 1936 |
+
"feasible": false,
|
| 1937 |
+
"cost": null,
|
| 1938 |
+
"optimal_cost": 858,
|
| 1939 |
+
"floor_cost": 785,
|
| 1940 |
+
"greedy_cost": 2986,
|
| 1941 |
+
"real_cost": 76,
|
| 1942 |
+
"hardness": {
|
| 1943 |
+
"runs": 32,
|
| 1944 |
+
"sets": 7,
|
| 1945 |
+
"stops": 228,
|
| 1946 |
+
"conflicts": 38,
|
| 1947 |
+
"single_track": 4,
|
| 1948 |
+
"headway": 21,
|
| 1949 |
+
"platforms": 3,
|
| 1950 |
+
"closed": 10,
|
| 1951 |
+
"decisions": 37,
|
| 1952 |
+
"holds": 31,
|
| 1953 |
+
"short_turns": 6,
|
| 1954 |
+
"cancels": 0,
|
| 1955 |
+
"turn_points": 2,
|
| 1956 |
+
"avoidable": 73,
|
| 1957 |
+
"optimal": 858,
|
| 1958 |
+
"greedy_gap": 0.8,
|
| 1959 |
+
"closed_min": 34,
|
| 1960 |
+
"slow_min": 0,
|
| 1961 |
+
"track_min": 0,
|
| 1962 |
+
"single_closed": true,
|
| 1963 |
+
"mid_line": false,
|
| 1964 |
+
"late_sets": 2
|
| 1965 |
+
},
|
| 1966 |
+
"violations": 0,
|
| 1967 |
+
"agent_turns": 4,
|
| 1968 |
+
"checks": 1,
|
| 1969 |
+
"seconds": 1677.9,
|
| 1970 |
+
"input_tokens": 294512,
|
| 1971 |
+
"cache_read_tokens": 143360,
|
| 1972 |
+
"cache_write_tokens": 0,
|
| 1973 |
+
"output_tokens": 192263,
|
| 1974 |
+
"served_by": [
|
| 1975 |
+
"zai-org/GLM-5.3-Flash"
|
| 1976 |
+
],
|
| 1977 |
+
"end_reason": "no_tool_call",
|
| 1978 |
+
"errors": 0,
|
| 1979 |
+
"plan": null
|
| 1980 |
+
},
|
| 1981 |
+
{
|
| 1982 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 1983 |
+
"task_id": "gnlw-2025-02-15-1630-150m-loop-0",
|
| 1984 |
+
"split": "eval",
|
| 1985 |
+
"difficulty": "loop",
|
| 1986 |
+
"day": "2025-02-15",
|
| 1987 |
+
"runs": 26,
|
| 1988 |
+
"reward": 0.0,
|
| 1989 |
+
"submitted": false,
|
| 1990 |
+
"feasible": false,
|
| 1991 |
+
"cost": null,
|
| 1992 |
+
"optimal_cost": 275,
|
| 1993 |
+
"floor_cost": 234,
|
| 1994 |
+
"greedy_cost": 1464,
|
| 1995 |
+
"real_cost": 35,
|
| 1996 |
+
"hardness": {
|
| 1997 |
+
"runs": 26,
|
| 1998 |
+
"sets": 7,
|
| 1999 |
+
"stops": 174,
|
| 2000 |
+
"conflicts": 10,
|
| 2001 |
+
"single_track": 3,
|
| 2002 |
+
"headway": 5,
|
| 2003 |
+
"platforms": 2,
|
| 2004 |
+
"closed": 0,
|
| 2005 |
+
"decisions": 52,
|
| 2006 |
+
"holds": 50,
|
| 2007 |
+
"short_turns": 2,
|
| 2008 |
+
"cancels": 0,
|
| 2009 |
+
"turn_points": 1,
|
| 2010 |
+
"avoidable": 41,
|
| 2011 |
+
"optimal": 275,
|
| 2012 |
+
"greedy_gap": 0.8,
|
| 2013 |
+
"closed_min": 0,
|
| 2014 |
+
"slow_min": 25,
|
| 2015 |
+
"track_min": 140,
|
| 2016 |
+
"single_closed": false,
|
| 2017 |
+
"mid_line": false,
|
| 2018 |
+
"late_sets": 1
|
| 2019 |
+
},
|
| 2020 |
+
"violations": 0,
|
| 2021 |
+
"agent_turns": 2,
|
| 2022 |
+
"checks": 0,
|
| 2023 |
+
"seconds": 926.2,
|
| 2024 |
+
"input_tokens": 75109,
|
| 2025 |
+
"cache_read_tokens": 1600,
|
| 2026 |
+
"cache_write_tokens": 0,
|
| 2027 |
+
"output_tokens": 128000,
|
| 2028 |
+
"served_by": [
|
| 2029 |
+
"zai-org/GLM-5.3-Flash"
|
| 2030 |
+
],
|
| 2031 |
+
"end_reason": "no_tool_call",
|
| 2032 |
+
"errors": 0,
|
| 2033 |
+
"plan": null
|
| 2034 |
+
},
|
| 2035 |
+
{
|
| 2036 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 2037 |
+
"task_id": "gnlw-2025-02-16-1530-210m-compound-0",
|
| 2038 |
+
"split": "eval",
|
| 2039 |
+
"difficulty": "compound",
|
| 2040 |
+
"day": "2025-02-16",
|
| 2041 |
+
"runs": 18,
|
| 2042 |
+
"reward": 0.200325,
|
| 2043 |
+
"submitted": true,
|
| 2044 |
+
"feasible": true,
|
| 2045 |
+
"cost": 679,
|
| 2046 |
+
"optimal_cost": 273,
|
| 2047 |
+
"floor_cost": 269,
|
| 2048 |
+
"greedy_cost": 2157,
|
| 2049 |
+
"real_cost": 38,
|
| 2050 |
+
"hardness": {
|
| 2051 |
+
"runs": 18,
|
| 2052 |
+
"sets": 3,
|
| 2053 |
+
"stops": 122,
|
| 2054 |
+
"conflicts": 5,
|
| 2055 |
+
"single_track": 2,
|
| 2056 |
+
"headway": 0,
|
| 2057 |
+
"platforms": 0,
|
| 2058 |
+
"closed": 3,
|
| 2059 |
+
"decisions": 9,
|
| 2060 |
+
"holds": 5,
|
| 2061 |
+
"short_turns": 4,
|
| 2062 |
+
"cancels": 0,
|
| 2063 |
+
"turn_points": 1,
|
| 2064 |
+
"avoidable": 4,
|
| 2065 |
+
"optimal": 273,
|
| 2066 |
+
"greedy_gap": 0.8,
|
| 2067 |
+
"closed_min": 21,
|
| 2068 |
+
"slow_min": 80,
|
| 2069 |
+
"track_min": 0,
|
| 2070 |
+
"single_closed": true,
|
| 2071 |
+
"mid_line": false,
|
| 2072 |
+
"late_sets": 0
|
| 2073 |
+
},
|
| 2074 |
+
"violations": 0,
|
| 2075 |
+
"agent_turns": 6,
|
| 2076 |
+
"checks": 3,
|
| 2077 |
+
"seconds": 2070.3,
|
| 2078 |
+
"input_tokens": 735585,
|
| 2079 |
+
"cache_read_tokens": 520704,
|
| 2080 |
+
"cache_write_tokens": 0,
|
| 2081 |
+
"output_tokens": 202536,
|
| 2082 |
+
"served_by": [
|
| 2083 |
+
"zai-org/GLM-5.3-Flash"
|
| 2084 |
+
],
|
| 2085 |
+
"end_reason": "submitted",
|
| 2086 |
+
"errors": 0,
|
| 2087 |
+
"plan": [
|
| 2088 |
+
{
|
| 2089 |
+
"train": "37458",
|
| 2090 |
+
"cancel": true
|
| 2091 |
+
},
|
| 2092 |
+
{
|
| 2093 |
+
"train": "37353",
|
| 2094 |
+
"cancel": true
|
| 2095 |
+
},
|
| 2096 |
+
{
|
| 2097 |
+
"train": "37360",
|
| 2098 |
+
"to": "BP",
|
| 2099 |
+
"hold": {
|
| 2100 |
+
"LW": "16:48"
|
| 2101 |
+
}
|
| 2102 |
+
},
|
| 2103 |
+
{
|
| 2104 |
+
"train": "37455",
|
| 2105 |
+
"from": "BP",
|
| 2106 |
+
"hold": {
|
| 2107 |
+
"FWD": "17:28"
|
| 2108 |
+
}
|
| 2109 |
+
},
|
| 2110 |
+
{
|
| 2111 |
+
"train": "37349",
|
| 2112 |
+
"hold": {
|
| 2113 |
+
"FWD": "16:09"
|
| 2114 |
+
}
|
| 2115 |
+
}
|
| 2116 |
+
]
|
| 2117 |
+
},
|
| 2118 |
+
{
|
| 2119 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 2120 |
+
"task_id": "gnlw-2025-02-16-1700-210m-compound-0",
|
| 2121 |
+
"split": "eval",
|
| 2122 |
+
"difficulty": "compound",
|
| 2123 |
+
"day": "2025-02-16",
|
| 2124 |
+
"runs": 16,
|
| 2125 |
+
"reward": 1.0,
|
| 2126 |
+
"submitted": true,
|
| 2127 |
+
"feasible": true,
|
| 2128 |
+
"cost": 796,
|
| 2129 |
+
"optimal_cost": 796,
|
| 2130 |
+
"floor_cost": 787,
|
| 2131 |
+
"greedy_cost": 1089,
|
| 2132 |
+
"real_cost": 90,
|
| 2133 |
+
"hardness": {
|
| 2134 |
+
"runs": 16,
|
| 2135 |
+
"sets": 3,
|
| 2136 |
+
"stops": 104,
|
| 2137 |
+
"conflicts": 31,
|
| 2138 |
+
"single_track": 0,
|
| 2139 |
+
"headway": 0,
|
| 2140 |
+
"platforms": 0,
|
| 2141 |
+
"closed": 31,
|
| 2142 |
+
"decisions": 13,
|
| 2143 |
+
"holds": 6,
|
| 2144 |
+
"short_turns": 6,
|
| 2145 |
+
"cancels": 1,
|
| 2146 |
+
"turn_points": 2,
|
| 2147 |
+
"avoidable": 9,
|
| 2148 |
+
"optimal": 796,
|
| 2149 |
+
"greedy_gap": 0.7963,
|
| 2150 |
+
"closed_min": 92,
|
| 2151 |
+
"slow_min": 0,
|
| 2152 |
+
"track_min": 0,
|
| 2153 |
+
"single_closed": true,
|
| 2154 |
+
"mid_line": false,
|
| 2155 |
+
"late_sets": 0
|
| 2156 |
+
},
|
| 2157 |
+
"violations": 0,
|
| 2158 |
+
"agent_turns": 5,
|
| 2159 |
+
"checks": 3,
|
| 2160 |
+
"seconds": 1182.2,
|
| 2161 |
+
"input_tokens": 347239,
|
| 2162 |
+
"cache_read_tokens": 169152,
|
| 2163 |
+
"cache_write_tokens": 0,
|
| 2164 |
+
"output_tokens": 110263,
|
| 2165 |
+
"served_by": [
|
| 2166 |
+
"zai-org/GLM-5.3-Flash"
|
| 2167 |
+
],
|
| 2168 |
+
"end_reason": "submitted",
|
| 2169 |
+
"errors": 0,
|
| 2170 |
+
"plan": [
|
| 2171 |
+
{
|
| 2172 |
+
"train": "37455",
|
| 2173 |
+
"to": "ZH"
|
| 2174 |
+
},
|
| 2175 |
+
{
|
| 2176 |
+
"train": "37466",
|
| 2177 |
+
"cancel": true
|
| 2178 |
+
},
|
| 2179 |
+
{
|
| 2180 |
+
"train": "37361",
|
| 2181 |
+
"from": "ZH",
|
| 2182 |
+
"hold": {
|
| 2183 |
+
"ZH": "18:42"
|
| 2184 |
+
}
|
| 2185 |
+
},
|
| 2186 |
+
{
|
| 2187 |
+
"train": "37462",
|
| 2188 |
+
"to": "BP"
|
| 2189 |
+
},
|
| 2190 |
+
{
|
| 2191 |
+
"train": "37357",
|
| 2192 |
+
"from": "BP",
|
| 2193 |
+
"hold": {
|
| 2194 |
+
"BP": "17:56"
|
| 2195 |
+
}
|
| 2196 |
+
},
|
| 2197 |
+
{
|
| 2198 |
+
"train": "37368",
|
| 2199 |
+
"hold": {
|
| 2200 |
+
"BP": "18:43"
|
| 2201 |
+
}
|
| 2202 |
+
},
|
| 2203 |
+
{
|
| 2204 |
+
"train": "37364",
|
| 2205 |
+
"to": "BP"
|
| 2206 |
+
},
|
| 2207 |
+
{
|
| 2208 |
+
"train": "37459",
|
| 2209 |
+
"from": "BP",
|
| 2210 |
+
"hold": {
|
| 2211 |
+
"BP": "18:18"
|
| 2212 |
+
}
|
| 2213 |
+
}
|
| 2214 |
+
]
|
| 2215 |
+
},
|
| 2216 |
+
{
|
| 2217 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 2218 |
+
"task_id": "gnlw-2025-02-16-1730-240m-compound-0",
|
| 2219 |
+
"split": "eval",
|
| 2220 |
+
"difficulty": "compound",
|
| 2221 |
+
"day": "2025-02-16",
|
| 2222 |
+
"runs": 19,
|
| 2223 |
+
"reward": 0.776911,
|
| 2224 |
+
"submitted": true,
|
| 2225 |
+
"feasible": true,
|
| 2226 |
+
"cost": 673,
|
| 2227 |
+
"optimal_cost": 656,
|
| 2228 |
+
"floor_cost": 652,
|
| 2229 |
+
"greedy_cost": null,
|
| 2230 |
+
"real_cost": 147,
|
| 2231 |
+
"hardness": {
|
| 2232 |
+
"runs": 19,
|
| 2233 |
+
"sets": 3,
|
| 2234 |
+
"stops": 126,
|
| 2235 |
+
"conflicts": 14,
|
| 2236 |
+
"single_track": 8,
|
| 2237 |
+
"headway": 0,
|
| 2238 |
+
"platforms": 0,
|
| 2239 |
+
"closed": 6,
|
| 2240 |
+
"decisions": 21,
|
| 2241 |
+
"holds": 11,
|
| 2242 |
+
"short_turns": 10,
|
| 2243 |
+
"cancels": 0,
|
| 2244 |
+
"turn_points": 3,
|
| 2245 |
+
"avoidable": 4,
|
| 2246 |
+
"optimal": 656,
|
| 2247 |
+
"greedy_gap": 0.9579,
|
| 2248 |
+
"closed_min": 36,
|
| 2249 |
+
"slow_min": 210,
|
| 2250 |
+
"track_min": 235,
|
| 2251 |
+
"single_closed": true,
|
| 2252 |
+
"mid_line": false,
|
| 2253 |
+
"late_sets": 0
|
| 2254 |
+
},
|
| 2255 |
+
"violations": 0,
|
| 2256 |
+
"agent_turns": 7,
|
| 2257 |
+
"checks": 4,
|
| 2258 |
+
"seconds": 1892.2,
|
| 2259 |
+
"input_tokens": 849411,
|
| 2260 |
+
"cache_read_tokens": 632000,
|
| 2261 |
+
"cache_write_tokens": 0,
|
| 2262 |
+
"output_tokens": 199796,
|
| 2263 |
+
"served_by": [
|
| 2264 |
+
"zai-org/GLM-5.3-Flash"
|
| 2265 |
+
],
|
| 2266 |
+
"end_reason": "submitted",
|
| 2267 |
+
"errors": 0,
|
| 2268 |
+
"plan": [
|
| 2269 |
+
{
|
| 2270 |
+
"train": "37364",
|
| 2271 |
+
"to": "ZH"
|
| 2272 |
+
},
|
| 2273 |
+
{
|
| 2274 |
+
"train": "37459",
|
| 2275 |
+
"from": "ZH"
|
| 2276 |
+
},
|
| 2277 |
+
{
|
| 2278 |
+
"train": "37357",
|
| 2279 |
+
"to": "ZH",
|
| 2280 |
+
"hold": {
|
| 2281 |
+
"GN": "18:16"
|
| 2282 |
+
}
|
| 2283 |
+
},
|
| 2284 |
+
{
|
| 2285 |
+
"train": "37368",
|
| 2286 |
+
"from": "ZH"
|
| 2287 |
+
},
|
| 2288 |
+
{
|
| 2289 |
+
"train": "37361",
|
| 2290 |
+
"to": "HDG"
|
| 2291 |
+
},
|
| 2292 |
+
{
|
| 2293 |
+
"train": "37372",
|
| 2294 |
+
"from": "HDG"
|
| 2295 |
+
},
|
| 2296 |
+
{
|
| 2297 |
+
"train": "37365",
|
| 2298 |
+
"hold": {
|
| 2299 |
+
"FWD": "20:11"
|
| 2300 |
+
},
|
| 2301 |
+
"to": "HDG"
|
| 2302 |
+
},
|
| 2303 |
+
{
|
| 2304 |
+
"train": "37376",
|
| 2305 |
+
"from": "HDG"
|
| 2306 |
+
},
|
| 2307 |
+
{
|
| 2308 |
+
"train": "37369",
|
| 2309 |
+
"hold": {
|
| 2310 |
+
"FWD": "21:11"
|
| 2311 |
+
},
|
| 2312 |
+
"to": "HDG"
|
| 2313 |
+
},
|
| 2314 |
+
{
|
| 2315 |
+
"train": "37380",
|
| 2316 |
+
"from": "HDG"
|
| 2317 |
+
}
|
| 2318 |
+
]
|
| 2319 |
+
},
|
| 2320 |
+
{
|
| 2321 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 2322 |
+
"task_id": "gnlw-2025-03-10-0700-210m-blockage-0",
|
| 2323 |
+
"split": "eval",
|
| 2324 |
+
"difficulty": "blockage",
|
| 2325 |
+
"day": "2025-03-10",
|
| 2326 |
+
"runs": 34,
|
| 2327 |
+
"reward": 0.0,
|
| 2328 |
+
"submitted": false,
|
| 2329 |
+
"feasible": false,
|
| 2330 |
+
"cost": null,
|
| 2331 |
+
"optimal_cost": 455,
|
| 2332 |
+
"floor_cost": 386,
|
| 2333 |
+
"greedy_cost": null,
|
| 2334 |
+
"real_cost": 157,
|
| 2335 |
+
"hardness": {
|
| 2336 |
+
"runs": 34,
|
| 2337 |
+
"sets": 7,
|
| 2338 |
+
"stops": 226,
|
| 2339 |
+
"conflicts": 17,
|
| 2340 |
+
"single_track": 0,
|
| 2341 |
+
"headway": 0,
|
| 2342 |
+
"platforms": 0,
|
| 2343 |
+
"closed": 17,
|
| 2344 |
+
"decisions": 32,
|
| 2345 |
+
"holds": 26,
|
| 2346 |
+
"short_turns": 6,
|
| 2347 |
+
"cancels": 0,
|
| 2348 |
+
"turn_points": 2,
|
| 2349 |
+
"avoidable": 69,
|
| 2350 |
+
"optimal": 455,
|
| 2351 |
+
"greedy_gap": 0.8294,
|
| 2352 |
+
"closed_min": 23,
|
| 2353 |
+
"slow_min": 0,
|
| 2354 |
+
"track_min": 0,
|
| 2355 |
+
"single_closed": true,
|
| 2356 |
+
"mid_line": false,
|
| 2357 |
+
"late_sets": 0
|
| 2358 |
+
},
|
| 2359 |
+
"violations": 0,
|
| 2360 |
+
"agent_turns": 2,
|
| 2361 |
+
"checks": 0,
|
| 2362 |
+
"seconds": 919.6,
|
| 2363 |
+
"input_tokens": 76613,
|
| 2364 |
+
"cache_read_tokens": 0,
|
| 2365 |
+
"cache_write_tokens": 0,
|
| 2366 |
+
"output_tokens": 128000,
|
| 2367 |
+
"served_by": [
|
| 2368 |
+
"zai-org/GLM-5.3-Flash"
|
| 2369 |
+
],
|
| 2370 |
+
"end_reason": "no_tool_call",
|
| 2371 |
+
"errors": 0,
|
| 2372 |
+
"plan": null
|
| 2373 |
+
},
|
| 2374 |
+
{
|
| 2375 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 2376 |
+
"task_id": "gnlw-2025-03-11-0730-150m-blockage-0",
|
| 2377 |
+
"split": "eval",
|
| 2378 |
+
"difficulty": "blockage",
|
| 2379 |
+
"day": "2025-03-11",
|
| 2380 |
+
"runs": 26,
|
| 2381 |
+
"reward": 0.0,
|
| 2382 |
+
"submitted": false,
|
| 2383 |
+
"feasible": false,
|
| 2384 |
+
"cost": null,
|
| 2385 |
+
"optimal_cost": 2043,
|
| 2386 |
+
"floor_cost": 1851,
|
| 2387 |
+
"greedy_cost": null,
|
| 2388 |
+
"real_cost": 201,
|
| 2389 |
+
"hardness": {
|
| 2390 |
+
"runs": 26,
|
| 2391 |
+
"sets": 7,
|
| 2392 |
+
"stops": 174,
|
| 2393 |
+
"conflicts": 72,
|
| 2394 |
+
"single_track": 0,
|
| 2395 |
+
"headway": 0,
|
| 2396 |
+
"platforms": 0,
|
| 2397 |
+
"closed": 72,
|
| 2398 |
+
"decisions": 35,
|
| 2399 |
+
"holds": 19,
|
| 2400 |
+
"short_turns": 12,
|
| 2401 |
+
"cancels": 4,
|
| 2402 |
+
"turn_points": 2,
|
| 2403 |
+
"avoidable": 192,
|
| 2404 |
+
"optimal": 2043,
|
| 2405 |
+
"greedy_gap": 0.9,
|
| 2406 |
+
"closed_min": 130,
|
| 2407 |
+
"slow_min": 0,
|
| 2408 |
+
"track_min": 0,
|
| 2409 |
+
"single_closed": true,
|
| 2410 |
+
"mid_line": false,
|
| 2411 |
+
"late_sets": 0
|
| 2412 |
+
},
|
| 2413 |
+
"violations": 0,
|
| 2414 |
+
"agent_turns": 2,
|
| 2415 |
+
"checks": 0,
|
| 2416 |
+
"seconds": 1157.3,
|
| 2417 |
+
"input_tokens": 74967,
|
| 2418 |
+
"cache_read_tokens": 6976,
|
| 2419 |
+
"cache_write_tokens": 0,
|
| 2420 |
+
"output_tokens": 128000,
|
| 2421 |
+
"served_by": [
|
| 2422 |
+
"zai-org/GLM-5.3-Flash"
|
| 2423 |
+
],
|
| 2424 |
+
"end_reason": "no_tool_call",
|
| 2425 |
+
"errors": 0,
|
| 2426 |
+
"plan": null
|
| 2427 |
+
},
|
| 2428 |
+
{
|
| 2429 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 2430 |
+
"task_id": "gnlw-2025-03-12-1530-180m-blockage-0",
|
| 2431 |
+
"split": "eval",
|
| 2432 |
+
"difficulty": "blockage",
|
| 2433 |
+
"day": "2025-03-12",
|
| 2434 |
+
"runs": 30,
|
| 2435 |
+
"reward": 0.0,
|
| 2436 |
+
"submitted": false,
|
| 2437 |
+
"feasible": false,
|
| 2438 |
+
"cost": null,
|
| 2439 |
+
"optimal_cost": 806,
|
| 2440 |
+
"floor_cost": 730,
|
| 2441 |
+
"greedy_cost": 1632,
|
| 2442 |
+
"real_cost": 98,
|
| 2443 |
+
"hardness": {
|
| 2444 |
+
"runs": 30,
|
| 2445 |
+
"sets": 7,
|
| 2446 |
+
"stops": 200,
|
| 2447 |
+
"conflicts": 31,
|
| 2448 |
+
"single_track": 0,
|
| 2449 |
+
"headway": 0,
|
| 2450 |
+
"platforms": 0,
|
| 2451 |
+
"closed": 31,
|
| 2452 |
+
"decisions": 36,
|
| 2453 |
+
"holds": 28,
|
| 2454 |
+
"short_turns": 8,
|
| 2455 |
+
"cancels": 0,
|
| 2456 |
+
"turn_points": 2,
|
| 2457 |
+
"avoidable": 76,
|
| 2458 |
+
"optimal": 806,
|
| 2459 |
+
"greedy_gap": 0.7999,
|
| 2460 |
+
"closed_min": 25,
|
| 2461 |
+
"slow_min": 0,
|
| 2462 |
+
"track_min": 0,
|
| 2463 |
+
"single_closed": true,
|
| 2464 |
+
"mid_line": false,
|
| 2465 |
+
"late_sets": 0
|
| 2466 |
+
},
|
| 2467 |
+
"violations": 0,
|
| 2468 |
+
"agent_turns": 5,
|
| 2469 |
+
"checks": 2,
|
| 2470 |
+
"seconds": 2063.0,
|
| 2471 |
+
"input_tokens": 422539,
|
| 2472 |
+
"cache_read_tokens": 151744,
|
| 2473 |
+
"cache_write_tokens": 0,
|
| 2474 |
+
"output_tokens": 216391,
|
| 2475 |
+
"served_by": [
|
| 2476 |
+
"zai-org/GLM-5.3-Flash"
|
| 2477 |
+
],
|
| 2478 |
+
"end_reason": "no_tool_call",
|
| 2479 |
+
"errors": 0,
|
| 2480 |
+
"plan": null
|
| 2481 |
+
},
|
| 2482 |
+
{
|
| 2483 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 2484 |
+
"task_id": "gnlw-2025-03-12-1730-210m-loop-0",
|
| 2485 |
+
"split": "eval",
|
| 2486 |
+
"difficulty": "loop",
|
| 2487 |
+
"day": "2025-03-12",
|
| 2488 |
+
"runs": 33,
|
| 2489 |
+
"reward": 0.0,
|
| 2490 |
+
"submitted": false,
|
| 2491 |
+
"feasible": false,
|
| 2492 |
+
"cost": null,
|
| 2493 |
+
"optimal_cost": 166,
|
| 2494 |
+
"floor_cost": 158,
|
| 2495 |
+
"greedy_cost": 826,
|
| 2496 |
+
"real_cost": 64,
|
| 2497 |
+
"hardness": {
|
| 2498 |
+
"runs": 33,
|
| 2499 |
+
"sets": 7,
|
| 2500 |
+
"stops": 208,
|
| 2501 |
+
"conflicts": 11,
|
| 2502 |
+
"single_track": 2,
|
| 2503 |
+
"headway": 8,
|
| 2504 |
+
"platforms": 1,
|
| 2505 |
+
"closed": 0,
|
| 2506 |
+
"decisions": 35,
|
| 2507 |
+
"holds": 33,
|
| 2508 |
+
"short_turns": 2,
|
| 2509 |
+
"cancels": 0,
|
| 2510 |
+
"turn_points": 1,
|
| 2511 |
+
"avoidable": 8,
|
| 2512 |
+
"optimal": 166,
|
| 2513 |
+
"greedy_gap": 0.8,
|
| 2514 |
+
"closed_min": 0,
|
| 2515 |
+
"slow_min": 0,
|
| 2516 |
+
"track_min": 10,
|
| 2517 |
+
"single_closed": false,
|
| 2518 |
+
"mid_line": false,
|
| 2519 |
+
"late_sets": 1
|
| 2520 |
+
},
|
| 2521 |
+
"violations": 0,
|
| 2522 |
+
"agent_turns": 4,
|
| 2523 |
+
"checks": 1,
|
| 2524 |
+
"seconds": 1521.3,
|
| 2525 |
+
"input_tokens": 289611,
|
| 2526 |
+
"cache_read_tokens": 134400,
|
| 2527 |
+
"cache_write_tokens": 0,
|
| 2528 |
+
"output_tokens": 192468,
|
| 2529 |
+
"served_by": [
|
| 2530 |
+
"zai-org/GLM-5.3-Flash"
|
| 2531 |
+
],
|
| 2532 |
+
"end_reason": "no_tool_call",
|
| 2533 |
+
"errors": 0,
|
| 2534 |
+
"plan": null
|
| 2535 |
+
},
|
| 2536 |
+
{
|
| 2537 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 2538 |
+
"task_id": "gnlw-2025-03-13-0706-180m-real-0",
|
| 2539 |
+
"split": "eval",
|
| 2540 |
+
"difficulty": "real",
|
| 2541 |
+
"day": "2025-03-13",
|
| 2542 |
+
"runs": 26,
|
| 2543 |
+
"reward": 1.0,
|
| 2544 |
+
"submitted": true,
|
| 2545 |
+
"feasible": true,
|
| 2546 |
+
"cost": 378,
|
| 2547 |
+
"optimal_cost": 378,
|
| 2548 |
+
"floor_cost": 378,
|
| 2549 |
+
"greedy_cost": null,
|
| 2550 |
+
"real_cost": 554,
|
| 2551 |
+
"hardness": {
|
| 2552 |
+
"runs": 26,
|
| 2553 |
+
"sets": 6,
|
| 2554 |
+
"stops": 177,
|
| 2555 |
+
"conflicts": 18,
|
| 2556 |
+
"single_track": 1,
|
| 2557 |
+
"headway": 0,
|
| 2558 |
+
"platforms": 0,
|
| 2559 |
+
"closed": 17,
|
| 2560 |
+
"decisions": 27,
|
| 2561 |
+
"holds": 19,
|
| 2562 |
+
"short_turns": 8,
|
| 2563 |
+
"cancels": 0,
|
| 2564 |
+
"turn_points": 1,
|
| 2565 |
+
"avoidable": 0,
|
| 2566 |
+
"optimal": 378,
|
| 2567 |
+
"greedy_gap": 0.8077,
|
| 2568 |
+
"closed_min": 59,
|
| 2569 |
+
"slow_min": 0,
|
| 2570 |
+
"track_min": 0,
|
| 2571 |
+
"single_closed": true,
|
| 2572 |
+
"mid_line": false,
|
| 2573 |
+
"late_sets": 0
|
| 2574 |
+
},
|
| 2575 |
+
"violations": 0,
|
| 2576 |
+
"agent_turns": 6,
|
| 2577 |
+
"checks": 4,
|
| 2578 |
+
"seconds": 899.1,
|
| 2579 |
+
"input_tokens": 404098,
|
| 2580 |
+
"cache_read_tokens": 217088,
|
| 2581 |
+
"cache_write_tokens": 0,
|
| 2582 |
+
"output_tokens": 89748,
|
| 2583 |
+
"served_by": [
|
| 2584 |
+
"zai-org/GLM-5.3-Flash"
|
| 2585 |
+
],
|
| 2586 |
+
"end_reason": "submitted",
|
| 2587 |
+
"errors": 0,
|
| 2588 |
+
"plan": [
|
| 2589 |
+
{
|
| 2590 |
+
"train": "37413",
|
| 2591 |
+
"to": "HDG"
|
| 2592 |
+
},
|
| 2593 |
+
{
|
| 2594 |
+
"train": "37324",
|
| 2595 |
+
"from": "HDG",
|
| 2596 |
+
"hold": {
|
| 2597 |
+
"HDG": "07:22"
|
| 2598 |
+
}
|
| 2599 |
+
},
|
| 2600 |
+
{
|
| 2601 |
+
"train": "37313",
|
| 2602 |
+
"to": "HDG"
|
| 2603 |
+
},
|
| 2604 |
+
{
|
| 2605 |
+
"train": "37424",
|
| 2606 |
+
"from": "HDG",
|
| 2607 |
+
"hold": {
|
| 2608 |
+
"HDG": "07:29"
|
| 2609 |
+
}
|
| 2610 |
+
},
|
| 2611 |
+
{
|
| 2612 |
+
"train": "37415",
|
| 2613 |
+
"to": "HDG"
|
| 2614 |
+
},
|
| 2615 |
+
{
|
| 2616 |
+
"train": "37326",
|
| 2617 |
+
"from": "HDG",
|
| 2618 |
+
"hold": {
|
| 2619 |
+
"HDG": "07:52"
|
| 2620 |
+
}
|
| 2621 |
+
},
|
| 2622 |
+
{
|
| 2623 |
+
"train": "37315",
|
| 2624 |
+
"to": "HDG"
|
| 2625 |
+
},
|
| 2626 |
+
{
|
| 2627 |
+
"train": "37426",
|
| 2628 |
+
"from": "HDG",
|
| 2629 |
+
"hold": {
|
| 2630 |
+
"HDG": "07:59"
|
| 2631 |
+
}
|
| 2632 |
+
},
|
| 2633 |
+
{
|
| 2634 |
+
"train": "37417",
|
| 2635 |
+
"hold": {
|
| 2636 |
+
"HDG": "08:00"
|
| 2637 |
+
}
|
| 2638 |
+
}
|
| 2639 |
+
]
|
| 2640 |
+
},
|
| 2641 |
+
{
|
| 2642 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 2643 |
+
"task_id": "gnlw-2025-03-16-1630-180m-compound-0",
|
| 2644 |
+
"split": "eval",
|
| 2645 |
+
"difficulty": "compound",
|
| 2646 |
+
"day": "2025-03-16",
|
| 2647 |
+
"runs": 15,
|
| 2648 |
+
"reward": 1.0,
|
| 2649 |
+
"submitted": true,
|
| 2650 |
+
"feasible": true,
|
| 2651 |
+
"cost": 1259,
|
| 2652 |
+
"optimal_cost": 1259,
|
| 2653 |
+
"floor_cost": 1256,
|
| 2654 |
+
"greedy_cost": null,
|
| 2655 |
+
"real_cost": 73,
|
| 2656 |
+
"hardness": {
|
| 2657 |
+
"runs": 15,
|
| 2658 |
+
"sets": 3,
|
| 2659 |
+
"stops": 100,
|
| 2660 |
+
"conflicts": 53,
|
| 2661 |
+
"single_track": 3,
|
| 2662 |
+
"headway": 0,
|
| 2663 |
+
"platforms": 0,
|
| 2664 |
+
"closed": 50,
|
| 2665 |
+
"decisions": 15,
|
| 2666 |
+
"holds": 3,
|
| 2667 |
+
"short_turns": 10,
|
| 2668 |
+
"cancels": 2,
|
| 2669 |
+
"turn_points": 2,
|
| 2670 |
+
"avoidable": 3,
|
| 2671 |
+
"optimal": 1259,
|
| 2672 |
+
"greedy_gap": 0.8533,
|
| 2673 |
+
"closed_min": 149,
|
| 2674 |
+
"slow_min": 95,
|
| 2675 |
+
"track_min": 31,
|
| 2676 |
+
"single_closed": true,
|
| 2677 |
+
"mid_line": false,
|
| 2678 |
+
"late_sets": 0
|
| 2679 |
+
},
|
| 2680 |
+
"violations": 0,
|
| 2681 |
+
"agent_turns": 4,
|
| 2682 |
+
"checks": 2,
|
| 2683 |
+
"seconds": 835.2,
|
| 2684 |
+
"input_tokens": 230449,
|
| 2685 |
+
"cache_read_tokens": 139008,
|
| 2686 |
+
"cache_write_tokens": 0,
|
| 2687 |
+
"output_tokens": 91920,
|
| 2688 |
+
"served_by": [
|
| 2689 |
+
"zai-org/GLM-5.3-Flash"
|
| 2690 |
+
],
|
| 2691 |
+
"end_reason": "submitted",
|
| 2692 |
+
"errors": 0,
|
| 2693 |
+
"plan": [
|
| 2694 |
+
{
|
| 2695 |
+
"train": "37360",
|
| 2696 |
+
"to": "BP"
|
| 2697 |
+
},
|
| 2698 |
+
{
|
| 2699 |
+
"train": "37455",
|
| 2700 |
+
"from": "BP"
|
| 2701 |
+
},
|
| 2702 |
+
{
|
| 2703 |
+
"train": "37466",
|
| 2704 |
+
"to": "BP"
|
| 2705 |
+
},
|
| 2706 |
+
{
|
| 2707 |
+
"train": "37361",
|
| 2708 |
+
"from": "BP"
|
| 2709 |
+
},
|
| 2710 |
+
{
|
| 2711 |
+
"train": "37462",
|
| 2712 |
+
"to": "BP"
|
| 2713 |
+
},
|
| 2714 |
+
{
|
| 2715 |
+
"train": "37357",
|
| 2716 |
+
"from": "BP",
|
| 2717 |
+
"hold": {
|
| 2718 |
+
"FWD": "18:05"
|
| 2719 |
+
}
|
| 2720 |
+
},
|
| 2721 |
+
{
|
| 2722 |
+
"train": "37368",
|
| 2723 |
+
"to": "BP"
|
| 2724 |
+
},
|
| 2725 |
+
{
|
| 2726 |
+
"train": "37463",
|
| 2727 |
+
"from": "BP"
|
| 2728 |
+
},
|
| 2729 |
+
{
|
| 2730 |
+
"train": "37353",
|
| 2731 |
+
"to": "ZH"
|
| 2732 |
+
},
|
| 2733 |
+
{
|
| 2734 |
+
"train": "37364",
|
| 2735 |
+
"cancel": true
|
| 2736 |
+
},
|
| 2737 |
+
{
|
| 2738 |
+
"train": "37459",
|
| 2739 |
+
"cancel": true
|
| 2740 |
+
},
|
| 2741 |
+
{
|
| 2742 |
+
"train": "37470",
|
| 2743 |
+
"from": "ZH"
|
| 2744 |
+
}
|
| 2745 |
+
]
|
| 2746 |
+
},
|
| 2747 |
+
{
|
| 2748 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 2749 |
+
"task_id": "gnlw-2025-03-16-1700-210m-compound-0",
|
| 2750 |
+
"split": "eval",
|
| 2751 |
+
"difficulty": "compound",
|
| 2752 |
+
"day": "2025-03-16",
|
| 2753 |
+
"runs": 16,
|
| 2754 |
+
"reward": 0.0,
|
| 2755 |
+
"submitted": false,
|
| 2756 |
+
"feasible": false,
|
| 2757 |
+
"cost": null,
|
| 2758 |
+
"optimal_cost": 529,
|
| 2759 |
+
"floor_cost": 529,
|
| 2760 |
+
"greedy_cost": 2995,
|
| 2761 |
+
"real_cost": 86,
|
| 2762 |
+
"hardness": {
|
| 2763 |
+
"runs": 16,
|
| 2764 |
+
"sets": 3,
|
| 2765 |
+
"stops": 104,
|
| 2766 |
+
"conflicts": 13,
|
| 2767 |
+
"single_track": 8,
|
| 2768 |
+
"headway": 0,
|
| 2769 |
+
"platforms": 0,
|
| 2770 |
+
"closed": 5,
|
| 2771 |
+
"decisions": 20,
|
| 2772 |
+
"holds": 12,
|
| 2773 |
+
"short_turns": 8,
|
| 2774 |
+
"cancels": 0,
|
| 2775 |
+
"turn_points": 1,
|
| 2776 |
+
"avoidable": 0,
|
| 2777 |
+
"optimal": 529,
|
| 2778 |
+
"greedy_gap": 0.8,
|
| 2779 |
+
"closed_min": 35,
|
| 2780 |
+
"slow_min": 210,
|
| 2781 |
+
"track_min": 0,
|
| 2782 |
+
"single_closed": true,
|
| 2783 |
+
"mid_line": false,
|
| 2784 |
+
"late_sets": 0
|
| 2785 |
+
},
|
| 2786 |
+
"violations": 0,
|
| 2787 |
+
"agent_turns": 4,
|
| 2788 |
+
"checks": 1,
|
| 2789 |
+
"seconds": 1760.8,
|
| 2790 |
+
"input_tokens": 278022,
|
| 2791 |
+
"cache_read_tokens": 69696,
|
| 2792 |
+
"cache_write_tokens": 0,
|
| 2793 |
+
"output_tokens": 192030,
|
| 2794 |
+
"served_by": [
|
| 2795 |
+
"zai-org/GLM-5.3-Flash"
|
| 2796 |
+
],
|
| 2797 |
+
"end_reason": "no_tool_call",
|
| 2798 |
+
"errors": 0,
|
| 2799 |
+
"plan": null
|
| 2800 |
+
},
|
| 2801 |
+
{
|
| 2802 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 2803 |
+
"task_id": "gnlw-2025-04-23-1630-150m-blockage-0",
|
| 2804 |
+
"split": "eval",
|
| 2805 |
+
"difficulty": "blockage",
|
| 2806 |
+
"day": "2025-04-23",
|
| 2807 |
+
"runs": 26,
|
| 2808 |
+
"reward": 0.0,
|
| 2809 |
+
"submitted": false,
|
| 2810 |
+
"feasible": false,
|
| 2811 |
+
"cost": null,
|
| 2812 |
+
"optimal_cost": 1926,
|
| 2813 |
+
"floor_cost": 1782,
|
| 2814 |
+
"greedy_cost": null,
|
| 2815 |
+
"real_cost": 40,
|
| 2816 |
+
"hardness": {
|
| 2817 |
+
"runs": 26,
|
| 2818 |
+
"sets": 7,
|
| 2819 |
+
"stops": 174,
|
| 2820 |
+
"conflicts": 18,
|
| 2821 |
+
"single_track": 0,
|
| 2822 |
+
"headway": 0,
|
| 2823 |
+
"platforms": 0,
|
| 2824 |
+
"closed": 18,
|
| 2825 |
+
"decisions": 26,
|
| 2826 |
+
"holds": 12,
|
| 2827 |
+
"short_turns": 14,
|
| 2828 |
+
"cancels": 0,
|
| 2829 |
+
"turn_points": 2,
|
| 2830 |
+
"avoidable": 144,
|
| 2831 |
+
"optimal": 1926,
|
| 2832 |
+
"greedy_gap": 0.8385,
|
| 2833 |
+
"closed_min": 120,
|
| 2834 |
+
"slow_min": 0,
|
| 2835 |
+
"track_min": 0,
|
| 2836 |
+
"single_closed": true,
|
| 2837 |
+
"mid_line": true,
|
| 2838 |
+
"late_sets": 0
|
| 2839 |
+
},
|
| 2840 |
+
"violations": 0,
|
| 2841 |
+
"agent_turns": 2,
|
| 2842 |
+
"checks": 0,
|
| 2843 |
+
"seconds": 1484.8,
|
| 2844 |
+
"input_tokens": 74971,
|
| 2845 |
+
"cache_read_tokens": 0,
|
| 2846 |
+
"cache_write_tokens": 0,
|
| 2847 |
+
"output_tokens": 128000,
|
| 2848 |
+
"served_by": [
|
| 2849 |
+
"zai-org/GLM-5.3-Flash"
|
| 2850 |
+
],
|
| 2851 |
+
"end_reason": "no_tool_call",
|
| 2852 |
+
"errors": 0,
|
| 2853 |
+
"plan": null
|
| 2854 |
+
},
|
| 2855 |
+
{
|
| 2856 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 2857 |
+
"task_id": "gnlw-2025-04-25-1730-210m-blockage-0",
|
| 2858 |
+
"split": "eval",
|
| 2859 |
+
"difficulty": "blockage",
|
| 2860 |
+
"day": "2025-04-25",
|
| 2861 |
+
"runs": 33,
|
| 2862 |
+
"reward": 0.212973,
|
| 2863 |
+
"submitted": true,
|
| 2864 |
+
"feasible": true,
|
| 2865 |
+
"cost": 1607,
|
| 2866 |
+
"optimal_cost": 1370,
|
| 2867 |
+
"floor_cost": 1355,
|
| 2868 |
+
"greedy_cost": null,
|
| 2869 |
+
"real_cost": 51,
|
| 2870 |
+
"hardness": {
|
| 2871 |
+
"runs": 33,
|
| 2872 |
+
"sets": 7,
|
| 2873 |
+
"stops": 208,
|
| 2874 |
+
"conflicts": 33,
|
| 2875 |
+
"single_track": 5,
|
| 2876 |
+
"headway": 0,
|
| 2877 |
+
"platforms": 0,
|
| 2878 |
+
"closed": 28,
|
| 2879 |
+
"decisions": 25,
|
| 2880 |
+
"holds": 11,
|
| 2881 |
+
"short_turns": 14,
|
| 2882 |
+
"cancels": 0,
|
| 2883 |
+
"turn_points": 2,
|
| 2884 |
+
"avoidable": 15,
|
| 2885 |
+
"optimal": 1370,
|
| 2886 |
+
"greedy_gap": 0.8424,
|
| 2887 |
+
"closed_min": 66,
|
| 2888 |
+
"slow_min": 0,
|
| 2889 |
+
"track_min": 0,
|
| 2890 |
+
"single_closed": true,
|
| 2891 |
+
"mid_line": true,
|
| 2892 |
+
"late_sets": 1
|
| 2893 |
+
},
|
| 2894 |
+
"violations": 0,
|
| 2895 |
+
"agent_turns": 7,
|
| 2896 |
+
"checks": 4,
|
| 2897 |
+
"seconds": 1667.7,
|
| 2898 |
+
"input_tokens": 833434,
|
| 2899 |
+
"cache_read_tokens": 601536,
|
| 2900 |
+
"cache_write_tokens": 0,
|
| 2901 |
+
"output_tokens": 214284,
|
| 2902 |
+
"served_by": [
|
| 2903 |
+
"zai-org/GLM-5.3-Flash"
|
| 2904 |
+
],
|
| 2905 |
+
"end_reason": "submitted",
|
| 2906 |
+
"errors": 0,
|
| 2907 |
+
"plan": [
|
| 2908 |
+
{
|
| 2909 |
+
"train": "37457",
|
| 2910 |
+
"hold": {
|
| 2911 |
+
"GK": "18:46"
|
| 2912 |
+
},
|
| 2913 |
+
"to": "BP"
|
| 2914 |
+
},
|
| 2915 |
+
{
|
| 2916 |
+
"train": "37368",
|
| 2917 |
+
"cancel": true
|
| 2918 |
+
},
|
| 2919 |
+
{
|
| 2920 |
+
"train": "37364",
|
| 2921 |
+
"to": "BP"
|
| 2922 |
+
},
|
| 2923 |
+
{
|
| 2924 |
+
"train": "37359",
|
| 2925 |
+
"from": "BP"
|
| 2926 |
+
},
|
| 2927 |
+
{
|
| 2928 |
+
"train": "37357",
|
| 2929 |
+
"to": "ZH"
|
| 2930 |
+
},
|
| 2931 |
+
{
|
| 2932 |
+
"train": "37468",
|
| 2933 |
+
"from": "ZH"
|
| 2934 |
+
},
|
| 2935 |
+
{
|
| 2936 |
+
"train": "37459",
|
| 2937 |
+
"to": "ZH"
|
| 2938 |
+
},
|
| 2939 |
+
{
|
| 2940 |
+
"train": "37370",
|
| 2941 |
+
"from": "ZH"
|
| 2942 |
+
},
|
| 2943 |
+
{
|
| 2944 |
+
"train": "37366",
|
| 2945 |
+
"to": "BP",
|
| 2946 |
+
"hold": {
|
| 2947 |
+
"FWD": "18:22"
|
| 2948 |
+
}
|
| 2949 |
+
},
|
| 2950 |
+
{
|
| 2951 |
+
"train": "37361",
|
| 2952 |
+
"from": "BP"
|
| 2953 |
+
},
|
| 2954 |
+
{
|
| 2955 |
+
"train": "37466",
|
| 2956 |
+
"to": "BP",
|
| 2957 |
+
"hold": {
|
| 2958 |
+
"FWD": "18:50"
|
| 2959 |
+
}
|
| 2960 |
+
},
|
| 2961 |
+
{
|
| 2962 |
+
"train": "37463",
|
| 2963 |
+
"from": "BP"
|
| 2964 |
+
},
|
| 2965 |
+
{
|
| 2966 |
+
"train": "37464",
|
| 2967 |
+
"to": "BP"
|
| 2968 |
+
},
|
| 2969 |
+
{
|
| 2970 |
+
"train": "37461",
|
| 2971 |
+
"from": "BP"
|
| 2972 |
+
},
|
| 2973 |
+
{
|
| 2974 |
+
"train": "37363",
|
| 2975 |
+
"from": "BP"
|
| 2976 |
+
},
|
| 2977 |
+
{
|
| 2978 |
+
"train": "37470",
|
| 2979 |
+
"hold": {
|
| 2980 |
+
"DWE": "19:11"
|
| 2981 |
+
}
|
| 2982 |
+
},
|
| 2983 |
+
{
|
| 2984 |
+
"train": "37467",
|
| 2985 |
+
"hold": {
|
| 2986 |
+
"FWD": "20:26"
|
| 2987 |
+
}
|
| 2988 |
+
}
|
| 2989 |
+
]
|
| 2990 |
+
},
|
| 2991 |
+
{
|
| 2992 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 2993 |
+
"task_id": "gnlw-2025-04-26-0630-180m-blockage-0",
|
| 2994 |
+
"split": "eval",
|
| 2995 |
+
"difficulty": "blockage",
|
| 2996 |
+
"day": "2025-04-26",
|
| 2997 |
+
"runs": 20,
|
| 2998 |
+
"reward": 0.200353,
|
| 2999 |
+
"submitted": true,
|
| 3000 |
+
"feasible": true,
|
| 3001 |
+
"cost": 1692,
|
| 3002 |
+
"optimal_cost": 1047,
|
| 3003 |
+
"floor_cost": 980,
|
| 3004 |
+
"greedy_cost": 3569,
|
| 3005 |
+
"real_cost": 88,
|
| 3006 |
+
"hardness": {
|
| 3007 |
+
"runs": 20,
|
| 3008 |
+
"sets": 7,
|
| 3009 |
+
"stops": 150,
|
| 3010 |
+
"conflicts": 35,
|
| 3011 |
+
"single_track": 9,
|
| 3012 |
+
"headway": 19,
|
| 3013 |
+
"platforms": 1,
|
| 3014 |
+
"closed": 6,
|
| 3015 |
+
"decisions": 38,
|
| 3016 |
+
"holds": 30,
|
| 3017 |
+
"short_turns": 8,
|
| 3018 |
+
"cancels": 0,
|
| 3019 |
+
"turn_points": 3,
|
| 3020 |
+
"avoidable": 67,
|
| 3021 |
+
"optimal": 1047,
|
| 3022 |
+
"greedy_gap": 0.8,
|
| 3023 |
+
"closed_min": 59,
|
| 3024 |
+
"slow_min": 0,
|
| 3025 |
+
"track_min": 0,
|
| 3026 |
+
"single_closed": true,
|
| 3027 |
+
"mid_line": false,
|
| 3028 |
+
"late_sets": 1
|
| 3029 |
+
},
|
| 3030 |
+
"violations": 0,
|
| 3031 |
+
"agent_turns": 10,
|
| 3032 |
+
"checks": 7,
|
| 3033 |
+
"seconds": 2400.5,
|
| 3034 |
+
"input_tokens": 1567517,
|
| 3035 |
+
"cache_read_tokens": 926144,
|
| 3036 |
+
"cache_write_tokens": 0,
|
| 3037 |
+
"output_tokens": 221777,
|
| 3038 |
+
"served_by": [
|
| 3039 |
+
"zai-org/GLM-5.3-Flash"
|
| 3040 |
+
],
|
| 3041 |
+
"end_reason": "submitted",
|
| 3042 |
+
"errors": 0,
|
| 3043 |
+
"plan": [
|
| 3044 |
+
{
|
| 3045 |
+
"hold": {
|
| 3046 |
+
"HDG": "07:36"
|
| 3047 |
+
},
|
| 3048 |
+
"train": "37413"
|
| 3049 |
+
},
|
| 3050 |
+
{
|
| 3051 |
+
"hold": {
|
| 3052 |
+
"FWD": "07:34",
|
| 3053 |
+
"HDG": "07:39"
|
| 3054 |
+
},
|
| 3055 |
+
"train": "37415"
|
| 3056 |
+
},
|
| 3057 |
+
{
|
| 3058 |
+
"hold": {
|
| 3059 |
+
"LW": "07:29",
|
| 3060 |
+
"HDG": "07:37"
|
| 3061 |
+
},
|
| 3062 |
+
"train": "37422"
|
| 3063 |
+
},
|
| 3064 |
+
{
|
| 3065 |
+
"to": "BP",
|
| 3066 |
+
"train": "37420"
|
| 3067 |
+
},
|
| 3068 |
+
{
|
| 3069 |
+
"from": "BP",
|
| 3070 |
+
"hold": {
|
| 3071 |
+
"FWD": "07:58"
|
| 3072 |
+
},
|
| 3073 |
+
"train": "37417"
|
| 3074 |
+
},
|
| 3075 |
+
{
|
| 3076 |
+
"hold": {
|
| 3077 |
+
"LW": "07:48"
|
| 3078 |
+
},
|
| 3079 |
+
"train": "37424"
|
| 3080 |
+
},
|
| 3081 |
+
{
|
| 3082 |
+
"hold": {
|
| 3083 |
+
"LW": "07:51",
|
| 3084 |
+
"HDG": "08:01",
|
| 3085 |
+
"GK": "08:24"
|
| 3086 |
+
},
|
| 3087 |
+
"train": "37426"
|
| 3088 |
+
},
|
| 3089 |
+
{
|
| 3090 |
+
"hold": {
|
| 3091 |
+
"GN": "08:12"
|
| 3092 |
+
},
|
| 3093 |
+
"train": "37419"
|
| 3094 |
+
},
|
| 3095 |
+
{
|
| 3096 |
+
"hold": {
|
| 3097 |
+
"GN": "08:32"
|
| 3098 |
+
},
|
| 3099 |
+
"train": "37421"
|
| 3100 |
+
},
|
| 3101 |
+
{
|
| 3102 |
+
"hold": {
|
| 3103 |
+
"LW": "08:17",
|
| 3104 |
+
"GK": "08:49"
|
| 3105 |
+
},
|
| 3106 |
+
"train": "37328"
|
| 3107 |
+
},
|
| 3108 |
+
{
|
| 3109 |
+
"hold": {
|
| 3110 |
+
"GK": "08:53"
|
| 3111 |
+
},
|
| 3112 |
+
"train": "37428"
|
| 3113 |
+
},
|
| 3114 |
+
{
|
| 3115 |
+
"hold": {
|
| 3116 |
+
"GN": "08:29"
|
| 3117 |
+
},
|
| 3118 |
+
"train": "37319"
|
| 3119 |
+
},
|
| 3120 |
+
{
|
| 3121 |
+
"hold": {
|
| 3122 |
+
"LW": "09:14"
|
| 3123 |
+
},
|
| 3124 |
+
"train": "37430"
|
| 3125 |
+
},
|
| 3126 |
+
{
|
| 3127 |
+
"hold": {
|
| 3128 |
+
"LW": "09:18",
|
| 3129 |
+
"HDG": "09:25",
|
| 3130 |
+
"FWD": "09:27",
|
| 3131 |
+
"DWE": "09:30",
|
| 3132 |
+
"BP": "09:35",
|
| 3133 |
+
"GK": "09:44",
|
| 3134 |
+
"ZH": "09:47"
|
| 3135 |
+
},
|
| 3136 |
+
"train": "37330"
|
| 3137 |
+
},
|
| 3138 |
+
{
|
| 3139 |
+
"hold": {
|
| 3140 |
+
"LW": "09:23",
|
| 3141 |
+
"BP": "09:40",
|
| 3142 |
+
"GK": "09:47",
|
| 3143 |
+
"ZH": "09:51"
|
| 3144 |
+
},
|
| 3145 |
+
"train": "37332"
|
| 3146 |
+
},
|
| 3147 |
+
{
|
| 3148 |
+
"hold": {
|
| 3149 |
+
"LW": "09:26",
|
| 3150 |
+
"HDG": "09:37",
|
| 3151 |
+
"ZH": "10:01"
|
| 3152 |
+
},
|
| 3153 |
+
"train": "37432"
|
| 3154 |
+
},
|
| 3155 |
+
{
|
| 3156 |
+
"hold": {
|
| 3157 |
+
"ZH": "09:08",
|
| 3158 |
+
"FWD": "09:31"
|
| 3159 |
+
},
|
| 3160 |
+
"train": "37423"
|
| 3161 |
+
},
|
| 3162 |
+
{
|
| 3163 |
+
"hold": {
|
| 3164 |
+
"FWD": "09:35",
|
| 3165 |
+
"LWC": "09:42"
|
| 3166 |
+
},
|
| 3167 |
+
"train": "37323"
|
| 3168 |
+
}
|
| 3169 |
+
]
|
| 3170 |
+
},
|
| 3171 |
+
{
|
| 3172 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 3173 |
+
"task_id": "gnlw-2025-04-26-1700-180m-blockage-0",
|
| 3174 |
+
"split": "eval",
|
| 3175 |
+
"difficulty": "blockage",
|
| 3176 |
+
"day": "2025-04-26",
|
| 3177 |
+
"runs": 30,
|
| 3178 |
+
"reward": 0.0,
|
| 3179 |
+
"submitted": false,
|
| 3180 |
+
"feasible": false,
|
| 3181 |
+
"cost": null,
|
| 3182 |
+
"optimal_cost": 447,
|
| 3183 |
+
"floor_cost": 433,
|
| 3184 |
+
"greedy_cost": 2402,
|
| 3185 |
+
"real_cost": 59,
|
| 3186 |
+
"hardness": {
|
| 3187 |
+
"runs": 30,
|
| 3188 |
+
"sets": 7,
|
| 3189 |
+
"stops": 200,
|
| 3190 |
+
"conflicts": 10,
|
| 3191 |
+
"single_track": 0,
|
| 3192 |
+
"headway": 0,
|
| 3193 |
+
"platforms": 0,
|
| 3194 |
+
"closed": 10,
|
| 3195 |
+
"decisions": 31,
|
| 3196 |
+
"holds": 25,
|
| 3197 |
+
"short_turns": 6,
|
| 3198 |
+
"cancels": 0,
|
| 3199 |
+
"turn_points": 2,
|
| 3200 |
+
"avoidable": 14,
|
| 3201 |
+
"optimal": 447,
|
| 3202 |
+
"greedy_gap": 0.8,
|
| 3203 |
+
"closed_min": 36,
|
| 3204 |
+
"slow_min": 0,
|
| 3205 |
+
"track_min": 0,
|
| 3206 |
+
"single_closed": true,
|
| 3207 |
+
"mid_line": false,
|
| 3208 |
+
"late_sets": 0
|
| 3209 |
+
},
|
| 3210 |
+
"violations": 0,
|
| 3211 |
+
"agent_turns": 9,
|
| 3212 |
+
"checks": 4,
|
| 3213 |
+
"seconds": 3456.6,
|
| 3214 |
+
"input_tokens": 1424299,
|
| 3215 |
+
"cache_read_tokens": 663424,
|
| 3216 |
+
"cache_write_tokens": 0,
|
| 3217 |
+
"output_tokens": 358778,
|
| 3218 |
+
"served_by": [
|
| 3219 |
+
"zai-org/GLM-5.3-Flash"
|
| 3220 |
+
],
|
| 3221 |
+
"end_reason": "no_tool_call",
|
| 3222 |
+
"errors": 0,
|
| 3223 |
+
"plan": null
|
| 3224 |
+
},
|
| 3225 |
+
{
|
| 3226 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 3227 |
+
"task_id": "gnlw-2025-04-27-0800-210m-compound-0",
|
| 3228 |
+
"split": "eval",
|
| 3229 |
+
"difficulty": "compound",
|
| 3230 |
+
"day": "2025-04-27",
|
| 3231 |
+
"runs": 15,
|
| 3232 |
+
"reward": 0.847471,
|
| 3233 |
+
"submitted": true,
|
| 3234 |
+
"feasible": true,
|
| 3235 |
+
"cost": 393,
|
| 3236 |
+
"optimal_cost": 382,
|
| 3237 |
+
"floor_cost": 378,
|
| 3238 |
+
"greedy_cost": 1155,
|
| 3239 |
+
"real_cost": 39,
|
| 3240 |
+
"hardness": {
|
| 3241 |
+
"runs": 15,
|
| 3242 |
+
"sets": 3,
|
| 3243 |
+
"stops": 100,
|
| 3244 |
+
"conflicts": 20,
|
| 3245 |
+
"single_track": 3,
|
| 3246 |
+
"headway": 14,
|
| 3247 |
+
"platforms": 0,
|
| 3248 |
+
"closed": 3,
|
| 3249 |
+
"decisions": 20,
|
| 3250 |
+
"holds": 16,
|
| 3251 |
+
"short_turns": 4,
|
| 3252 |
+
"cancels": 0,
|
| 3253 |
+
"turn_points": 2,
|
| 3254 |
+
"avoidable": 4,
|
| 3255 |
+
"optimal": 382,
|
| 3256 |
+
"greedy_gap": 0.8,
|
| 3257 |
+
"closed_min": 36,
|
| 3258 |
+
"slow_min": 80,
|
| 3259 |
+
"track_min": 0,
|
| 3260 |
+
"single_closed": true,
|
| 3261 |
+
"mid_line": false,
|
| 3262 |
+
"late_sets": 1
|
| 3263 |
+
},
|
| 3264 |
+
"violations": 0,
|
| 3265 |
+
"agent_turns": 4,
|
| 3266 |
+
"checks": 2,
|
| 3267 |
+
"seconds": 1169.0,
|
| 3268 |
+
"input_tokens": 258427,
|
| 3269 |
+
"cache_read_tokens": 132288,
|
| 3270 |
+
"cache_write_tokens": 0,
|
| 3271 |
+
"output_tokens": 127858,
|
| 3272 |
+
"served_by": [
|
| 3273 |
+
"zai-org/GLM-5.3-Flash"
|
| 3274 |
+
],
|
| 3275 |
+
"end_reason": "submitted",
|
| 3276 |
+
"errors": 0,
|
| 3277 |
+
"plan": [
|
| 3278 |
+
{
|
| 3279 |
+
"train": "37426",
|
| 3280 |
+
"to": "ZH"
|
| 3281 |
+
},
|
| 3282 |
+
{
|
| 3283 |
+
"train": "37321",
|
| 3284 |
+
"from": "ZH",
|
| 3285 |
+
"hold": {
|
| 3286 |
+
"FWD": "09:05"
|
| 3287 |
+
}
|
| 3288 |
+
},
|
| 3289 |
+
{
|
| 3290 |
+
"train": "37328",
|
| 3291 |
+
"to": "BP"
|
| 3292 |
+
},
|
| 3293 |
+
{
|
| 3294 |
+
"train": "37423",
|
| 3295 |
+
"from": "BP",
|
| 3296 |
+
"hold": {
|
| 3297 |
+
"FWD": "09:33"
|
| 3298 |
+
}
|
| 3299 |
+
},
|
| 3300 |
+
{
|
| 3301 |
+
"train": "37325",
|
| 3302 |
+
"hold": {
|
| 3303 |
+
"GN": "09:43"
|
| 3304 |
+
}
|
| 3305 |
+
}
|
| 3306 |
+
]
|
| 3307 |
+
},
|
| 3308 |
+
{
|
| 3309 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 3310 |
+
"task_id": "gnlw-2025-04-27-0830-210m-blockage-0",
|
| 3311 |
+
"split": "eval",
|
| 3312 |
+
"difficulty": "blockage",
|
| 3313 |
+
"day": "2025-04-27",
|
| 3314 |
+
"runs": 18,
|
| 3315 |
+
"reward": 0.314963,
|
| 3316 |
+
"submitted": true,
|
| 3317 |
+
"feasible": true,
|
| 3318 |
+
"cost": 1380,
|
| 3319 |
+
"optimal_cost": 1283,
|
| 3320 |
+
"floor_cost": 1283,
|
| 3321 |
+
"greedy_cost": 1624,
|
| 3322 |
+
"real_cost": 45,
|
| 3323 |
+
"hardness": {
|
| 3324 |
+
"runs": 18,
|
| 3325 |
+
"sets": 3,
|
| 3326 |
+
"stops": 122,
|
| 3327 |
+
"conflicts": 10,
|
| 3328 |
+
"single_track": 0,
|
| 3329 |
+
"headway": 0,
|
| 3330 |
+
"platforms": 0,
|
| 3331 |
+
"closed": 10,
|
| 3332 |
+
"decisions": 15,
|
| 3333 |
+
"holds": 3,
|
| 3334 |
+
"short_turns": 12,
|
| 3335 |
+
"cancels": 0,
|
| 3336 |
+
"turn_points": 2,
|
| 3337 |
+
"avoidable": 0,
|
| 3338 |
+
"optimal": 1283,
|
| 3339 |
+
"greedy_gap": 0.7991,
|
| 3340 |
+
"closed_min": 157,
|
| 3341 |
+
"slow_min": 0,
|
| 3342 |
+
"track_min": 0,
|
| 3343 |
+
"single_closed": false,
|
| 3344 |
+
"mid_line": true,
|
| 3345 |
+
"late_sets": 0
|
| 3346 |
+
},
|
| 3347 |
+
"violations": 0,
|
| 3348 |
+
"agent_turns": 3,
|
| 3349 |
+
"checks": 1,
|
| 3350 |
+
"seconds": 661.3,
|
| 3351 |
+
"input_tokens": 142460,
|
| 3352 |
+
"cache_read_tokens": 69824,
|
| 3353 |
+
"cache_write_tokens": 0,
|
| 3354 |
+
"output_tokens": 67383,
|
| 3355 |
+
"served_by": [
|
| 3356 |
+
"zai-org/GLM-5.3-Flash"
|
| 3357 |
+
],
|
| 3358 |
+
"end_reason": "submitted",
|
| 3359 |
+
"errors": 0,
|
| 3360 |
+
"plan": [
|
| 3361 |
+
{
|
| 3362 |
+
"train": "37430",
|
| 3363 |
+
"to": "HDG"
|
| 3364 |
+
},
|
| 3365 |
+
{
|
| 3366 |
+
"train": "37325",
|
| 3367 |
+
"from": "HDG"
|
| 3368 |
+
},
|
| 3369 |
+
{
|
| 3370 |
+
"train": "37336",
|
| 3371 |
+
"to": "HDG"
|
| 3372 |
+
},
|
| 3373 |
+
{
|
| 3374 |
+
"train": "37431",
|
| 3375 |
+
"from": "HDG"
|
| 3376 |
+
},
|
| 3377 |
+
{
|
| 3378 |
+
"train": "37321",
|
| 3379 |
+
"to": "BP"
|
| 3380 |
+
},
|
| 3381 |
+
{
|
| 3382 |
+
"train": "37332",
|
| 3383 |
+
"from": "BP"
|
| 3384 |
+
},
|
| 3385 |
+
{
|
| 3386 |
+
"train": "37427",
|
| 3387 |
+
"to": "BP"
|
| 3388 |
+
},
|
| 3389 |
+
{
|
| 3390 |
+
"train": "37438",
|
| 3391 |
+
"from": "BP"
|
| 3392 |
+
},
|
| 3393 |
+
{
|
| 3394 |
+
"train": "37423",
|
| 3395 |
+
"to": "BP"
|
| 3396 |
+
},
|
| 3397 |
+
{
|
| 3398 |
+
"train": "37434",
|
| 3399 |
+
"from": "BP"
|
| 3400 |
+
},
|
| 3401 |
+
{
|
| 3402 |
+
"train": "37329",
|
| 3403 |
+
"to": "BP"
|
| 3404 |
+
},
|
| 3405 |
+
{
|
| 3406 |
+
"train": "37340",
|
| 3407 |
+
"from": "BP"
|
| 3408 |
+
}
|
| 3409 |
+
]
|
| 3410 |
+
},
|
| 3411 |
+
{
|
| 3412 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 3413 |
+
"task_id": "gnlw-2025-05-26-0730-150m-blockage-0",
|
| 3414 |
+
"split": "eval",
|
| 3415 |
+
"difficulty": "blockage",
|
| 3416 |
+
"day": "2025-05-26",
|
| 3417 |
+
"runs": 26,
|
| 3418 |
+
"reward": 0.971174,
|
| 3419 |
+
"submitted": true,
|
| 3420 |
+
"feasible": true,
|
| 3421 |
+
"cost": 859,
|
| 3422 |
+
"optimal_cost": 857,
|
| 3423 |
+
"floor_cost": 848,
|
| 3424 |
+
"greedy_cost": null,
|
| 3425 |
+
"real_cost": 165,
|
| 3426 |
+
"hardness": {
|
| 3427 |
+
"runs": 26,
|
| 3428 |
+
"sets": 7,
|
| 3429 |
+
"stops": 174,
|
| 3430 |
+
"conflicts": 24,
|
| 3431 |
+
"single_track": 3,
|
| 3432 |
+
"headway": 4,
|
| 3433 |
+
"platforms": 0,
|
| 3434 |
+
"closed": 17,
|
| 3435 |
+
"decisions": 32,
|
| 3436 |
+
"holds": 14,
|
| 3437 |
+
"short_turns": 18,
|
| 3438 |
+
"cancels": 0,
|
| 3439 |
+
"turn_points": 1,
|
| 3440 |
+
"avoidable": 9,
|
| 3441 |
+
"optimal": 857,
|
| 3442 |
+
"greedy_gap": 0.8231,
|
| 3443 |
+
"closed_min": 130,
|
| 3444 |
+
"slow_min": 0,
|
| 3445 |
+
"track_min": 0,
|
| 3446 |
+
"single_closed": true,
|
| 3447 |
+
"mid_line": false,
|
| 3448 |
+
"late_sets": 1
|
| 3449 |
+
},
|
| 3450 |
+
"violations": 0,
|
| 3451 |
+
"agent_turns": 4,
|
| 3452 |
+
"checks": 1,
|
| 3453 |
+
"seconds": 698.6,
|
| 3454 |
+
"input_tokens": 164449,
|
| 3455 |
+
"cache_read_tokens": 79360,
|
| 3456 |
+
"cache_write_tokens": 0,
|
| 3457 |
+
"output_tokens": 72465,
|
| 3458 |
+
"served_by": [
|
| 3459 |
+
"zai-org/GLM-5.3-Flash"
|
| 3460 |
+
],
|
| 3461 |
+
"end_reason": "submitted",
|
| 3462 |
+
"errors": 0,
|
| 3463 |
+
"plan": [
|
| 3464 |
+
{
|
| 3465 |
+
"train": "37315",
|
| 3466 |
+
"to": "HDG"
|
| 3467 |
+
},
|
| 3468 |
+
{
|
| 3469 |
+
"train": "37426",
|
| 3470 |
+
"from": "HDG"
|
| 3471 |
+
},
|
| 3472 |
+
{
|
| 3473 |
+
"train": "37417",
|
| 3474 |
+
"to": "HDG"
|
| 3475 |
+
},
|
| 3476 |
+
{
|
| 3477 |
+
"train": "37328",
|
| 3478 |
+
"from": "HDG"
|
| 3479 |
+
},
|
| 3480 |
+
{
|
| 3481 |
+
"train": "37317",
|
| 3482 |
+
"to": "HDG"
|
| 3483 |
+
},
|
| 3484 |
+
{
|
| 3485 |
+
"train": "37428",
|
| 3486 |
+
"from": "HDG"
|
| 3487 |
+
},
|
| 3488 |
+
{
|
| 3489 |
+
"train": "37419",
|
| 3490 |
+
"hold": {
|
| 3491 |
+
"GN": "07:57"
|
| 3492 |
+
},
|
| 3493 |
+
"to": "HDG"
|
| 3494 |
+
},
|
| 3495 |
+
{
|
| 3496 |
+
"train": "37330",
|
| 3497 |
+
"from": "HDG"
|
| 3498 |
+
},
|
| 3499 |
+
{
|
| 3500 |
+
"train": "37421",
|
| 3501 |
+
"to": "HDG"
|
| 3502 |
+
},
|
| 3503 |
+
{
|
| 3504 |
+
"train": "37332",
|
| 3505 |
+
"from": "HDG"
|
| 3506 |
+
},
|
| 3507 |
+
{
|
| 3508 |
+
"train": "37321",
|
| 3509 |
+
"to": "HDG"
|
| 3510 |
+
},
|
| 3511 |
+
{
|
| 3512 |
+
"train": "37432",
|
| 3513 |
+
"from": "HDG"
|
| 3514 |
+
},
|
| 3515 |
+
{
|
| 3516 |
+
"train": "37319",
|
| 3517 |
+
"to": "HDG"
|
| 3518 |
+
},
|
| 3519 |
+
{
|
| 3520 |
+
"train": "37430",
|
| 3521 |
+
"from": "HDG"
|
| 3522 |
+
},
|
| 3523 |
+
{
|
| 3524 |
+
"train": "37423",
|
| 3525 |
+
"to": "HDG"
|
| 3526 |
+
},
|
| 3527 |
+
{
|
| 3528 |
+
"train": "37334",
|
| 3529 |
+
"from": "HDG"
|
| 3530 |
+
},
|
| 3531 |
+
{
|
| 3532 |
+
"train": "37323",
|
| 3533 |
+
"to": "HDG"
|
| 3534 |
+
},
|
| 3535 |
+
{
|
| 3536 |
+
"train": "37434",
|
| 3537 |
+
"from": "HDG"
|
| 3538 |
+
},
|
| 3539 |
+
{
|
| 3540 |
+
"train": "37326",
|
| 3541 |
+
"hold": {
|
| 3542 |
+
"GK": "08:09"
|
| 3543 |
+
}
|
| 3544 |
+
}
|
| 3545 |
+
]
|
| 3546 |
+
},
|
| 3547 |
+
{
|
| 3548 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 3549 |
+
"task_id": "gnlw-2025-05-26-1630-150m-blockage-0",
|
| 3550 |
+
"split": "eval",
|
| 3551 |
+
"difficulty": "blockage",
|
| 3552 |
+
"day": "2025-05-26",
|
| 3553 |
+
"runs": 26,
|
| 3554 |
+
"reward": 0.722475,
|
| 3555 |
+
"submitted": true,
|
| 3556 |
+
"feasible": true,
|
| 3557 |
+
"cost": 290,
|
| 3558 |
+
"optimal_cost": 254,
|
| 3559 |
+
"floor_cost": 185,
|
| 3560 |
+
"greedy_cost": null,
|
| 3561 |
+
"real_cost": 55,
|
| 3562 |
+
"hardness": {
|
| 3563 |
+
"runs": 26,
|
| 3564 |
+
"sets": 7,
|
| 3565 |
+
"stops": 174,
|
| 3566 |
+
"conflicts": 8,
|
| 3567 |
+
"single_track": 0,
|
| 3568 |
+
"headway": 0,
|
| 3569 |
+
"platforms": 0,
|
| 3570 |
+
"closed": 8,
|
| 3571 |
+
"decisions": 27,
|
| 3572 |
+
"holds": 25,
|
| 3573 |
+
"short_turns": 2,
|
| 3574 |
+
"cancels": 0,
|
| 3575 |
+
"turn_points": 1,
|
| 3576 |
+
"avoidable": 69,
|
| 3577 |
+
"optimal": 254,
|
| 3578 |
+
"greedy_gap": 0.8,
|
| 3579 |
+
"closed_min": 10,
|
| 3580 |
+
"slow_min": 0,
|
| 3581 |
+
"track_min": 0,
|
| 3582 |
+
"single_closed": true,
|
| 3583 |
+
"mid_line": false,
|
| 3584 |
+
"late_sets": 0
|
| 3585 |
+
},
|
| 3586 |
+
"violations": 0,
|
| 3587 |
+
"agent_turns": 8,
|
| 3588 |
+
"checks": 5,
|
| 3589 |
+
"seconds": 2191.8,
|
| 3590 |
+
"input_tokens": 1088436,
|
| 3591 |
+
"cache_read_tokens": 744512,
|
| 3592 |
+
"cache_write_tokens": 0,
|
| 3593 |
+
"output_tokens": 233550,
|
| 3594 |
+
"served_by": [
|
| 3595 |
+
"zai-org/GLM-5.3-Flash"
|
| 3596 |
+
],
|
| 3597 |
+
"end_reason": "submitted",
|
| 3598 |
+
"errors": 0,
|
| 3599 |
+
"plan": [
|
| 3600 |
+
{
|
| 3601 |
+
"train": "37460",
|
| 3602 |
+
"hold": {
|
| 3603 |
+
"BP": "17:00"
|
| 3604 |
+
}
|
| 3605 |
+
},
|
| 3606 |
+
{
|
| 3607 |
+
"train": "37360",
|
| 3608 |
+
"hold": {
|
| 3609 |
+
"ZH": "17:00"
|
| 3610 |
+
}
|
| 3611 |
+
},
|
| 3612 |
+
{
|
| 3613 |
+
"train": "37353",
|
| 3614 |
+
"hold": {
|
| 3615 |
+
"ZH": "17:00"
|
| 3616 |
+
}
|
| 3617 |
+
},
|
| 3618 |
+
{
|
| 3619 |
+
"train": "37362",
|
| 3620 |
+
"hold": {
|
| 3621 |
+
"FWD": "17:03",
|
| 3622 |
+
"ZH": "17:20"
|
| 3623 |
+
}
|
| 3624 |
+
},
|
| 3625 |
+
{
|
| 3626 |
+
"train": "37462",
|
| 3627 |
+
"hold": {
|
| 3628 |
+
"FWD": "17:06"
|
| 3629 |
+
}
|
| 3630 |
+
},
|
| 3631 |
+
{
|
| 3632 |
+
"train": "37455",
|
| 3633 |
+
"hold": {
|
| 3634 |
+
"GN": "17:00",
|
| 3635 |
+
"ZH": "17:12",
|
| 3636 |
+
"FWD": "17:34"
|
| 3637 |
+
}
|
| 3638 |
+
},
|
| 3639 |
+
{
|
| 3640 |
+
"train": "37355",
|
| 3641 |
+
"hold": {
|
| 3642 |
+
"FWD": "17:40"
|
| 3643 |
+
}
|
| 3644 |
+
},
|
| 3645 |
+
{
|
| 3646 |
+
"train": "37364",
|
| 3647 |
+
"hold": {
|
| 3648 |
+
"LW": "17:26"
|
| 3649 |
+
}
|
| 3650 |
+
},
|
| 3651 |
+
{
|
| 3652 |
+
"train": "37357",
|
| 3653 |
+
"hold": {
|
| 3654 |
+
"ZH": "17:51"
|
| 3655 |
+
}
|
| 3656 |
+
},
|
| 3657 |
+
{
|
| 3658 |
+
"train": "37368",
|
| 3659 |
+
"hold": {
|
| 3660 |
+
"LW": "18:17"
|
| 3661 |
+
}
|
| 3662 |
+
},
|
| 3663 |
+
{
|
| 3664 |
+
"train": "37457",
|
| 3665 |
+
"hold": {
|
| 3666 |
+
"FWD": "17:57"
|
| 3667 |
+
}
|
| 3668 |
+
},
|
| 3669 |
+
{
|
| 3670 |
+
"train": "37466",
|
| 3671 |
+
"hold": {
|
| 3672 |
+
"HDG": "18:00"
|
| 3673 |
+
}
|
| 3674 |
+
}
|
| 3675 |
+
]
|
| 3676 |
+
},
|
| 3677 |
+
{
|
| 3678 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 3679 |
+
"task_id": "gnlw-2025-05-26-1800-150m-blockage-0",
|
| 3680 |
+
"split": "eval",
|
| 3681 |
+
"difficulty": "blockage",
|
| 3682 |
+
"day": "2025-05-26",
|
| 3683 |
+
"runs": 25,
|
| 3684 |
+
"reward": 0.957344,
|
| 3685 |
+
"submitted": true,
|
| 3686 |
+
"feasible": true,
|
| 3687 |
+
"cost": 1018,
|
| 3688 |
+
"optimal_cost": 1014,
|
| 3689 |
+
"floor_cost": 968,
|
| 3690 |
+
"greedy_cost": null,
|
| 3691 |
+
"real_cost": 41,
|
| 3692 |
+
"hardness": {
|
| 3693 |
+
"runs": 25,
|
| 3694 |
+
"sets": 7,
|
| 3695 |
+
"stops": 158,
|
| 3696 |
+
"conflicts": 17,
|
| 3697 |
+
"single_track": 0,
|
| 3698 |
+
"headway": 0,
|
| 3699 |
+
"platforms": 0,
|
| 3700 |
+
"closed": 17,
|
| 3701 |
+
"decisions": 31,
|
| 3702 |
+
"holds": 17,
|
| 3703 |
+
"short_turns": 12,
|
| 3704 |
+
"cancels": 2,
|
| 3705 |
+
"turn_points": 1,
|
| 3706 |
+
"avoidable": 46,
|
| 3707 |
+
"optimal": 1014,
|
| 3708 |
+
"greedy_gap": 0.824,
|
| 3709 |
+
"closed_min": 130,
|
| 3710 |
+
"slow_min": 0,
|
| 3711 |
+
"track_min": 0,
|
| 3712 |
+
"single_closed": true,
|
| 3713 |
+
"mid_line": true,
|
| 3714 |
+
"late_sets": 0
|
| 3715 |
+
},
|
| 3716 |
+
"violations": 0,
|
| 3717 |
+
"agent_turns": 6,
|
| 3718 |
+
"checks": 4,
|
| 3719 |
+
"seconds": 1307.8,
|
| 3720 |
+
"input_tokens": 506659,
|
| 3721 |
+
"cache_read_tokens": 369152,
|
| 3722 |
+
"cache_write_tokens": 0,
|
| 3723 |
+
"output_tokens": 136057,
|
| 3724 |
+
"served_by": [
|
| 3725 |
+
"zai-org/GLM-5.3-Flash"
|
| 3726 |
+
],
|
| 3727 |
+
"end_reason": "submitted",
|
| 3728 |
+
"errors": 0,
|
| 3729 |
+
"plan": [
|
| 3730 |
+
{
|
| 3731 |
+
"train": "37357",
|
| 3732 |
+
"to": "HDG"
|
| 3733 |
+
},
|
| 3734 |
+
{
|
| 3735 |
+
"train": "37459",
|
| 3736 |
+
"to": "HDG"
|
| 3737 |
+
},
|
| 3738 |
+
{
|
| 3739 |
+
"train": "37359",
|
| 3740 |
+
"to": "HDG"
|
| 3741 |
+
},
|
| 3742 |
+
{
|
| 3743 |
+
"train": "37461",
|
| 3744 |
+
"to": "HDG"
|
| 3745 |
+
},
|
| 3746 |
+
{
|
| 3747 |
+
"train": "37361",
|
| 3748 |
+
"to": "HDG"
|
| 3749 |
+
},
|
| 3750 |
+
{
|
| 3751 |
+
"train": "37463",
|
| 3752 |
+
"to": "HDG"
|
| 3753 |
+
},
|
| 3754 |
+
{
|
| 3755 |
+
"train": "37465",
|
| 3756 |
+
"to": "HDG"
|
| 3757 |
+
},
|
| 3758 |
+
{
|
| 3759 |
+
"train": "37468",
|
| 3760 |
+
"from": "HDG",
|
| 3761 |
+
"hold": {
|
| 3762 |
+
"HDG": "18:29"
|
| 3763 |
+
}
|
| 3764 |
+
},
|
| 3765 |
+
{
|
| 3766 |
+
"train": "37370",
|
| 3767 |
+
"from": "HDG",
|
| 3768 |
+
"hold": {
|
| 3769 |
+
"HDG": "18:51"
|
| 3770 |
+
}
|
| 3771 |
+
},
|
| 3772 |
+
{
|
| 3773 |
+
"train": "37470",
|
| 3774 |
+
"from": "HDG",
|
| 3775 |
+
"hold": {
|
| 3776 |
+
"HDG": "18:59"
|
| 3777 |
+
}
|
| 3778 |
+
},
|
| 3779 |
+
{
|
| 3780 |
+
"train": "37372",
|
| 3781 |
+
"from": "HDG",
|
| 3782 |
+
"hold": {
|
| 3783 |
+
"HDG": "19:21"
|
| 3784 |
+
}
|
| 3785 |
+
},
|
| 3786 |
+
{
|
| 3787 |
+
"train": "37472",
|
| 3788 |
+
"from": "HDG",
|
| 3789 |
+
"hold": {
|
| 3790 |
+
"HDG": "19:29"
|
| 3791 |
+
}
|
| 3792 |
+
},
|
| 3793 |
+
{
|
| 3794 |
+
"train": "37374",
|
| 3795 |
+
"from": "HDG",
|
| 3796 |
+
"hold": {
|
| 3797 |
+
"HDG": "19:51"
|
| 3798 |
+
}
|
| 3799 |
+
},
|
| 3800 |
+
{
|
| 3801 |
+
"train": "37376",
|
| 3802 |
+
"from": "HDG",
|
| 3803 |
+
"hold": {
|
| 3804 |
+
"HDG": "20:24",
|
| 3805 |
+
"GK": "20:41"
|
| 3806 |
+
}
|
| 3807 |
+
},
|
| 3808 |
+
{
|
| 3809 |
+
"train": "37467",
|
| 3810 |
+
"hold": {
|
| 3811 |
+
"FWD": "20:27"
|
| 3812 |
+
}
|
| 3813 |
+
},
|
| 3814 |
+
{
|
| 3815 |
+
"train": "37365",
|
| 3816 |
+
"hold": {
|
| 3817 |
+
"FWD": "20:22"
|
| 3818 |
+
}
|
| 3819 |
+
},
|
| 3820 |
+
{
|
| 3821 |
+
"train": "37474",
|
| 3822 |
+
"hold": {
|
| 3823 |
+
"LWC": "20:15"
|
| 3824 |
+
}
|
| 3825 |
+
},
|
| 3826 |
+
{
|
| 3827 |
+
"train": "37368",
|
| 3828 |
+
"cancel": true
|
| 3829 |
+
},
|
| 3830 |
+
{
|
| 3831 |
+
"train": "37363",
|
| 3832 |
+
"cancel": true
|
| 3833 |
+
}
|
| 3834 |
+
]
|
| 3835 |
+
},
|
| 3836 |
+
{
|
| 3837 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 3838 |
+
"task_id": "gnlw-2025-05-27-0830-210m-blockage-0",
|
| 3839 |
+
"split": "eval",
|
| 3840 |
+
"difficulty": "blockage",
|
| 3841 |
+
"day": "2025-05-27",
|
| 3842 |
+
"runs": 34,
|
| 3843 |
+
"reward": 0.204715,
|
| 3844 |
+
"submitted": true,
|
| 3845 |
+
"feasible": true,
|
| 3846 |
+
"cost": 1121,
|
| 3847 |
+
"optimal_cost": 795,
|
| 3848 |
+
"floor_cost": 768,
|
| 3849 |
+
"greedy_cost": null,
|
| 3850 |
+
"real_cost": 285,
|
| 3851 |
+
"hardness": {
|
| 3852 |
+
"runs": 34,
|
| 3853 |
+
"sets": 7,
|
| 3854 |
+
"stops": 226,
|
| 3855 |
+
"conflicts": 28,
|
| 3856 |
+
"single_track": 1,
|
| 3857 |
+
"headway": 0,
|
| 3858 |
+
"platforms": 0,
|
| 3859 |
+
"closed": 27,
|
| 3860 |
+
"decisions": 44,
|
| 3861 |
+
"holds": 30,
|
| 3862 |
+
"short_turns": 12,
|
| 3863 |
+
"cancels": 2,
|
| 3864 |
+
"turn_points": 1,
|
| 3865 |
+
"avoidable": 27,
|
| 3866 |
+
"optimal": 795,
|
| 3867 |
+
"greedy_gap": 0.8529,
|
| 3868 |
+
"closed_min": 98,
|
| 3869 |
+
"slow_min": 0,
|
| 3870 |
+
"track_min": 0,
|
| 3871 |
+
"single_closed": true,
|
| 3872 |
+
"mid_line": false,
|
| 3873 |
+
"late_sets": 0
|
| 3874 |
+
},
|
| 3875 |
+
"violations": 0,
|
| 3876 |
+
"agent_turns": 5,
|
| 3877 |
+
"checks": 3,
|
| 3878 |
+
"seconds": 1610.8,
|
| 3879 |
+
"input_tokens": 428158,
|
| 3880 |
+
"cache_read_tokens": 264576,
|
| 3881 |
+
"cache_write_tokens": 0,
|
| 3882 |
+
"output_tokens": 170879,
|
| 3883 |
+
"served_by": [
|
| 3884 |
+
"zai-org/GLM-5.3-Flash"
|
| 3885 |
+
],
|
| 3886 |
+
"end_reason": "submitted",
|
| 3887 |
+
"errors": 0,
|
| 3888 |
+
"plan": [
|
| 3889 |
+
{
|
| 3890 |
+
"hold": {
|
| 3891 |
+
"HDG": "08:32"
|
| 3892 |
+
},
|
| 3893 |
+
"to": "ZH",
|
| 3894 |
+
"train": "37428"
|
| 3895 |
+
},
|
| 3896 |
+
{
|
| 3897 |
+
"from": "ZH",
|
| 3898 |
+
"train": "37425"
|
| 3899 |
+
},
|
| 3900 |
+
{
|
| 3901 |
+
"hold": {
|
| 3902 |
+
"FWD": "08:35"
|
| 3903 |
+
},
|
| 3904 |
+
"train": "37319"
|
| 3905 |
+
},
|
| 3906 |
+
{
|
| 3907 |
+
"hold": {
|
| 3908 |
+
"BP": "08:36"
|
| 3909 |
+
},
|
| 3910 |
+
"train": "37328"
|
| 3911 |
+
},
|
| 3912 |
+
{
|
| 3913 |
+
"hold": {
|
| 3914 |
+
"BP": "09:29"
|
| 3915 |
+
},
|
| 3916 |
+
"to": "ZH",
|
| 3917 |
+
"train": "37330"
|
| 3918 |
+
},
|
| 3919 |
+
{
|
| 3920 |
+
"from": "ZH",
|
| 3921 |
+
"train": "37325"
|
| 3922 |
+
},
|
| 3923 |
+
{
|
| 3924 |
+
"to": "ZH",
|
| 3925 |
+
"train": "37430"
|
| 3926 |
+
},
|
| 3927 |
+
{
|
| 3928 |
+
"from": "ZH",
|
| 3929 |
+
"train": "37427"
|
| 3930 |
+
},
|
| 3931 |
+
{
|
| 3932 |
+
"hold": {
|
| 3933 |
+
"BP": "10:01"
|
| 3934 |
+
},
|
| 3935 |
+
"to": "ZH",
|
| 3936 |
+
"train": "37332"
|
| 3937 |
+
},
|
| 3938 |
+
{
|
| 3939 |
+
"from": "ZH",
|
| 3940 |
+
"train": "37327"
|
| 3941 |
+
},
|
| 3942 |
+
{
|
| 3943 |
+
"to": "ZH",
|
| 3944 |
+
"train": "37432"
|
| 3945 |
+
},
|
| 3946 |
+
{
|
| 3947 |
+
"from": "ZH",
|
| 3948 |
+
"train": "37429"
|
| 3949 |
+
},
|
| 3950 |
+
{
|
| 3951 |
+
"cancel": true,
|
| 3952 |
+
"train": "37423"
|
| 3953 |
+
},
|
| 3954 |
+
{
|
| 3955 |
+
"cancel": true,
|
| 3956 |
+
"train": "37334"
|
| 3957 |
+
},
|
| 3958 |
+
{
|
| 3959 |
+
"cancel": true,
|
| 3960 |
+
"train": "37323"
|
| 3961 |
+
},
|
| 3962 |
+
{
|
| 3963 |
+
"cancel": true,
|
| 3964 |
+
"train": "37434"
|
| 3965 |
+
}
|
| 3966 |
+
]
|
| 3967 |
+
},
|
| 3968 |
+
{
|
| 3969 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 3970 |
+
"task_id": "gnlw-2025-05-27-1700-180m-blockage-0",
|
| 3971 |
+
"split": "eval",
|
| 3972 |
+
"difficulty": "blockage",
|
| 3973 |
+
"day": "2025-05-27",
|
| 3974 |
+
"runs": 30,
|
| 3975 |
+
"reward": 0.06,
|
| 3976 |
+
"submitted": true,
|
| 3977 |
+
"feasible": false,
|
| 3978 |
+
"cost": null,
|
| 3979 |
+
"optimal_cost": 791,
|
| 3980 |
+
"floor_cost": 670,
|
| 3981 |
+
"greedy_cost": null,
|
| 3982 |
+
"real_cost": 253,
|
| 3983 |
+
"hardness": {
|
| 3984 |
+
"runs": 30,
|
| 3985 |
+
"sets": 7,
|
| 3986 |
+
"stops": 200,
|
| 3987 |
+
"conflicts": 17,
|
| 3988 |
+
"single_track": 0,
|
| 3989 |
+
"headway": 0,
|
| 3990 |
+
"platforms": 0,
|
| 3991 |
+
"closed": 17,
|
| 3992 |
+
"decisions": 27,
|
| 3993 |
+
"holds": 21,
|
| 3994 |
+
"short_turns": 6,
|
| 3995 |
+
"cancels": 0,
|
| 3996 |
+
"turn_points": 2,
|
| 3997 |
+
"avoidable": 121,
|
| 3998 |
+
"optimal": 791,
|
| 3999 |
+
"greedy_gap": 0.8467,
|
| 4000 |
+
"closed_min": 35,
|
| 4001 |
+
"slow_min": 0,
|
| 4002 |
+
"track_min": 0,
|
| 4003 |
+
"single_closed": true,
|
| 4004 |
+
"mid_line": true,
|
| 4005 |
+
"late_sets": 0
|
| 4006 |
+
},
|
| 4007 |
+
"violations": 29,
|
| 4008 |
+
"agent_turns": 12,
|
| 4009 |
+
"checks": 7,
|
| 4010 |
+
"seconds": 3352.7,
|
| 4011 |
+
"input_tokens": 2662189,
|
| 4012 |
+
"cache_read_tokens": 2171520,
|
| 4013 |
+
"cache_write_tokens": 0,
|
| 4014 |
+
"output_tokens": 434564,
|
| 4015 |
+
"served_by": [
|
| 4016 |
+
"zai-org/GLM-5.3-Flash"
|
| 4017 |
+
],
|
| 4018 |
+
"end_reason": "submitted",
|
| 4019 |
+
"errors": 0,
|
| 4020 |
+
"plan": [
|
| 4021 |
+
{
|
| 4022 |
+
"hold": {
|
| 4023 |
+
"ZH": "17:55"
|
| 4024 |
+
},
|
| 4025 |
+
"train": "37355"
|
| 4026 |
+
},
|
| 4027 |
+
{
|
| 4028 |
+
"hold": {
|
| 4029 |
+
"ZH": "18:02"
|
| 4030 |
+
},
|
| 4031 |
+
"train": "37457"
|
| 4032 |
+
},
|
| 4033 |
+
{
|
| 4034 |
+
"hold": {
|
| 4035 |
+
"BP": "17:58"
|
| 4036 |
+
},
|
| 4037 |
+
"train": "37364"
|
| 4038 |
+
},
|
| 4039 |
+
{
|
| 4040 |
+
"hold": {
|
| 4041 |
+
"BP": "18:04"
|
| 4042 |
+
},
|
| 4043 |
+
"to": "ZH",
|
| 4044 |
+
"train": "37462"
|
| 4045 |
+
},
|
| 4046 |
+
{
|
| 4047 |
+
"hold": {
|
| 4048 |
+
"GN": "17:49",
|
| 4049 |
+
"ZH": "18:20"
|
| 4050 |
+
},
|
| 4051 |
+
"train": "37357"
|
| 4052 |
+
},
|
| 4053 |
+
{
|
| 4054 |
+
"hold": {
|
| 4055 |
+
"DWE": "18:04",
|
| 4056 |
+
"GK": "18:33"
|
| 4057 |
+
},
|
| 4058 |
+
"train": "37464"
|
| 4059 |
+
},
|
| 4060 |
+
{
|
| 4061 |
+
"from": "ZH",
|
| 4062 |
+
"hold": {
|
| 4063 |
+
"FWD": "18:58",
|
| 4064 |
+
"ZH": "18:41"
|
| 4065 |
+
},
|
| 4066 |
+
"train": "37459"
|
| 4067 |
+
},
|
| 4068 |
+
{
|
| 4069 |
+
"hold": {
|
| 4070 |
+
"FWD": "19:18",
|
| 4071 |
+
"ZH": "18:55"
|
| 4072 |
+
},
|
| 4073 |
+
"train": "37461"
|
| 4074 |
+
},
|
| 4075 |
+
{
|
| 4076 |
+
"hold": {
|
| 4077 |
+
"FWD": "19:24",
|
| 4078 |
+
"ZH": "19:05"
|
| 4079 |
+
},
|
| 4080 |
+
"train": "37359"
|
| 4081 |
+
},
|
| 4082 |
+
{
|
| 4083 |
+
"hold": {
|
| 4084 |
+
"LW": "18:30"
|
| 4085 |
+
},
|
| 4086 |
+
"to": "BP",
|
| 4087 |
+
"train": "37466"
|
| 4088 |
+
},
|
| 4089 |
+
{
|
| 4090 |
+
"hold": {
|
| 4091 |
+
"BP": "18:58",
|
| 4092 |
+
"FWD": "18:02"
|
| 4093 |
+
},
|
| 4094 |
+
"train": "37366"
|
| 4095 |
+
},
|
| 4096 |
+
{
|
| 4097 |
+
"hold": {
|
| 4098 |
+
"BP": "19:12",
|
| 4099 |
+
"GK": "19:27",
|
| 4100 |
+
"LW": "18:38"
|
| 4101 |
+
},
|
| 4102 |
+
"train": "37368"
|
| 4103 |
+
},
|
| 4104 |
+
{
|
| 4105 |
+
"hold": {
|
| 4106 |
+
"GK": "19:36"
|
| 4107 |
+
},
|
| 4108 |
+
"train": "37370"
|
| 4109 |
+
},
|
| 4110 |
+
{
|
| 4111 |
+
"from": "BP",
|
| 4112 |
+
"hold": {
|
| 4113 |
+
"BP": "19:16",
|
| 4114 |
+
"FWD": "19:36",
|
| 4115 |
+
"HDG": "19:49"
|
| 4116 |
+
},
|
| 4117 |
+
"train": "37463"
|
| 4118 |
+
},
|
| 4119 |
+
{
|
| 4120 |
+
"hold": {
|
| 4121 |
+
"GK": "19:55"
|
| 4122 |
+
},
|
| 4123 |
+
"train": "37470"
|
| 4124 |
+
},
|
| 4125 |
+
{
|
| 4126 |
+
"hold": {
|
| 4127 |
+
"FWD": "20:23",
|
| 4128 |
+
"ZH": "19:57"
|
| 4129 |
+
},
|
| 4130 |
+
"train": "37363"
|
| 4131 |
+
},
|
| 4132 |
+
{
|
| 4133 |
+
"hold": {
|
| 4134 |
+
"ZH": "19:45"
|
| 4135 |
+
},
|
| 4136 |
+
"train": "37465"
|
| 4137 |
+
},
|
| 4138 |
+
{
|
| 4139 |
+
"hold": {
|
| 4140 |
+
"GN": "20:16"
|
| 4141 |
+
},
|
| 4142 |
+
"train": "37365"
|
| 4143 |
+
},
|
| 4144 |
+
{
|
| 4145 |
+
"hold": {
|
| 4146 |
+
"BP": "20:15",
|
| 4147 |
+
"GK": "20:15",
|
| 4148 |
+
"LW": "19:36"
|
| 4149 |
+
},
|
| 4150 |
+
"train": "37372"
|
| 4151 |
+
},
|
| 4152 |
+
{
|
| 4153 |
+
"hold": {
|
| 4154 |
+
"BP": "20:32",
|
| 4155 |
+
"GK": "20:46"
|
| 4156 |
+
},
|
| 4157 |
+
"train": "37472"
|
| 4158 |
+
},
|
| 4159 |
+
{
|
| 4160 |
+
"hold": {
|
| 4161 |
+
"BP": "20:24",
|
| 4162 |
+
"GK": "20:31"
|
| 4163 |
+
},
|
| 4164 |
+
"train": "37374"
|
| 4165 |
+
},
|
| 4166 |
+
{
|
| 4167 |
+
"hold": {
|
| 4168 |
+
"BP": "20:22",
|
| 4169 |
+
"GK": "20:29"
|
| 4170 |
+
},
|
| 4171 |
+
"train": "37474"
|
| 4172 |
+
},
|
| 4173 |
+
{
|
| 4174 |
+
"hold": {
|
| 4175 |
+
"ZH": "20:38"
|
| 4176 |
+
},
|
| 4177 |
+
"train": "37467"
|
| 4178 |
+
}
|
| 4179 |
+
]
|
| 4180 |
+
},
|
| 4181 |
+
{
|
| 4182 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 4183 |
+
"task_id": "gnlw-2025-05-27-1830-180m-blockage-0",
|
| 4184 |
+
"split": "eval",
|
| 4185 |
+
"difficulty": "blockage",
|
| 4186 |
+
"day": "2025-05-27",
|
| 4187 |
+
"runs": 27,
|
| 4188 |
+
"reward": 0.0,
|
| 4189 |
+
"submitted": false,
|
| 4190 |
+
"feasible": false,
|
| 4191 |
+
"cost": null,
|
| 4192 |
+
"optimal_cost": 596,
|
| 4193 |
+
"floor_cost": 560,
|
| 4194 |
+
"greedy_cost": null,
|
| 4195 |
+
"real_cost": 170,
|
| 4196 |
+
"hardness": {
|
| 4197 |
+
"runs": 27,
|
| 4198 |
+
"sets": 7,
|
| 4199 |
+
"stops": 162,
|
| 4200 |
+
"conflicts": 25,
|
| 4201 |
+
"single_track": 8,
|
| 4202 |
+
"headway": 0,
|
| 4203 |
+
"platforms": 0,
|
| 4204 |
+
"closed": 17,
|
| 4205 |
+
"decisions": 27,
|
| 4206 |
+
"holds": 17,
|
| 4207 |
+
"short_turns": 10,
|
| 4208 |
+
"cancels": 0,
|
| 4209 |
+
"turn_points": 1,
|
| 4210 |
+
"avoidable": 36,
|
| 4211 |
+
"optimal": 596,
|
| 4212 |
+
"greedy_gap": 0.8593,
|
| 4213 |
+
"closed_min": 59,
|
| 4214 |
+
"slow_min": 0,
|
| 4215 |
+
"track_min": 0,
|
| 4216 |
+
"single_closed": true,
|
| 4217 |
+
"mid_line": false,
|
| 4218 |
+
"late_sets": 1
|
| 4219 |
+
},
|
| 4220 |
+
"violations": 0,
|
| 4221 |
+
"agent_turns": 3,
|
| 4222 |
+
"checks": 0,
|
| 4223 |
+
"seconds": 1204.6,
|
| 4224 |
+
"input_tokens": 88076,
|
| 4225 |
+
"cache_read_tokens": 11456,
|
| 4226 |
+
"cache_write_tokens": 0,
|
| 4227 |
+
"output_tokens": 128020,
|
| 4228 |
+
"served_by": [
|
| 4229 |
+
"zai-org/GLM-5.3-Flash"
|
| 4230 |
+
],
|
| 4231 |
+
"end_reason": "no_tool_call",
|
| 4232 |
+
"errors": 0,
|
| 4233 |
+
"plan": null
|
| 4234 |
+
},
|
| 4235 |
+
{
|
| 4236 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 4237 |
+
"task_id": "gnlw-2025-05-28-1830-210m-compound-0",
|
| 4238 |
+
"split": "eval",
|
| 4239 |
+
"difficulty": "compound",
|
| 4240 |
+
"day": "2025-05-28",
|
| 4241 |
+
"runs": 31,
|
| 4242 |
+
"reward": 0.740479,
|
| 4243 |
+
"submitted": true,
|
| 4244 |
+
"feasible": true,
|
| 4245 |
+
"cost": 466,
|
| 4246 |
+
"optimal_cost": 436,
|
| 4247 |
+
"floor_cost": 383,
|
| 4248 |
+
"greedy_cost": 1271,
|
| 4249 |
+
"real_cost": 69,
|
| 4250 |
+
"hardness": {
|
| 4251 |
+
"runs": 31,
|
| 4252 |
+
"sets": 7,
|
| 4253 |
+
"stops": 186,
|
| 4254 |
+
"conflicts": 23,
|
| 4255 |
+
"single_track": 10,
|
| 4256 |
+
"headway": 11,
|
| 4257 |
+
"platforms": 1,
|
| 4258 |
+
"closed": 1,
|
| 4259 |
+
"decisions": 36,
|
| 4260 |
+
"holds": 32,
|
| 4261 |
+
"short_turns": 4,
|
| 4262 |
+
"cancels": 0,
|
| 4263 |
+
"turn_points": 1,
|
| 4264 |
+
"avoidable": 53,
|
| 4265 |
+
"optimal": 436,
|
| 4266 |
+
"greedy_gap": 0.8,
|
| 4267 |
+
"closed_min": 10,
|
| 4268 |
+
"slow_min": 0,
|
| 4269 |
+
"track_min": 0,
|
| 4270 |
+
"single_closed": true,
|
| 4271 |
+
"mid_line": false,
|
| 4272 |
+
"late_sets": 1
|
| 4273 |
+
},
|
| 4274 |
+
"violations": 0,
|
| 4275 |
+
"agent_turns": 11,
|
| 4276 |
+
"checks": 8,
|
| 4277 |
+
"seconds": 2370.8,
|
| 4278 |
+
"input_tokens": 2102819,
|
| 4279 |
+
"cache_read_tokens": 1567360,
|
| 4280 |
+
"cache_write_tokens": 0,
|
| 4281 |
+
"output_tokens": 266912,
|
| 4282 |
+
"served_by": [
|
| 4283 |
+
"zai-org/GLM-5.3-Flash"
|
| 4284 |
+
],
|
| 4285 |
+
"end_reason": "submitted",
|
| 4286 |
+
"errors": 0,
|
| 4287 |
+
"plan": [
|
| 4288 |
+
{
|
| 4289 |
+
"train": "37368",
|
| 4290 |
+
"hold": {
|
| 4291 |
+
"BP": "18:43"
|
| 4292 |
+
}
|
| 4293 |
+
},
|
| 4294 |
+
{
|
| 4295 |
+
"train": "37468",
|
| 4296 |
+
"hold": {
|
| 4297 |
+
"BP": "18:46",
|
| 4298 |
+
"ZH": "18:59"
|
| 4299 |
+
}
|
| 4300 |
+
},
|
| 4301 |
+
{
|
| 4302 |
+
"train": "37359",
|
| 4303 |
+
"to": "HDG"
|
| 4304 |
+
},
|
| 4305 |
+
{
|
| 4306 |
+
"train": "37470",
|
| 4307 |
+
"from": "HDG"
|
| 4308 |
+
},
|
| 4309 |
+
{
|
| 4310 |
+
"train": "37361",
|
| 4311 |
+
"to": "HDG",
|
| 4312 |
+
"hold": {
|
| 4313 |
+
"GN": "19:00"
|
| 4314 |
+
}
|
| 4315 |
+
},
|
| 4316 |
+
{
|
| 4317 |
+
"train": "37472",
|
| 4318 |
+
"from": "HDG",
|
| 4319 |
+
"hold": {
|
| 4320 |
+
"HDG": "19:34"
|
| 4321 |
+
}
|
| 4322 |
+
},
|
| 4323 |
+
{
|
| 4324 |
+
"train": "37370",
|
| 4325 |
+
"hold": {
|
| 4326 |
+
"HDG": "18:55",
|
| 4327 |
+
"GK": "19:13"
|
| 4328 |
+
}
|
| 4329 |
+
},
|
| 4330 |
+
{
|
| 4331 |
+
"train": "37463",
|
| 4332 |
+
"hold": {
|
| 4333 |
+
"GN": "19:03"
|
| 4334 |
+
}
|
| 4335 |
+
},
|
| 4336 |
+
{
|
| 4337 |
+
"train": "37363",
|
| 4338 |
+
"hold": {
|
| 4339 |
+
"ZH": "19:19",
|
| 4340 |
+
"FWD": "19:37"
|
| 4341 |
+
}
|
| 4342 |
+
},
|
| 4343 |
+
{
|
| 4344 |
+
"train": "37465",
|
| 4345 |
+
"hold": {
|
| 4346 |
+
"GN": "19:25",
|
| 4347 |
+
"FWD": "19:56"
|
| 4348 |
+
}
|
| 4349 |
+
},
|
| 4350 |
+
{
|
| 4351 |
+
"train": "37467",
|
| 4352 |
+
"hold": {
|
| 4353 |
+
"FWD": "20:25"
|
| 4354 |
+
}
|
| 4355 |
+
},
|
| 4356 |
+
{
|
| 4357 |
+
"train": "37374"
|
| 4358 |
+
},
|
| 4359 |
+
{
|
| 4360 |
+
"train": "37474",
|
| 4361 |
+
"hold": {
|
| 4362 |
+
"LW": "19:50"
|
| 4363 |
+
}
|
| 4364 |
+
},
|
| 4365 |
+
{
|
| 4366 |
+
"train": "37365",
|
| 4367 |
+
"hold": {
|
| 4368 |
+
"FWD": "20:05"
|
| 4369 |
+
}
|
| 4370 |
+
},
|
| 4371 |
+
{
|
| 4372 |
+
"train": "37471",
|
| 4373 |
+
"hold": {
|
| 4374 |
+
"FWD": "21:25"
|
| 4375 |
+
}
|
| 4376 |
+
},
|
| 4377 |
+
{
|
| 4378 |
+
"train": "37373",
|
| 4379 |
+
"hold": {
|
| 4380 |
+
"FWD": "22:04"
|
| 4381 |
+
}
|
| 4382 |
+
}
|
| 4383 |
+
]
|
| 4384 |
+
},
|
| 4385 |
+
{
|
| 4386 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 4387 |
+
"task_id": "gnlw-2025-05-30-0630-180m-blockage-0",
|
| 4388 |
+
"split": "eval",
|
| 4389 |
+
"difficulty": "blockage",
|
| 4390 |
+
"day": "2025-05-30",
|
| 4391 |
+
"runs": 29,
|
| 4392 |
+
"reward": 0.0,
|
| 4393 |
+
"submitted": false,
|
| 4394 |
+
"feasible": false,
|
| 4395 |
+
"cost": null,
|
| 4396 |
+
"optimal_cost": 454,
|
| 4397 |
+
"floor_cost": 385,
|
| 4398 |
+
"greedy_cost": 1397,
|
| 4399 |
+
"real_cost": 1128,
|
| 4400 |
+
"hardness": {
|
| 4401 |
+
"runs": 29,
|
| 4402 |
+
"sets": 7,
|
| 4403 |
+
"stops": 201,
|
| 4404 |
+
"conflicts": 13,
|
| 4405 |
+
"single_track": 1,
|
| 4406 |
+
"headway": 0,
|
| 4407 |
+
"platforms": 0,
|
| 4408 |
+
"closed": 12,
|
| 4409 |
+
"decisions": 37,
|
| 4410 |
+
"holds": 33,
|
| 4411 |
+
"short_turns": 4,
|
| 4412 |
+
"cancels": 0,
|
| 4413 |
+
"turn_points": 2,
|
| 4414 |
+
"avoidable": 69,
|
| 4415 |
+
"optimal": 454,
|
| 4416 |
+
"greedy_gap": 0.8,
|
| 4417 |
+
"closed_min": 28,
|
| 4418 |
+
"slow_min": 0,
|
| 4419 |
+
"track_min": 0,
|
| 4420 |
+
"single_closed": true,
|
| 4421 |
+
"mid_line": true,
|
| 4422 |
+
"late_sets": 0
|
| 4423 |
+
},
|
| 4424 |
+
"violations": 0,
|
| 4425 |
+
"agent_turns": 2,
|
| 4426 |
+
"checks": 0,
|
| 4427 |
+
"seconds": 1182.1,
|
| 4428 |
+
"input_tokens": 75775,
|
| 4429 |
+
"cache_read_tokens": 6080,
|
| 4430 |
+
"cache_write_tokens": 0,
|
| 4431 |
+
"output_tokens": 128000,
|
| 4432 |
+
"served_by": [
|
| 4433 |
+
"zai-org/GLM-5.3-Flash"
|
| 4434 |
+
],
|
| 4435 |
+
"end_reason": "no_tool_call",
|
| 4436 |
+
"errors": 0,
|
| 4437 |
+
"plan": null
|
| 4438 |
+
},
|
| 4439 |
+
{
|
| 4440 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 4441 |
+
"task_id": "gnlw-2025-05-31-0800-210m-compound-0",
|
| 4442 |
+
"split": "eval",
|
| 4443 |
+
"difficulty": "compound",
|
| 4444 |
+
"day": "2025-05-31",
|
| 4445 |
+
"runs": 32,
|
| 4446 |
+
"reward": 0.1875,
|
| 4447 |
+
"submitted": true,
|
| 4448 |
+
"feasible": false,
|
| 4449 |
+
"cost": null,
|
| 4450 |
+
"optimal_cost": 1179,
|
| 4451 |
+
"floor_cost": 1017,
|
| 4452 |
+
"greedy_cost": 5859,
|
| 4453 |
+
"real_cost": 90,
|
| 4454 |
+
"hardness": {
|
| 4455 |
+
"runs": 32,
|
| 4456 |
+
"sets": 7,
|
| 4457 |
+
"stops": 218,
|
| 4458 |
+
"conflicts": 43,
|
| 4459 |
+
"single_track": 6,
|
| 4460 |
+
"headway": 16,
|
| 4461 |
+
"platforms": 2,
|
| 4462 |
+
"closed": 19,
|
| 4463 |
+
"decisions": 30,
|
| 4464 |
+
"holds": 20,
|
| 4465 |
+
"short_turns": 10,
|
| 4466 |
+
"cancels": 0,
|
| 4467 |
+
"turn_points": 2,
|
| 4468 |
+
"avoidable": 162,
|
| 4469 |
+
"optimal": 1179,
|
| 4470 |
+
"greedy_gap": 0.8,
|
| 4471 |
+
"closed_min": 39,
|
| 4472 |
+
"slow_min": 0,
|
| 4473 |
+
"track_min": 0,
|
| 4474 |
+
"single_closed": true,
|
| 4475 |
+
"mid_line": false,
|
| 4476 |
+
"late_sets": 2
|
| 4477 |
+
},
|
| 4478 |
+
"violations": 3,
|
| 4479 |
+
"agent_turns": 12,
|
| 4480 |
+
"checks": 7,
|
| 4481 |
+
"seconds": 3479.2,
|
| 4482 |
+
"input_tokens": 2488326,
|
| 4483 |
+
"cache_read_tokens": 1455168,
|
| 4484 |
+
"cache_write_tokens": 0,
|
| 4485 |
+
"output_tokens": 335738,
|
| 4486 |
+
"served_by": [
|
| 4487 |
+
"zai-org/GLM-5.3-Flash"
|
| 4488 |
+
],
|
| 4489 |
+
"end_reason": "submitted",
|
| 4490 |
+
"errors": 0,
|
| 4491 |
+
"plan": [
|
| 4492 |
+
{
|
| 4493 |
+
"hold": {
|
| 4494 |
+
"GK": "08:49"
|
| 4495 |
+
},
|
| 4496 |
+
"train": "37419"
|
| 4497 |
+
},
|
| 4498 |
+
{
|
| 4499 |
+
"to": "BP",
|
| 4500 |
+
"train": "37426"
|
| 4501 |
+
},
|
| 4502 |
+
{
|
| 4503 |
+
"from": "BP",
|
| 4504 |
+
"hold": {
|
| 4505 |
+
"BP": "09:17",
|
| 4506 |
+
"FWD": "09:30"
|
| 4507 |
+
},
|
| 4508 |
+
"train": "37423"
|
| 4509 |
+
},
|
| 4510 |
+
{
|
| 4511 |
+
"hold": {
|
| 4512 |
+
"GN": "08:49"
|
| 4513 |
+
},
|
| 4514 |
+
"to": "BP",
|
| 4515 |
+
"train": "37319"
|
| 4516 |
+
},
|
| 4517 |
+
{
|
| 4518 |
+
"from": "BP",
|
| 4519 |
+
"train": "37430"
|
| 4520 |
+
},
|
| 4521 |
+
{
|
| 4522 |
+
"hold": {
|
| 4523 |
+
"GN": "08:52",
|
| 4524 |
+
"GK": "09:08"
|
| 4525 |
+
},
|
| 4526 |
+
"to": "BP",
|
| 4527 |
+
"train": "37421"
|
| 4528 |
+
},
|
| 4529 |
+
{
|
| 4530 |
+
"from": "BP",
|
| 4531 |
+
"train": "37332"
|
| 4532 |
+
},
|
| 4533 |
+
{
|
| 4534 |
+
"hold": {
|
| 4535 |
+
"FWD": "08:50"
|
| 4536 |
+
},
|
| 4537 |
+
"train": "37328"
|
| 4538 |
+
},
|
| 4539 |
+
{
|
| 4540 |
+
"hold": {
|
| 4541 |
+
"FWD": "08:53",
|
| 4542 |
+
"BP": "09:04"
|
| 4543 |
+
},
|
| 4544 |
+
"train": "37428"
|
| 4545 |
+
},
|
| 4546 |
+
{
|
| 4547 |
+
"hold": {
|
| 4548 |
+
"ZH": "09:07"
|
| 4549 |
+
},
|
| 4550 |
+
"to": "BP",
|
| 4551 |
+
"train": "37321"
|
| 4552 |
+
},
|
| 4553 |
+
{
|
| 4554 |
+
"from": "BP",
|
| 4555 |
+
"train": "37432"
|
| 4556 |
+
},
|
| 4557 |
+
{
|
| 4558 |
+
"hold": {
|
| 4559 |
+
"DWE": "09:39"
|
| 4560 |
+
},
|
| 4561 |
+
"to": "BP",
|
| 4562 |
+
"train": "37330"
|
| 4563 |
+
},
|
| 4564 |
+
{
|
| 4565 |
+
"from": "BP",
|
| 4566 |
+
"hold": {
|
| 4567 |
+
"FWD": "10:11"
|
| 4568 |
+
},
|
| 4569 |
+
"train": "37325"
|
| 4570 |
+
},
|
| 4571 |
+
{
|
| 4572 |
+
"hold": {
|
| 4573 |
+
"ZH": "09:27"
|
| 4574 |
+
},
|
| 4575 |
+
"train": "37323"
|
| 4576 |
+
},
|
| 4577 |
+
{
|
| 4578 |
+
"hold": {
|
| 4579 |
+
"LW": "09:52"
|
| 4580 |
+
},
|
| 4581 |
+
"train": "37334"
|
| 4582 |
+
},
|
| 4583 |
+
{
|
| 4584 |
+
"hold": {
|
| 4585 |
+
"LW": "09:55",
|
| 4586 |
+
"HDG": "10:04"
|
| 4587 |
+
},
|
| 4588 |
+
"train": "37434"
|
| 4589 |
+
},
|
| 4590 |
+
{
|
| 4591 |
+
"hold": {
|
| 4592 |
+
"FWD": "10:01"
|
| 4593 |
+
},
|
| 4594 |
+
"train": "37425"
|
| 4595 |
+
},
|
| 4596 |
+
{
|
| 4597 |
+
"hold": {
|
| 4598 |
+
"FWD": "10:30"
|
| 4599 |
+
},
|
| 4600 |
+
"train": "37427"
|
| 4601 |
+
},
|
| 4602 |
+
{
|
| 4603 |
+
"hold": {
|
| 4604 |
+
"ZH": "10:18",
|
| 4605 |
+
"FWD": "10:36"
|
| 4606 |
+
},
|
| 4607 |
+
"train": "37327"
|
| 4608 |
+
},
|
| 4609 |
+
{
|
| 4610 |
+
"hold": {
|
| 4611 |
+
"GN": "10:26",
|
| 4612 |
+
"ZH": "10:47"
|
| 4613 |
+
},
|
| 4614 |
+
"train": "37429"
|
| 4615 |
+
},
|
| 4616 |
+
{
|
| 4617 |
+
"hold": {
|
| 4618 |
+
"GN": "10:40",
|
| 4619 |
+
"ZH": "10:51",
|
| 4620 |
+
"BP": "11:03"
|
| 4621 |
+
},
|
| 4622 |
+
"train": "37329"
|
| 4623 |
+
},
|
| 4624 |
+
{
|
| 4625 |
+
"hold": {
|
| 4626 |
+
"LW": "10:22"
|
| 4627 |
+
},
|
| 4628 |
+
"train": "37336"
|
| 4629 |
+
},
|
| 4630 |
+
{
|
| 4631 |
+
"hold": {
|
| 4632 |
+
"LW": "10:25",
|
| 4633 |
+
"HDG": "10:33",
|
| 4634 |
+
"GK": "10:54"
|
| 4635 |
+
},
|
| 4636 |
+
"train": "37436"
|
| 4637 |
+
},
|
| 4638 |
+
{
|
| 4639 |
+
"hold": {
|
| 4640 |
+
"FWD": "11:30"
|
| 4641 |
+
},
|
| 4642 |
+
"train": "37431"
|
| 4643 |
+
},
|
| 4644 |
+
{
|
| 4645 |
+
"hold": {
|
| 4646 |
+
"LW": "11:26",
|
| 4647 |
+
"HDG": "11:36"
|
| 4648 |
+
},
|
| 4649 |
+
"train": "37440"
|
| 4650 |
+
}
|
| 4651 |
+
]
|
| 4652 |
+
},
|
| 4653 |
+
{
|
| 4654 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 4655 |
+
"task_id": "gnlw-2025-05-31-0900-180m-compound-0",
|
| 4656 |
+
"split": "eval",
|
| 4657 |
+
"difficulty": "compound",
|
| 4658 |
+
"day": "2025-05-31",
|
| 4659 |
+
"runs": 30,
|
| 4660 |
+
"reward": 0.985586,
|
| 4661 |
+
"submitted": true,
|
| 4662 |
+
"feasible": true,
|
| 4663 |
+
"cost": 227,
|
| 4664 |
+
"optimal_cost": 226,
|
| 4665 |
+
"floor_cost": 216,
|
| 4666 |
+
"greedy_cost": 1092,
|
| 4667 |
+
"real_cost": 76,
|
| 4668 |
+
"hardness": {
|
| 4669 |
+
"runs": 30,
|
| 4670 |
+
"sets": 7,
|
| 4671 |
+
"stops": 200,
|
| 4672 |
+
"conflicts": 14,
|
| 4673 |
+
"single_track": 3,
|
| 4674 |
+
"headway": 5,
|
| 4675 |
+
"platforms": 0,
|
| 4676 |
+
"closed": 6,
|
| 4677 |
+
"decisions": 49,
|
| 4678 |
+
"holds": 45,
|
| 4679 |
+
"short_turns": 4,
|
| 4680 |
+
"cancels": 0,
|
| 4681 |
+
"turn_points": 1,
|
| 4682 |
+
"avoidable": 10,
|
| 4683 |
+
"optimal": 226,
|
| 4684 |
+
"greedy_gap": 0.8,
|
| 4685 |
+
"closed_min": 26,
|
| 4686 |
+
"slow_min": 0,
|
| 4687 |
+
"track_min": 0,
|
| 4688 |
+
"single_closed": true,
|
| 4689 |
+
"mid_line": false,
|
| 4690 |
+
"late_sets": 1
|
| 4691 |
+
},
|
| 4692 |
+
"violations": 0,
|
| 4693 |
+
"agent_turns": 4,
|
| 4694 |
+
"checks": 2,
|
| 4695 |
+
"seconds": 720.7,
|
| 4696 |
+
"input_tokens": 235008,
|
| 4697 |
+
"cache_read_tokens": 142912,
|
| 4698 |
+
"cache_write_tokens": 0,
|
| 4699 |
+
"output_tokens": 89136,
|
| 4700 |
+
"served_by": [
|
| 4701 |
+
"zai-org/GLM-5.3-Flash"
|
| 4702 |
+
],
|
| 4703 |
+
"end_reason": "submitted",
|
| 4704 |
+
"errors": 0,
|
| 4705 |
+
"plan": [
|
| 4706 |
+
{
|
| 4707 |
+
"hold": {
|
| 4708 |
+
"HDG": "09:01"
|
| 4709 |
+
},
|
| 4710 |
+
"train": "37430"
|
| 4711 |
+
},
|
| 4712 |
+
{
|
| 4713 |
+
"hold": {
|
| 4714 |
+
"FWD": "09:05"
|
| 4715 |
+
},
|
| 4716 |
+
"train": "37321"
|
| 4717 |
+
},
|
| 4718 |
+
{
|
| 4719 |
+
"to": "HDG",
|
| 4720 |
+
"train": "37423"
|
| 4721 |
+
},
|
| 4722 |
+
{
|
| 4723 |
+
"from": "HDG",
|
| 4724 |
+
"train": "37334"
|
| 4725 |
+
},
|
| 4726 |
+
{
|
| 4727 |
+
"to": "HDG",
|
| 4728 |
+
"train": "37323"
|
| 4729 |
+
},
|
| 4730 |
+
{
|
| 4731 |
+
"from": "HDG",
|
| 4732 |
+
"hold": {
|
| 4733 |
+
"BP": "10:15"
|
| 4734 |
+
},
|
| 4735 |
+
"train": "37434"
|
| 4736 |
+
},
|
| 4737 |
+
{
|
| 4738 |
+
"hold": {
|
| 4739 |
+
"GN": "09:27"
|
| 4740 |
+
},
|
| 4741 |
+
"train": "37425"
|
| 4742 |
+
}
|
| 4743 |
+
]
|
| 4744 |
+
},
|
| 4745 |
+
{
|
| 4746 |
+
"model": "hf:zai-org/GLM-5.3-Flash:together@default",
|
| 4747 |
+
"task_id": "gnlw-2025-06-22-1600-240m-compound-0",
|
| 4748 |
+
"split": "eval",
|
| 4749 |
+
"difficulty": "compound",
|
| 4750 |
+
"day": "2025-06-22",
|
| 4751 |
+
"runs": 18,
|
| 4752 |
+
"reward": 1.0,
|
| 4753 |
+
"submitted": true,
|
| 4754 |
+
"feasible": true,
|
| 4755 |
+
"cost": 668,
|
| 4756 |
+
"optimal_cost": 668,
|
| 4757 |
+
"floor_cost": 668,
|
| 4758 |
+
"greedy_cost": null,
|
| 4759 |
+
"real_cost": 27,
|
| 4760 |
+
"hardness": {
|
| 4761 |
+
"runs": 18,
|
| 4762 |
+
"sets": 3,
|
| 4763 |
+
"stops": 108,
|
| 4764 |
+
"conflicts": 22,
|
| 4765 |
+
"single_track": 1,
|
| 4766 |
+
"headway": 0,
|
| 4767 |
+
"platforms": 0,
|
| 4768 |
+
"closed": 21,
|
| 4769 |
+
"decisions": 22,
|
| 4770 |
+
"holds": 16,
|
| 4771 |
+
"short_turns": 4,
|
| 4772 |
+
"cancels": 2,
|
| 4773 |
+
"turn_points": 1,
|
| 4774 |
+
"avoidable": 0,
|
| 4775 |
+
"optimal": 668,
|
| 4776 |
+
"greedy_gap": 0.8444,
|
| 4777 |
+
"closed_min": 92,
|
| 4778 |
+
"slow_min": 0,
|
| 4779 |
+
"track_min": 0,
|
| 4780 |
+
"single_closed": true,
|
| 4781 |
+
"mid_line": false,
|
| 4782 |
+
"late_sets": 1
|
| 4783 |
+
},
|
| 4784 |
+
"violations": 0,
|
| 4785 |
+
"agent_turns": 4,
|
| 4786 |
+
"checks": 2,
|
| 4787 |
+
"seconds": 760.2,
|
| 4788 |
+
"input_tokens": 214568,
|
| 4789 |
+
"cache_read_tokens": 139392,
|
| 4790 |
+
"cache_write_tokens": 0,
|
| 4791 |
+
"output_tokens": 66685,
|
| 4792 |
+
"served_by": [
|
| 4793 |
+
"zai-org/GLM-5.3-Flash"
|
| 4794 |
+
],
|
| 4795 |
+
"end_reason": "submitted",
|
| 4796 |
+
"errors": 0,
|
| 4797 |
+
"plan": [
|
| 4798 |
+
{
|
| 4799 |
+
"train": "37458",
|
| 4800 |
+
"to": "BP"
|
| 4801 |
+
},
|
| 4802 |
+
{
|
| 4803 |
+
"train": "37353",
|
| 4804 |
+
"from": "BP"
|
| 4805 |
+
},
|
| 4806 |
+
{
|
| 4807 |
+
"train": "37360",
|
| 4808 |
+
"to": "BP"
|
| 4809 |
+
},
|
| 4810 |
+
{
|
| 4811 |
+
"train": "37455",
|
| 4812 |
+
"from": "BP"
|
| 4813 |
+
},
|
| 4814 |
+
{
|
| 4815 |
+
"train": "37451",
|
| 4816 |
+
"cancel": true
|
| 4817 |
+
},
|
| 4818 |
+
{
|
| 4819 |
+
"train": "37462",
|
| 4820 |
+
"cancel": true
|
| 4821 |
+
},
|
| 4822 |
+
{
|
| 4823 |
+
"train": "37364",
|
| 4824 |
+
"hold": {
|
| 4825 |
+
"BP": "17:42"
|
| 4826 |
+
}
|
| 4827 |
+
}
|
| 4828 |
+
]
|
| 4829 |
+
}
|
| 4830 |
+
]
|
| 4831 |
+
}
|
runs/2026-10-08-qwen3-8-2-4t-eval45/index.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
runs/calib-sonnet-5-5/index.json
ADDED
|
@@ -0,0 +1,1900 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"run": "calib-sonnet-5-5",
|
| 3 |
+
"episodes": [
|
| 4 |
+
{
|
| 5 |
+
"model": "claude-sonnet-5-5@high",
|
| 6 |
+
"task_id": "gnlw-2025-01-05-0800-210m-compound-0",
|
| 7 |
+
"split": "train",
|
| 8 |
+
"difficulty": "compound",
|
| 9 |
+
"day": "2025-01-05",
|
| 10 |
+
"runs": 15,
|
| 11 |
+
"reward": 0.200941,
|
| 12 |
+
"submitted": true,
|
| 13 |
+
"feasible": true,
|
| 14 |
+
"cost": 1263,
|
| 15 |
+
"optimal_cost": 892,
|
| 16 |
+
"floor_cost": 882,
|
| 17 |
+
"greedy_cost": 3955,
|
| 18 |
+
"real_cost": 42,
|
| 19 |
+
"hardness": {
|
| 20 |
+
"runs": 15,
|
| 21 |
+
"sets": 3,
|
| 22 |
+
"stops": 100,
|
| 23 |
+
"conflicts": 6,
|
| 24 |
+
"single_track": 1,
|
| 25 |
+
"headway": 0,
|
| 26 |
+
"platforms": 0,
|
| 27 |
+
"closed": 5,
|
| 28 |
+
"decisions": 23,
|
| 29 |
+
"holds": 16,
|
| 30 |
+
"short_turns": 6,
|
| 31 |
+
"cancels": 1,
|
| 32 |
+
"turn_points": 1,
|
| 33 |
+
"avoidable": 10,
|
| 34 |
+
"optimal": 892,
|
| 35 |
+
"greedy_gap": 0.8,
|
| 36 |
+
"closed_min": 67,
|
| 37 |
+
"slow_min": 10,
|
| 38 |
+
"track_min": 31,
|
| 39 |
+
"single_closed": false,
|
| 40 |
+
"mid_line": true,
|
| 41 |
+
"late_sets": 1
|
| 42 |
+
},
|
| 43 |
+
"violations": 0,
|
| 44 |
+
"agent_turns": 5,
|
| 45 |
+
"checks": 4,
|
| 46 |
+
"seconds": 110.3,
|
| 47 |
+
"input_tokens": 90086,
|
| 48 |
+
"cache_read_tokens": 62969,
|
| 49 |
+
"cache_write_tokens": 27097,
|
| 50 |
+
"output_tokens": 11803,
|
| 51 |
+
"served_by": [
|
| 52 |
+
"claude-sonnet-5-5"
|
| 53 |
+
],
|
| 54 |
+
"end_reason": "submitted",
|
| 55 |
+
"errors": 0,
|
| 56 |
+
"plan": [
|
| 57 |
+
{
|
| 58 |
+
"train": "37426",
|
| 59 |
+
"hold": {
|
| 60 |
+
"FWD": "09:09"
|
| 61 |
+
}
|
| 62 |
+
},
|
| 63 |
+
{
|
| 64 |
+
"train": "37419",
|
| 65 |
+
"to": "BP"
|
| 66 |
+
},
|
| 67 |
+
{
|
| 68 |
+
"train": "37430",
|
| 69 |
+
"from": "BP"
|
| 70 |
+
},
|
| 71 |
+
{
|
| 72 |
+
"train": "37328",
|
| 73 |
+
"cancel": true
|
| 74 |
+
},
|
| 75 |
+
{
|
| 76 |
+
"train": "37423",
|
| 77 |
+
"cancel": true
|
| 78 |
+
},
|
| 79 |
+
{
|
| 80 |
+
"train": "37321",
|
| 81 |
+
"cancel": true
|
| 82 |
+
},
|
| 83 |
+
{
|
| 84 |
+
"train": "37332",
|
| 85 |
+
"cancel": true
|
| 86 |
+
}
|
| 87 |
+
]
|
| 88 |
+
},
|
| 89 |
+
{
|
| 90 |
+
"model": "claude-sonnet-5-5@high",
|
| 91 |
+
"task_id": "gnlw-2025-01-07-0703-180m-real-0",
|
| 92 |
+
"split": "train",
|
| 93 |
+
"difficulty": "real",
|
| 94 |
+
"day": "2025-01-07",
|
| 95 |
+
"runs": 26,
|
| 96 |
+
"reward": 0.37582,
|
| 97 |
+
"submitted": true,
|
| 98 |
+
"feasible": true,
|
| 99 |
+
"cost": 435,
|
| 100 |
+
"optimal_cost": 335,
|
| 101 |
+
"floor_cost": 303,
|
| 102 |
+
"greedy_cost": 1021,
|
| 103 |
+
"real_cost": 414,
|
| 104 |
+
"hardness": {
|
| 105 |
+
"runs": 26,
|
| 106 |
+
"sets": 6,
|
| 107 |
+
"stops": 176,
|
| 108 |
+
"conflicts": 20,
|
| 109 |
+
"single_track": 5,
|
| 110 |
+
"headway": 9,
|
| 111 |
+
"platforms": 0,
|
| 112 |
+
"closed": 6,
|
| 113 |
+
"decisions": 34,
|
| 114 |
+
"holds": 30,
|
| 115 |
+
"short_turns": 4,
|
| 116 |
+
"cancels": 0,
|
| 117 |
+
"turn_points": 2,
|
| 118 |
+
"avoidable": 32,
|
| 119 |
+
"optimal": 335,
|
| 120 |
+
"greedy_gap": 0.8,
|
| 121 |
+
"closed_min": 32,
|
| 122 |
+
"slow_min": 0,
|
| 123 |
+
"track_min": 0,
|
| 124 |
+
"single_closed": true,
|
| 125 |
+
"mid_line": false,
|
| 126 |
+
"late_sets": 0
|
| 127 |
+
},
|
| 128 |
+
"violations": 0,
|
| 129 |
+
"agent_turns": 6,
|
| 130 |
+
"checks": 5,
|
| 131 |
+
"seconds": 187.8,
|
| 132 |
+
"input_tokens": 174728,
|
| 133 |
+
"cache_read_tokens": 126106,
|
| 134 |
+
"cache_write_tokens": 48598,
|
| 135 |
+
"output_tokens": 23026,
|
| 136 |
+
"served_by": [
|
| 137 |
+
"claude-sonnet-5-5"
|
| 138 |
+
],
|
| 139 |
+
"end_reason": "submitted",
|
| 140 |
+
"errors": 0,
|
| 141 |
+
"plan": [
|
| 142 |
+
{
|
| 143 |
+
"train": "37313",
|
| 144 |
+
"to": "HDG"
|
| 145 |
+
},
|
| 146 |
+
{
|
| 147 |
+
"train": "37424",
|
| 148 |
+
"from": "HDG"
|
| 149 |
+
},
|
| 150 |
+
{
|
| 151 |
+
"train": "37415",
|
| 152 |
+
"hold": {
|
| 153 |
+
"HDG": "07:35"
|
| 154 |
+
}
|
| 155 |
+
},
|
| 156 |
+
{
|
| 157 |
+
"train": "37315",
|
| 158 |
+
"hold": {
|
| 159 |
+
"HDG": "07:40"
|
| 160 |
+
}
|
| 161 |
+
},
|
| 162 |
+
{
|
| 163 |
+
"train": "37324",
|
| 164 |
+
"cancel": true
|
| 165 |
+
},
|
| 166 |
+
{
|
| 167 |
+
"train": "37319",
|
| 168 |
+
"cancel": true
|
| 169 |
+
}
|
| 170 |
+
]
|
| 171 |
+
},
|
| 172 |
+
{
|
| 173 |
+
"model": "claude-sonnet-5-5@high",
|
| 174 |
+
"task_id": "gnlw-2025-01-13-1630-150m-blockage-0",
|
| 175 |
+
"split": "train",
|
| 176 |
+
"difficulty": "blockage",
|
| 177 |
+
"day": "2025-01-13",
|
| 178 |
+
"runs": 26,
|
| 179 |
+
"reward": 0.814541,
|
| 180 |
+
"submitted": true,
|
| 181 |
+
"feasible": true,
|
| 182 |
+
"cost": 256,
|
| 183 |
+
"optimal_cost": 232,
|
| 184 |
+
"floor_cost": 150,
|
| 185 |
+
"greedy_cost": 1412,
|
| 186 |
+
"real_cost": 473,
|
| 187 |
+
"hardness": {
|
| 188 |
+
"runs": 26,
|
| 189 |
+
"sets": 7,
|
| 190 |
+
"stops": 174,
|
| 191 |
+
"conflicts": 5,
|
| 192 |
+
"single_track": 0,
|
| 193 |
+
"headway": 0,
|
| 194 |
+
"platforms": 0,
|
| 195 |
+
"closed": 5,
|
| 196 |
+
"decisions": 17,
|
| 197 |
+
"holds": 17,
|
| 198 |
+
"short_turns": 0,
|
| 199 |
+
"cancels": 0,
|
| 200 |
+
"turn_points": 0,
|
| 201 |
+
"avoidable": 82,
|
| 202 |
+
"optimal": 232,
|
| 203 |
+
"greedy_gap": 0.8,
|
| 204 |
+
"closed_min": 20,
|
| 205 |
+
"slow_min": 0,
|
| 206 |
+
"track_min": 0,
|
| 207 |
+
"single_closed": true,
|
| 208 |
+
"mid_line": true,
|
| 209 |
+
"late_sets": 0
|
| 210 |
+
},
|
| 211 |
+
"violations": 0,
|
| 212 |
+
"agent_turns": 8,
|
| 213 |
+
"checks": 7,
|
| 214 |
+
"seconds": 226.0,
|
| 215 |
+
"input_tokens": 279241,
|
| 216 |
+
"cache_read_tokens": 221034,
|
| 217 |
+
"cache_write_tokens": 58175,
|
| 218 |
+
"output_tokens": 26644,
|
| 219 |
+
"served_by": [
|
| 220 |
+
"claude-sonnet-5-5"
|
| 221 |
+
],
|
| 222 |
+
"end_reason": "submitted",
|
| 223 |
+
"errors": 0,
|
| 224 |
+
"plan": [
|
| 225 |
+
{
|
| 226 |
+
"train": "37362",
|
| 227 |
+
"hold": {
|
| 228 |
+
"BP": "17:19"
|
| 229 |
+
}
|
| 230 |
+
},
|
| 231 |
+
{
|
| 232 |
+
"train": "37355",
|
| 233 |
+
"hold": {
|
| 234 |
+
"GN": "17:12"
|
| 235 |
+
}
|
| 236 |
+
},
|
| 237 |
+
{
|
| 238 |
+
"train": "37455",
|
| 239 |
+
"hold": {
|
| 240 |
+
"ZH": "17:23"
|
| 241 |
+
},
|
| 242 |
+
"to": "HDG"
|
| 243 |
+
},
|
| 244 |
+
{
|
| 245 |
+
"train": "37366",
|
| 246 |
+
"from": "HDG"
|
| 247 |
+
},
|
| 248 |
+
{
|
| 249 |
+
"train": "37462",
|
| 250 |
+
"hold": {
|
| 251 |
+
"BP": "17:22",
|
| 252 |
+
"GK": "17:29"
|
| 253 |
+
}
|
| 254 |
+
},
|
| 255 |
+
{
|
| 256 |
+
"train": "37457",
|
| 257 |
+
"hold": {
|
| 258 |
+
"GN": "17:26",
|
| 259 |
+
"FWD": "17:56"
|
| 260 |
+
}
|
| 261 |
+
},
|
| 262 |
+
{
|
| 263 |
+
"train": "37357",
|
| 264 |
+
"hold": {
|
| 265 |
+
"GN": "17:42"
|
| 266 |
+
}
|
| 267 |
+
},
|
| 268 |
+
{
|
| 269 |
+
"train": "37466",
|
| 270 |
+
"hold": {
|
| 271 |
+
"HDG": "18:00"
|
| 272 |
+
}
|
| 273 |
+
}
|
| 274 |
+
]
|
| 275 |
+
},
|
| 276 |
+
{
|
| 277 |
+
"model": "claude-sonnet-5-5@high",
|
| 278 |
+
"task_id": "gnlw-2025-01-14-1800-150m-loop-0",
|
| 279 |
+
"split": "train",
|
| 280 |
+
"difficulty": "loop",
|
| 281 |
+
"day": "2025-01-14",
|
| 282 |
+
"runs": 25,
|
| 283 |
+
"reward": 0.553439,
|
| 284 |
+
"submitted": true,
|
| 285 |
+
"feasible": true,
|
| 286 |
+
"cost": 510,
|
| 287 |
+
"optimal_cost": 423,
|
| 288 |
+
"floor_cost": 310,
|
| 289 |
+
"greedy_cost": 1411,
|
| 290 |
+
"real_cost": 85,
|
| 291 |
+
"hardness": {
|
| 292 |
+
"runs": 25,
|
| 293 |
+
"sets": 7,
|
| 294 |
+
"stops": 158,
|
| 295 |
+
"conflicts": 19,
|
| 296 |
+
"single_track": 13,
|
| 297 |
+
"headway": 6,
|
| 298 |
+
"platforms": 0,
|
| 299 |
+
"closed": 0,
|
| 300 |
+
"decisions": 25,
|
| 301 |
+
"holds": 21,
|
| 302 |
+
"short_turns": 4,
|
| 303 |
+
"cancels": 0,
|
| 304 |
+
"turn_points": 2,
|
| 305 |
+
"avoidable": 113,
|
| 306 |
+
"optimal": 423,
|
| 307 |
+
"greedy_gap": 0.7999,
|
| 308 |
+
"closed_min": 0,
|
| 309 |
+
"slow_min": 69,
|
| 310 |
+
"track_min": 113,
|
| 311 |
+
"single_closed": false,
|
| 312 |
+
"mid_line": false,
|
| 313 |
+
"late_sets": 1
|
| 314 |
+
},
|
| 315 |
+
"violations": 0,
|
| 316 |
+
"agent_turns": 5,
|
| 317 |
+
"checks": 4,
|
| 318 |
+
"seconds": 445.4,
|
| 319 |
+
"input_tokens": 218660,
|
| 320 |
+
"cache_read_tokens": 141886,
|
| 321 |
+
"cache_write_tokens": 76754,
|
| 322 |
+
"output_tokens": 60181,
|
| 323 |
+
"served_by": [
|
| 324 |
+
"claude-sonnet-5-5"
|
| 325 |
+
],
|
| 326 |
+
"end_reason": "submitted",
|
| 327 |
+
"errors": 0,
|
| 328 |
+
"plan": [
|
| 329 |
+
{
|
| 330 |
+
"train": "37359",
|
| 331 |
+
"to": "HDG",
|
| 332 |
+
"hold": {
|
| 333 |
+
"DWE": "18:55",
|
| 334 |
+
"FWD": "19:00"
|
| 335 |
+
}
|
| 336 |
+
},
|
| 337 |
+
{
|
| 338 |
+
"train": "37470",
|
| 339 |
+
"from": "HDG",
|
| 340 |
+
"hold": {
|
| 341 |
+
"BP": "19:23"
|
| 342 |
+
}
|
| 343 |
+
},
|
| 344 |
+
{
|
| 345 |
+
"train": "37368",
|
| 346 |
+
"hold": {
|
| 347 |
+
"BP": "18:43"
|
| 348 |
+
}
|
| 349 |
+
},
|
| 350 |
+
{
|
| 351 |
+
"train": "37468",
|
| 352 |
+
"hold": {
|
| 353 |
+
"BP": "18:49",
|
| 354 |
+
"ZH": "19:08"
|
| 355 |
+
}
|
| 356 |
+
},
|
| 357 |
+
{
|
| 358 |
+
"train": "37463",
|
| 359 |
+
"hold": {
|
| 360 |
+
"GN": "18:56",
|
| 361 |
+
"FWD": "19:33"
|
| 362 |
+
}
|
| 363 |
+
},
|
| 364 |
+
{
|
| 365 |
+
"train": "37363",
|
| 366 |
+
"hold": {
|
| 367 |
+
"GN": "19:12"
|
| 368 |
+
}
|
| 369 |
+
},
|
| 370 |
+
{
|
| 371 |
+
"train": "37465",
|
| 372 |
+
"hold": {
|
| 373 |
+
"GN": "19:29"
|
| 374 |
+
}
|
| 375 |
+
},
|
| 376 |
+
{
|
| 377 |
+
"train": "37365",
|
| 378 |
+
"hold": {
|
| 379 |
+
"GN": "19:47"
|
| 380 |
+
}
|
| 381 |
+
},
|
| 382 |
+
{
|
| 383 |
+
"train": "37372",
|
| 384 |
+
"hold": {
|
| 385 |
+
"LW": "19:19"
|
| 386 |
+
}
|
| 387 |
+
},
|
| 388 |
+
{
|
| 389 |
+
"train": "37472",
|
| 390 |
+
"hold": {
|
| 391 |
+
"BP": "19:49"
|
| 392 |
+
}
|
| 393 |
+
},
|
| 394 |
+
{
|
| 395 |
+
"train": "37374",
|
| 396 |
+
"hold": {
|
| 397 |
+
"LW": "19:51"
|
| 398 |
+
}
|
| 399 |
+
},
|
| 400 |
+
{
|
| 401 |
+
"train": "37467",
|
| 402 |
+
"hold": {
|
| 403 |
+
"FWD": "20:28"
|
| 404 |
+
}
|
| 405 |
+
}
|
| 406 |
+
]
|
| 407 |
+
},
|
| 408 |
+
{
|
| 409 |
+
"model": "claude-sonnet-5-5@high",
|
| 410 |
+
"task_id": "gnlw-2025-01-16-1802-180m-real-0",
|
| 411 |
+
"split": "train",
|
| 412 |
+
"difficulty": "real",
|
| 413 |
+
"day": "2025-01-16",
|
| 414 |
+
"runs": 24,
|
| 415 |
+
"reward": 0.41036,
|
| 416 |
+
"submitted": true,
|
| 417 |
+
"feasible": true,
|
| 418 |
+
"cost": 1335,
|
| 419 |
+
"optimal_cost": 1154,
|
| 420 |
+
"floor_cost": 983,
|
| 421 |
+
"greedy_cost": null,
|
| 422 |
+
"real_cost": 1127,
|
| 423 |
+
"hardness": {
|
| 424 |
+
"runs": 24,
|
| 425 |
+
"sets": 6,
|
| 426 |
+
"stops": 151,
|
| 427 |
+
"conflicts": 45,
|
| 428 |
+
"single_track": 10,
|
| 429 |
+
"headway": 32,
|
| 430 |
+
"platforms": 1,
|
| 431 |
+
"closed": 2,
|
| 432 |
+
"decisions": 21,
|
| 433 |
+
"holds": 12,
|
| 434 |
+
"short_turns": 9,
|
| 435 |
+
"cancels": 0,
|
| 436 |
+
"turn_points": 3,
|
| 437 |
+
"avoidable": 171,
|
| 438 |
+
"optimal": 1154,
|
| 439 |
+
"greedy_gap": 0.8583,
|
| 440 |
+
"closed_min": 36,
|
| 441 |
+
"slow_min": 0,
|
| 442 |
+
"track_min": 0,
|
| 443 |
+
"single_closed": false,
|
| 444 |
+
"mid_line": true,
|
| 445 |
+
"late_sets": 0
|
| 446 |
+
},
|
| 447 |
+
"violations": 0,
|
| 448 |
+
"agent_turns": 6,
|
| 449 |
+
"checks": 5,
|
| 450 |
+
"seconds": 475.8,
|
| 451 |
+
"input_tokens": 329656,
|
| 452 |
+
"cache_read_tokens": 242247,
|
| 453 |
+
"cache_write_tokens": 87385,
|
| 454 |
+
"output_tokens": 63115,
|
| 455 |
+
"served_by": [
|
| 456 |
+
"claude-sonnet-5-5"
|
| 457 |
+
],
|
| 458 |
+
"end_reason": "submitted",
|
| 459 |
+
"errors": 0,
|
| 460 |
+
"plan": [
|
| 461 |
+
{
|
| 462 |
+
"train": "37355",
|
| 463 |
+
"to": "HDG"
|
| 464 |
+
},
|
| 465 |
+
{
|
| 466 |
+
"train": "37466",
|
| 467 |
+
"from": "HDG",
|
| 468 |
+
"to": "ZH",
|
| 469 |
+
"hold": {
|
| 470 |
+
"HDG": "18:33"
|
| 471 |
+
}
|
| 472 |
+
},
|
| 473 |
+
{
|
| 474 |
+
"train": "37463",
|
| 475 |
+
"from": "ZH"
|
| 476 |
+
},
|
| 477 |
+
{
|
| 478 |
+
"train": "37457",
|
| 479 |
+
"to": "HDG"
|
| 480 |
+
},
|
| 481 |
+
{
|
| 482 |
+
"train": "37368",
|
| 483 |
+
"from": "HDG",
|
| 484 |
+
"hold": {
|
| 485 |
+
"HDG": "18:37",
|
| 486 |
+
"FWD": "18:40",
|
| 487 |
+
"BP": "18:49"
|
| 488 |
+
}
|
| 489 |
+
},
|
| 490 |
+
{
|
| 491 |
+
"train": "37357",
|
| 492 |
+
"to": "HDG",
|
| 493 |
+
"hold": {
|
| 494 |
+
"BP": "18:26",
|
| 495 |
+
"FWD": "18:40"
|
| 496 |
+
}
|
| 497 |
+
},
|
| 498 |
+
{
|
| 499 |
+
"train": "37468",
|
| 500 |
+
"from": "HDG",
|
| 501 |
+
"hold": {
|
| 502 |
+
"HDG": "18:47"
|
| 503 |
+
}
|
| 504 |
+
},
|
| 505 |
+
{
|
| 506 |
+
"train": "37459",
|
| 507 |
+
"hold": {
|
| 508 |
+
"GK": "18:20",
|
| 509 |
+
"BP": "18:33",
|
| 510 |
+
"FWD": "18:43"
|
| 511 |
+
}
|
| 512 |
+
},
|
| 513 |
+
{
|
| 514 |
+
"train": "37359",
|
| 515 |
+
"hold": {
|
| 516 |
+
"ZH": "18:29",
|
| 517 |
+
"FWD": "18:50"
|
| 518 |
+
}
|
| 519 |
+
},
|
| 520 |
+
{
|
| 521 |
+
"train": "37464",
|
| 522 |
+
"to": "BP"
|
| 523 |
+
},
|
| 524 |
+
{
|
| 525 |
+
"train": "37461",
|
| 526 |
+
"from": "BP",
|
| 527 |
+
"to": "HDG"
|
| 528 |
+
},
|
| 529 |
+
{
|
| 530 |
+
"train": "37372",
|
| 531 |
+
"from": "HDG"
|
| 532 |
+
},
|
| 533 |
+
{
|
| 534 |
+
"train": "37363",
|
| 535 |
+
"hold": {
|
| 536 |
+
"ZH": "19:21"
|
| 537 |
+
}
|
| 538 |
+
},
|
| 539 |
+
{
|
| 540 |
+
"train": "37365",
|
| 541 |
+
"hold": {
|
| 542 |
+
"GN": "19:40"
|
| 543 |
+
}
|
| 544 |
+
}
|
| 545 |
+
]
|
| 546 |
+
},
|
| 547 |
+
{
|
| 548 |
+
"model": "claude-sonnet-5-5@high",
|
| 549 |
+
"task_id": "gnlw-2025-02-02-1830-180m-blockage-0",
|
| 550 |
+
"split": "train",
|
| 551 |
+
"difficulty": "blockage",
|
| 552 |
+
"day": "2025-02-02",
|
| 553 |
+
"runs": 15,
|
| 554 |
+
"reward": 0.327054,
|
| 555 |
+
"submitted": true,
|
| 556 |
+
"feasible": true,
|
| 557 |
+
"cost": 726,
|
| 558 |
+
"optimal_cost": 634,
|
| 559 |
+
"floor_cost": 634,
|
| 560 |
+
"greedy_cost": 1483,
|
| 561 |
+
"real_cost": 226,
|
| 562 |
+
"hardness": {
|
| 563 |
+
"runs": 15,
|
| 564 |
+
"sets": 3,
|
| 565 |
+
"stops": 100,
|
| 566 |
+
"conflicts": 20,
|
| 567 |
+
"single_track": 0,
|
| 568 |
+
"headway": 0,
|
| 569 |
+
"platforms": 0,
|
| 570 |
+
"closed": 20,
|
| 571 |
+
"decisions": 20,
|
| 572 |
+
"holds": 15,
|
| 573 |
+
"short_turns": 5,
|
| 574 |
+
"cancels": 0,
|
| 575 |
+
"turn_points": 2,
|
| 576 |
+
"avoidable": 0,
|
| 577 |
+
"optimal": 634,
|
| 578 |
+
"greedy_gap": 0.8,
|
| 579 |
+
"closed_min": 57,
|
| 580 |
+
"slow_min": 0,
|
| 581 |
+
"track_min": 0,
|
| 582 |
+
"single_closed": true,
|
| 583 |
+
"mid_line": false,
|
| 584 |
+
"late_sets": 0
|
| 585 |
+
},
|
| 586 |
+
"violations": 0,
|
| 587 |
+
"agent_turns": 3,
|
| 588 |
+
"checks": 2,
|
| 589 |
+
"seconds": 122.4,
|
| 590 |
+
"input_tokens": 37433,
|
| 591 |
+
"cache_read_tokens": 14314,
|
| 592 |
+
"cache_write_tokens": 23107,
|
| 593 |
+
"output_tokens": 14308,
|
| 594 |
+
"served_by": [
|
| 595 |
+
"claude-sonnet-5-5"
|
| 596 |
+
],
|
| 597 |
+
"end_reason": "submitted",
|
| 598 |
+
"errors": 0,
|
| 599 |
+
"plan": [
|
| 600 |
+
{
|
| 601 |
+
"train": "37459",
|
| 602 |
+
"hold": {
|
| 603 |
+
"LWC": "19:37"
|
| 604 |
+
}
|
| 605 |
+
},
|
| 606 |
+
{
|
| 607 |
+
"train": "37470",
|
| 608 |
+
"cancel": true
|
| 609 |
+
},
|
| 610 |
+
{
|
| 611 |
+
"train": "37365",
|
| 612 |
+
"cancel": true
|
| 613 |
+
},
|
| 614 |
+
{
|
| 615 |
+
"train": "37361",
|
| 616 |
+
"to": "BP"
|
| 617 |
+
},
|
| 618 |
+
{
|
| 619 |
+
"train": "37372",
|
| 620 |
+
"from": "BP"
|
| 621 |
+
},
|
| 622 |
+
{
|
| 623 |
+
"train": "37463",
|
| 624 |
+
"hold": {
|
| 625 |
+
"BP": "19:37"
|
| 626 |
+
}
|
| 627 |
+
}
|
| 628 |
+
]
|
| 629 |
+
},
|
| 630 |
+
{
|
| 631 |
+
"model": "claude-sonnet-5-5@high",
|
| 632 |
+
"task_id": "gnlw-2025-02-03-0900-150m-loop-0",
|
| 633 |
+
"split": "train",
|
| 634 |
+
"difficulty": "loop",
|
| 635 |
+
"day": "2025-02-03",
|
| 636 |
+
"runs": 26,
|
| 637 |
+
"reward": 0.735231,
|
| 638 |
+
"submitted": true,
|
| 639 |
+
"feasible": true,
|
| 640 |
+
"cost": 398,
|
| 641 |
+
"optimal_cost": 356,
|
| 642 |
+
"floor_cost": 247,
|
| 643 |
+
"greedy_cost": 2860,
|
| 644 |
+
"real_cost": 515,
|
| 645 |
+
"hardness": {
|
| 646 |
+
"runs": 26,
|
| 647 |
+
"sets": 7,
|
| 648 |
+
"stops": 174,
|
| 649 |
+
"conflicts": 20,
|
| 650 |
+
"single_track": 14,
|
| 651 |
+
"headway": 5,
|
| 652 |
+
"platforms": 1,
|
| 653 |
+
"closed": 0,
|
| 654 |
+
"decisions": 26,
|
| 655 |
+
"holds": 24,
|
| 656 |
+
"short_turns": 2,
|
| 657 |
+
"cancels": 0,
|
| 658 |
+
"turn_points": 1,
|
| 659 |
+
"avoidable": 109,
|
| 660 |
+
"optimal": 356,
|
| 661 |
+
"greedy_gap": 0.8,
|
| 662 |
+
"closed_min": 0,
|
| 663 |
+
"slow_min": 33,
|
| 664 |
+
"track_min": 131,
|
| 665 |
+
"single_closed": false,
|
| 666 |
+
"mid_line": false,
|
| 667 |
+
"late_sets": 1
|
| 668 |
+
},
|
| 669 |
+
"violations": 0,
|
| 670 |
+
"agent_turns": 5,
|
| 671 |
+
"checks": 4,
|
| 672 |
+
"seconds": 389.3,
|
| 673 |
+
"input_tokens": 235299,
|
| 674 |
+
"cache_read_tokens": 159342,
|
| 675 |
+
"cache_write_tokens": 75937,
|
| 676 |
+
"output_tokens": 52360,
|
| 677 |
+
"served_by": [
|
| 678 |
+
"claude-sonnet-5-5"
|
| 679 |
+
],
|
| 680 |
+
"end_reason": "submitted",
|
| 681 |
+
"errors": 0,
|
| 682 |
+
"plan": [
|
| 683 |
+
{
|
| 684 |
+
"train": "37421",
|
| 685 |
+
"hold": {
|
| 686 |
+
"HDG": "09:03"
|
| 687 |
+
}
|
| 688 |
+
},
|
| 689 |
+
{
|
| 690 |
+
"train": "37321",
|
| 691 |
+
"hold": {
|
| 692 |
+
"FWD": "09:06"
|
| 693 |
+
}
|
| 694 |
+
},
|
| 695 |
+
{
|
| 696 |
+
"train": "37323",
|
| 697 |
+
"to": "HDG",
|
| 698 |
+
"hold": {
|
| 699 |
+
"GN": "09:27"
|
| 700 |
+
}
|
| 701 |
+
},
|
| 702 |
+
{
|
| 703 |
+
"train": "37425",
|
| 704 |
+
"hold": {
|
| 705 |
+
"GN": "09:30",
|
| 706 |
+
"FWD": "10:08"
|
| 707 |
+
}
|
| 708 |
+
},
|
| 709 |
+
{
|
| 710 |
+
"train": "37325",
|
| 711 |
+
"hold": {
|
| 712 |
+
"FWD": "10:14"
|
| 713 |
+
}
|
| 714 |
+
},
|
| 715 |
+
{
|
| 716 |
+
"train": "37427",
|
| 717 |
+
"hold": {
|
| 718 |
+
"FWD": "10:31"
|
| 719 |
+
}
|
| 720 |
+
},
|
| 721 |
+
{
|
| 722 |
+
"train": "37327",
|
| 723 |
+
"hold": {
|
| 724 |
+
"FWD": "10:37"
|
| 725 |
+
}
|
| 726 |
+
},
|
| 727 |
+
{
|
| 728 |
+
"train": "37430",
|
| 729 |
+
"hold": {
|
| 730 |
+
"ZH": "09:35"
|
| 731 |
+
}
|
| 732 |
+
},
|
| 733 |
+
{
|
| 734 |
+
"train": "37332",
|
| 735 |
+
"hold": {
|
| 736 |
+
"BP": "09:43"
|
| 737 |
+
}
|
| 738 |
+
},
|
| 739 |
+
{
|
| 740 |
+
"train": "37432",
|
| 741 |
+
"hold": {
|
| 742 |
+
"BP": "09:46",
|
| 743 |
+
"GK": "09:56"
|
| 744 |
+
}
|
| 745 |
+
},
|
| 746 |
+
{
|
| 747 |
+
"train": "37434",
|
| 748 |
+
"from": "HDG"
|
| 749 |
+
},
|
| 750 |
+
{
|
| 751 |
+
"train": "37436",
|
| 752 |
+
"hold": {
|
| 753 |
+
"HDG": "10:34"
|
| 754 |
+
}
|
| 755 |
+
},
|
| 756 |
+
{
|
| 757 |
+
"train": "37336",
|
| 758 |
+
"hold": {
|
| 759 |
+
"BP": "10:42"
|
| 760 |
+
}
|
| 761 |
+
}
|
| 762 |
+
]
|
| 763 |
+
},
|
| 764 |
+
{
|
| 765 |
+
"model": "claude-sonnet-5-5@high",
|
| 766 |
+
"task_id": "gnlw-2025-02-22-0754-180m-real-0",
|
| 767 |
+
"split": "train",
|
| 768 |
+
"difficulty": "real",
|
| 769 |
+
"day": "2025-02-22",
|
| 770 |
+
"runs": 27,
|
| 771 |
+
"reward": 0.407882,
|
| 772 |
+
"submitted": true,
|
| 773 |
+
"feasible": true,
|
| 774 |
+
"cost": 401,
|
| 775 |
+
"optimal_cost": 244,
|
| 776 |
+
"floor_cost": 111,
|
| 777 |
+
"greedy_cost": 3096,
|
| 778 |
+
"real_cost": 1637,
|
| 779 |
+
"hardness": {
|
| 780 |
+
"runs": 27,
|
| 781 |
+
"sets": 7,
|
| 782 |
+
"stops": 183,
|
| 783 |
+
"conflicts": 18,
|
| 784 |
+
"single_track": 18,
|
| 785 |
+
"headway": 0,
|
| 786 |
+
"platforms": 0,
|
| 787 |
+
"closed": 0,
|
| 788 |
+
"decisions": 28,
|
| 789 |
+
"holds": 28,
|
| 790 |
+
"short_turns": 0,
|
| 791 |
+
"cancels": 0,
|
| 792 |
+
"turn_points": 0,
|
| 793 |
+
"avoidable": 133,
|
| 794 |
+
"optimal": 244,
|
| 795 |
+
"greedy_gap": 0.8,
|
| 796 |
+
"closed_min": 0,
|
| 797 |
+
"slow_min": 180,
|
| 798 |
+
"track_min": 0,
|
| 799 |
+
"single_closed": false,
|
| 800 |
+
"mid_line": false,
|
| 801 |
+
"late_sets": 0
|
| 802 |
+
},
|
| 803 |
+
"violations": 0,
|
| 804 |
+
"agent_turns": 5,
|
| 805 |
+
"checks": 4,
|
| 806 |
+
"seconds": 212.7,
|
| 807 |
+
"input_tokens": 123680,
|
| 808 |
+
"cache_read_tokens": 75327,
|
| 809 |
+
"cache_write_tokens": 48333,
|
| 810 |
+
"output_tokens": 26914,
|
| 811 |
+
"served_by": [
|
| 812 |
+
"claude-sonnet-5-5"
|
| 813 |
+
],
|
| 814 |
+
"end_reason": "submitted",
|
| 815 |
+
"errors": 0,
|
| 816 |
+
"plan": [
|
| 817 |
+
{
|
| 818 |
+
"train": "37419",
|
| 819 |
+
"hold": {
|
| 820 |
+
"FWD": "08:28"
|
| 821 |
+
}
|
| 822 |
+
},
|
| 823 |
+
{
|
| 824 |
+
"train": "37421",
|
| 825 |
+
"hold": {
|
| 826 |
+
"FWD": "08:59"
|
| 827 |
+
}
|
| 828 |
+
},
|
| 829 |
+
{
|
| 830 |
+
"train": "37423",
|
| 831 |
+
"hold": {
|
| 832 |
+
"FWD": "09:30"
|
| 833 |
+
}
|
| 834 |
+
},
|
| 835 |
+
{
|
| 836 |
+
"train": "37425",
|
| 837 |
+
"hold": {
|
| 838 |
+
"FWD": "10:01"
|
| 839 |
+
}
|
| 840 |
+
},
|
| 841 |
+
{
|
| 842 |
+
"train": "37427",
|
| 843 |
+
"hold": {
|
| 844 |
+
"FWD": "10:32"
|
| 845 |
+
}
|
| 846 |
+
},
|
| 847 |
+
{
|
| 848 |
+
"train": "37429",
|
| 849 |
+
"hold": {
|
| 850 |
+
"GN": "10:25",
|
| 851 |
+
"FWD": "11:03"
|
| 852 |
+
}
|
| 853 |
+
},
|
| 854 |
+
{
|
| 855 |
+
"train": "37430",
|
| 856 |
+
"hold": {
|
| 857 |
+
"HDG": "09:02"
|
| 858 |
+
}
|
| 859 |
+
},
|
| 860 |
+
{
|
| 861 |
+
"train": "37432",
|
| 862 |
+
"hold": {
|
| 863 |
+
"HDG": "09:33"
|
| 864 |
+
}
|
| 865 |
+
},
|
| 866 |
+
{
|
| 867 |
+
"train": "37434",
|
| 868 |
+
"hold": {
|
| 869 |
+
"HDG": "10:04"
|
| 870 |
+
}
|
| 871 |
+
},
|
| 872 |
+
{
|
| 873 |
+
"train": "37436",
|
| 874 |
+
"hold": {
|
| 875 |
+
"HDG": "10:35"
|
| 876 |
+
}
|
| 877 |
+
},
|
| 878 |
+
{
|
| 879 |
+
"train": "37438",
|
| 880 |
+
"hold": {
|
| 881 |
+
"HDG": "11:06"
|
| 882 |
+
}
|
| 883 |
+
},
|
| 884 |
+
{
|
| 885 |
+
"train": "37319",
|
| 886 |
+
"hold": {
|
| 887 |
+
"FWD": "08:34"
|
| 888 |
+
}
|
| 889 |
+
},
|
| 890 |
+
{
|
| 891 |
+
"train": "37321",
|
| 892 |
+
"hold": {
|
| 893 |
+
"FWD": "09:05"
|
| 894 |
+
}
|
| 895 |
+
},
|
| 896 |
+
{
|
| 897 |
+
"train": "37323",
|
| 898 |
+
"hold": {
|
| 899 |
+
"FWD": "09:36"
|
| 900 |
+
}
|
| 901 |
+
},
|
| 902 |
+
{
|
| 903 |
+
"train": "37325",
|
| 904 |
+
"hold": {
|
| 905 |
+
"GN": "09:40",
|
| 906 |
+
"FWD": "10:07"
|
| 907 |
+
}
|
| 908 |
+
},
|
| 909 |
+
{
|
| 910 |
+
"train": "37327",
|
| 911 |
+
"hold": {
|
| 912 |
+
"GN": "10:10",
|
| 913 |
+
"FWD": "10:38"
|
| 914 |
+
}
|
| 915 |
+
},
|
| 916 |
+
{
|
| 917 |
+
"train": "37329",
|
| 918 |
+
"hold": {
|
| 919 |
+
"GN": "10:42",
|
| 920 |
+
"FWD": "11:09"
|
| 921 |
+
}
|
| 922 |
+
}
|
| 923 |
+
]
|
| 924 |
+
},
|
| 925 |
+
{
|
| 926 |
+
"model": "claude-sonnet-5-5@high",
|
| 927 |
+
"task_id": "gnlw-2025-02-27-1630-180m-compound-0",
|
| 928 |
+
"split": "train",
|
| 929 |
+
"difficulty": "compound",
|
| 930 |
+
"day": "2025-02-27",
|
| 931 |
+
"runs": 30,
|
| 932 |
+
"reward": 0.200001,
|
| 933 |
+
"submitted": true,
|
| 934 |
+
"feasible": true,
|
| 935 |
+
"cost": 2478,
|
| 936 |
+
"optimal_cost": 1035,
|
| 937 |
+
"floor_cost": 931,
|
| 938 |
+
"greedy_cost": 3334,
|
| 939 |
+
"real_cost": 96,
|
| 940 |
+
"hardness": {
|
| 941 |
+
"runs": 30,
|
| 942 |
+
"sets": 7,
|
| 943 |
+
"stops": 200,
|
| 944 |
+
"conflicts": 45,
|
| 945 |
+
"single_track": 8,
|
| 946 |
+
"headway": 20,
|
| 947 |
+
"platforms": 3,
|
| 948 |
+
"closed": 14,
|
| 949 |
+
"decisions": 41,
|
| 950 |
+
"holds": 29,
|
| 951 |
+
"short_turns": 12,
|
| 952 |
+
"cancels": 0,
|
| 953 |
+
"turn_points": 3,
|
| 954 |
+
"avoidable": 104,
|
| 955 |
+
"optimal": 1035,
|
| 956 |
+
"greedy_gap": 0.8,
|
| 957 |
+
"closed_min": 39,
|
| 958 |
+
"slow_min": 0,
|
| 959 |
+
"track_min": 0,
|
| 960 |
+
"single_closed": true,
|
| 961 |
+
"mid_line": false,
|
| 962 |
+
"late_sets": 1
|
| 963 |
+
},
|
| 964 |
+
"violations": 0,
|
| 965 |
+
"agent_turns": 7,
|
| 966 |
+
"checks": 6,
|
| 967 |
+
"seconds": 614.6,
|
| 968 |
+
"input_tokens": 493351,
|
| 969 |
+
"cache_read_tokens": 369633,
|
| 970 |
+
"cache_write_tokens": 123690,
|
| 971 |
+
"output_tokens": 82412,
|
| 972 |
+
"served_by": [
|
| 973 |
+
"claude-sonnet-5-5"
|
| 974 |
+
],
|
| 975 |
+
"end_reason": "submitted",
|
| 976 |
+
"errors": 0,
|
| 977 |
+
"plan": [
|
| 978 |
+
{
|
| 979 |
+
"train": "37451",
|
| 980 |
+
"to": "HDG"
|
| 981 |
+
},
|
| 982 |
+
{
|
| 983 |
+
"train": "37362",
|
| 984 |
+
"from": "HDG",
|
| 985 |
+
"hold": {
|
| 986 |
+
"HDG": "17:09",
|
| 987 |
+
"FWD": "17:17"
|
| 988 |
+
}
|
| 989 |
+
},
|
| 990 |
+
{
|
| 991 |
+
"train": "37460",
|
| 992 |
+
"hold": {
|
| 993 |
+
"FWD": "17:09"
|
| 994 |
+
}
|
| 995 |
+
},
|
| 996 |
+
{
|
| 997 |
+
"train": "37453",
|
| 998 |
+
"hold": {
|
| 999 |
+
"BP": "17:09"
|
| 1000 |
+
}
|
| 1001 |
+
},
|
| 1002 |
+
{
|
| 1003 |
+
"train": "37353",
|
| 1004 |
+
"hold": {
|
| 1005 |
+
"BP": "17:16"
|
| 1006 |
+
}
|
| 1007 |
+
},
|
| 1008 |
+
{
|
| 1009 |
+
"train": "37455",
|
| 1010 |
+
"hold": {
|
| 1011 |
+
"GN": "16:57",
|
| 1012 |
+
"BP": "17:19"
|
| 1013 |
+
}
|
| 1014 |
+
},
|
| 1015 |
+
{
|
| 1016 |
+
"train": "37462",
|
| 1017 |
+
"cancel": true
|
| 1018 |
+
},
|
| 1019 |
+
{
|
| 1020 |
+
"train": "37459",
|
| 1021 |
+
"cancel": true
|
| 1022 |
+
},
|
| 1023 |
+
{
|
| 1024 |
+
"train": "37464",
|
| 1025 |
+
"hold": {
|
| 1026 |
+
"LW": "17:40"
|
| 1027 |
+
}
|
| 1028 |
+
},
|
| 1029 |
+
{
|
| 1030 |
+
"train": "37364",
|
| 1031 |
+
"hold": {
|
| 1032 |
+
"LW": "17:45",
|
| 1033 |
+
"FWD": "17:54",
|
| 1034 |
+
"BP": "18:04"
|
| 1035 |
+
}
|
| 1036 |
+
},
|
| 1037 |
+
{
|
| 1038 |
+
"train": "37366",
|
| 1039 |
+
"hold": {
|
| 1040 |
+
"LW": "17:48",
|
| 1041 |
+
"FWD": "18:02"
|
| 1042 |
+
}
|
| 1043 |
+
},
|
| 1044 |
+
{
|
| 1045 |
+
"train": "37355",
|
| 1046 |
+
"hold": {
|
| 1047 |
+
"GN": "17:40"
|
| 1048 |
+
}
|
| 1049 |
+
},
|
| 1050 |
+
{
|
| 1051 |
+
"train": "37457",
|
| 1052 |
+
"hold": {
|
| 1053 |
+
"GN": "17:43"
|
| 1054 |
+
}
|
| 1055 |
+
},
|
| 1056 |
+
{
|
| 1057 |
+
"train": "37357",
|
| 1058 |
+
"hold": {
|
| 1059 |
+
"GN": "17:53"
|
| 1060 |
+
}
|
| 1061 |
+
},
|
| 1062 |
+
{
|
| 1063 |
+
"train": "37466",
|
| 1064 |
+
"hold": {
|
| 1065 |
+
"LW": "18:25"
|
| 1066 |
+
}
|
| 1067 |
+
},
|
| 1068 |
+
{
|
| 1069 |
+
"train": "37468",
|
| 1070 |
+
"hold": {
|
| 1071 |
+
"LW": "18:29"
|
| 1072 |
+
}
|
| 1073 |
+
},
|
| 1074 |
+
{
|
| 1075 |
+
"train": "37368",
|
| 1076 |
+
"hold": {
|
| 1077 |
+
"LW": "18:34",
|
| 1078 |
+
"FWD": "18:43",
|
| 1079 |
+
"BP": "18:53"
|
| 1080 |
+
}
|
| 1081 |
+
},
|
| 1082 |
+
{
|
| 1083 |
+
"train": "37359",
|
| 1084 |
+
"hold": {
|
| 1085 |
+
"GN": "18:25",
|
| 1086 |
+
"FWD": "18:54"
|
| 1087 |
+
}
|
| 1088 |
+
},
|
| 1089 |
+
{
|
| 1090 |
+
"train": "37461",
|
| 1091 |
+
"hold": {
|
| 1092 |
+
"GN": "18:28",
|
| 1093 |
+
"FWD": "18:57"
|
| 1094 |
+
}
|
| 1095 |
+
},
|
| 1096 |
+
{
|
| 1097 |
+
"train": "37463",
|
| 1098 |
+
"hold": {
|
| 1099 |
+
"GN": "19:10"
|
| 1100 |
+
}
|
| 1101 |
+
},
|
| 1102 |
+
{
|
| 1103 |
+
"train": "37363",
|
| 1104 |
+
"hold": {
|
| 1105 |
+
"GN": "19:21",
|
| 1106 |
+
"FWD": "19:44"
|
| 1107 |
+
}
|
| 1108 |
+
},
|
| 1109 |
+
{
|
| 1110 |
+
"train": "37470",
|
| 1111 |
+
"hold": {
|
| 1112 |
+
"LW": "19:14"
|
| 1113 |
+
}
|
| 1114 |
+
},
|
| 1115 |
+
{
|
| 1116 |
+
"train": "37372",
|
| 1117 |
+
"hold": {
|
| 1118 |
+
"LW": "19:19",
|
| 1119 |
+
"FWD": "19:28",
|
| 1120 |
+
"BP": "19:38"
|
| 1121 |
+
}
|
| 1122 |
+
},
|
| 1123 |
+
{
|
| 1124 |
+
"train": "37472",
|
| 1125 |
+
"hold": {
|
| 1126 |
+
"LW": "19:22",
|
| 1127 |
+
"BP": "19:41"
|
| 1128 |
+
}
|
| 1129 |
+
}
|
| 1130 |
+
]
|
| 1131 |
+
},
|
| 1132 |
+
{
|
| 1133 |
+
"model": "claude-sonnet-5-5@high",
|
| 1134 |
+
"task_id": "gnlw-2025-03-17-0830-210m-blockage-0",
|
| 1135 |
+
"split": "train",
|
| 1136 |
+
"difficulty": "blockage",
|
| 1137 |
+
"day": "2025-03-17",
|
| 1138 |
+
"runs": 34,
|
| 1139 |
+
"reward": 0.214259,
|
| 1140 |
+
"submitted": true,
|
| 1141 |
+
"feasible": true,
|
| 1142 |
+
"cost": 821,
|
| 1143 |
+
"optimal_cost": 525,
|
| 1144 |
+
"floor_cost": 478,
|
| 1145 |
+
"greedy_cost": null,
|
| 1146 |
+
"real_cost": 508,
|
| 1147 |
+
"hardness": {
|
| 1148 |
+
"runs": 34,
|
| 1149 |
+
"sets": 7,
|
| 1150 |
+
"stops": 226,
|
| 1151 |
+
"conflicts": 5,
|
| 1152 |
+
"single_track": 0,
|
| 1153 |
+
"headway": 0,
|
| 1154 |
+
"platforms": 0,
|
| 1155 |
+
"closed": 5,
|
| 1156 |
+
"decisions": 36,
|
| 1157 |
+
"holds": 30,
|
| 1158 |
+
"short_turns": 6,
|
| 1159 |
+
"cancels": 0,
|
| 1160 |
+
"turn_points": 2,
|
| 1161 |
+
"avoidable": 47,
|
| 1162 |
+
"optimal": 525,
|
| 1163 |
+
"greedy_gap": 0.8294,
|
| 1164 |
+
"closed_min": 33,
|
| 1165 |
+
"slow_min": 0,
|
| 1166 |
+
"track_min": 0,
|
| 1167 |
+
"single_closed": false,
|
| 1168 |
+
"mid_line": true,
|
| 1169 |
+
"late_sets": 0
|
| 1170 |
+
},
|
| 1171 |
+
"violations": 0,
|
| 1172 |
+
"agent_turns": 9,
|
| 1173 |
+
"checks": 8,
|
| 1174 |
+
"seconds": 300.8,
|
| 1175 |
+
"input_tokens": 445638,
|
| 1176 |
+
"cache_read_tokens": 360486,
|
| 1177 |
+
"cache_write_tokens": 85116,
|
| 1178 |
+
"output_tokens": 37113,
|
| 1179 |
+
"served_by": [
|
| 1180 |
+
"claude-sonnet-5-5"
|
| 1181 |
+
],
|
| 1182 |
+
"end_reason": "submitted",
|
| 1183 |
+
"errors": 0,
|
| 1184 |
+
"plan": [
|
| 1185 |
+
{
|
| 1186 |
+
"train": "37421",
|
| 1187 |
+
"to": "HDG",
|
| 1188 |
+
"hold": {
|
| 1189 |
+
"DWE": "09:24"
|
| 1190 |
+
}
|
| 1191 |
+
},
|
| 1192 |
+
{
|
| 1193 |
+
"train": "37321",
|
| 1194 |
+
"to": "HDG",
|
| 1195 |
+
"hold": {
|
| 1196 |
+
"BP": "09:24"
|
| 1197 |
+
}
|
| 1198 |
+
},
|
| 1199 |
+
{
|
| 1200 |
+
"train": "37423",
|
| 1201 |
+
"to": "HDG",
|
| 1202 |
+
"hold": {
|
| 1203 |
+
"GK": "09:26"
|
| 1204 |
+
}
|
| 1205 |
+
},
|
| 1206 |
+
{
|
| 1207 |
+
"train": "37323",
|
| 1208 |
+
"to": "HDG",
|
| 1209 |
+
"hold": {
|
| 1210 |
+
"GN": "09:21",
|
| 1211 |
+
"DWE": "09:43"
|
| 1212 |
+
}
|
| 1213 |
+
},
|
| 1214 |
+
{
|
| 1215 |
+
"train": "37330",
|
| 1216 |
+
"to": "ZH",
|
| 1217 |
+
"hold": {
|
| 1218 |
+
"FWD": "09:23"
|
| 1219 |
+
}
|
| 1220 |
+
},
|
| 1221 |
+
{
|
| 1222 |
+
"train": "37430",
|
| 1223 |
+
"hold": {
|
| 1224 |
+
"FWD": "09:26"
|
| 1225 |
+
}
|
| 1226 |
+
},
|
| 1227 |
+
{
|
| 1228 |
+
"train": "37332",
|
| 1229 |
+
"from": "HDG",
|
| 1230 |
+
"hold": {
|
| 1231 |
+
"HDG": "09:34",
|
| 1232 |
+
"BP": "09:45"
|
| 1233 |
+
}
|
| 1234 |
+
},
|
| 1235 |
+
{
|
| 1236 |
+
"train": "37432",
|
| 1237 |
+
"from": "HDG",
|
| 1238 |
+
"hold": {
|
| 1239 |
+
"HDG": "09:38"
|
| 1240 |
+
}
|
| 1241 |
+
},
|
| 1242 |
+
{
|
| 1243 |
+
"train": "37334",
|
| 1244 |
+
"from": "HDG",
|
| 1245 |
+
"hold": {
|
| 1246 |
+
"GK": "10:09"
|
| 1247 |
+
}
|
| 1248 |
+
},
|
| 1249 |
+
{
|
| 1250 |
+
"train": "37434",
|
| 1251 |
+
"from": "HDG"
|
| 1252 |
+
},
|
| 1253 |
+
{
|
| 1254 |
+
"train": "37325",
|
| 1255 |
+
"from": "ZH"
|
| 1256 |
+
}
|
| 1257 |
+
]
|
| 1258 |
+
},
|
| 1259 |
+
{
|
| 1260 |
+
"model": "claude-sonnet-5-5@high",
|
| 1261 |
+
"task_id": "gnlw-2025-03-19-0613-180m-real-0",
|
| 1262 |
+
"split": "train",
|
| 1263 |
+
"difficulty": "real",
|
| 1264 |
+
"day": "2025-03-19",
|
| 1265 |
+
"runs": 26,
|
| 1266 |
+
"reward": 1.0,
|
| 1267 |
+
"submitted": true,
|
| 1268 |
+
"feasible": true,
|
| 1269 |
+
"cost": 38,
|
| 1270 |
+
"optimal_cost": 38,
|
| 1271 |
+
"floor_cost": 17,
|
| 1272 |
+
"greedy_cost": 950,
|
| 1273 |
+
"real_cost": 1752,
|
| 1274 |
+
"hardness": {
|
| 1275 |
+
"runs": 26,
|
| 1276 |
+
"sets": 7,
|
| 1277 |
+
"stops": 174,
|
| 1278 |
+
"conflicts": 11,
|
| 1279 |
+
"single_track": 1,
|
| 1280 |
+
"headway": 0,
|
| 1281 |
+
"platforms": 10,
|
| 1282 |
+
"closed": 0,
|
| 1283 |
+
"decisions": 24,
|
| 1284 |
+
"holds": 24,
|
| 1285 |
+
"short_turns": 0,
|
| 1286 |
+
"cancels": 0,
|
| 1287 |
+
"turn_points": 0,
|
| 1288 |
+
"avoidable": 21,
|
| 1289 |
+
"optimal": 38,
|
| 1290 |
+
"greedy_gap": 0.8,
|
| 1291 |
+
"closed_min": 0,
|
| 1292 |
+
"slow_min": 0,
|
| 1293 |
+
"track_min": 180,
|
| 1294 |
+
"single_closed": false,
|
| 1295 |
+
"mid_line": false,
|
| 1296 |
+
"late_sets": 0
|
| 1297 |
+
},
|
| 1298 |
+
"violations": 0,
|
| 1299 |
+
"agent_turns": 3,
|
| 1300 |
+
"checks": 2,
|
| 1301 |
+
"seconds": 137.7,
|
| 1302 |
+
"input_tokens": 50578,
|
| 1303 |
+
"cache_read_tokens": 21047,
|
| 1304 |
+
"cache_write_tokens": 29519,
|
| 1305 |
+
"output_tokens": 17651,
|
| 1306 |
+
"served_by": [
|
| 1307 |
+
"claude-sonnet-5-5"
|
| 1308 |
+
],
|
| 1309 |
+
"end_reason": "submitted",
|
| 1310 |
+
"errors": 0,
|
| 1311 |
+
"plan": [
|
| 1312 |
+
{
|
| 1313 |
+
"train": "37420",
|
| 1314 |
+
"hold": {
|
| 1315 |
+
"HDG": "06:31"
|
| 1316 |
+
}
|
| 1317 |
+
},
|
| 1318 |
+
{
|
| 1319 |
+
"train": "37413",
|
| 1320 |
+
"hold": {
|
| 1321 |
+
"DWE": "06:53"
|
| 1322 |
+
}
|
| 1323 |
+
},
|
| 1324 |
+
{
|
| 1325 |
+
"train": "37313",
|
| 1326 |
+
"hold": {
|
| 1327 |
+
"BP": "06:58"
|
| 1328 |
+
}
|
| 1329 |
+
},
|
| 1330 |
+
{
|
| 1331 |
+
"train": "37415",
|
| 1332 |
+
"hold": {
|
| 1333 |
+
"DWE": "07:23"
|
| 1334 |
+
}
|
| 1335 |
+
},
|
| 1336 |
+
{
|
| 1337 |
+
"train": "37315",
|
| 1338 |
+
"hold": {
|
| 1339 |
+
"ZH": "07:20"
|
| 1340 |
+
}
|
| 1341 |
+
},
|
| 1342 |
+
{
|
| 1343 |
+
"train": "37417",
|
| 1344 |
+
"hold": {
|
| 1345 |
+
"DWE": "07:53"
|
| 1346 |
+
}
|
| 1347 |
+
},
|
| 1348 |
+
{
|
| 1349 |
+
"train": "37317",
|
| 1350 |
+
"hold": {
|
| 1351 |
+
"BP": "07:58"
|
| 1352 |
+
}
|
| 1353 |
+
},
|
| 1354 |
+
{
|
| 1355 |
+
"train": "37419",
|
| 1356 |
+
"hold": {
|
| 1357 |
+
"DWE": "08:23"
|
| 1358 |
+
}
|
| 1359 |
+
},
|
| 1360 |
+
{
|
| 1361 |
+
"train": "37319",
|
| 1362 |
+
"hold": {
|
| 1363 |
+
"ZH": "08:20"
|
| 1364 |
+
}
|
| 1365 |
+
},
|
| 1366 |
+
{
|
| 1367 |
+
"train": "37421",
|
| 1368 |
+
"hold": {
|
| 1369 |
+
"DWE": "08:53"
|
| 1370 |
+
}
|
| 1371 |
+
},
|
| 1372 |
+
{
|
| 1373 |
+
"train": "37321",
|
| 1374 |
+
"hold": {
|
| 1375 |
+
"BP": "08:58"
|
| 1376 |
+
}
|
| 1377 |
+
}
|
| 1378 |
+
]
|
| 1379 |
+
},
|
| 1380 |
+
{
|
| 1381 |
+
"model": "claude-sonnet-5-5@high",
|
| 1382 |
+
"task_id": "gnlw-2025-03-27-1800-180m-compound-0",
|
| 1383 |
+
"split": "train",
|
| 1384 |
+
"difficulty": "compound",
|
| 1385 |
+
"day": "2025-03-27",
|
| 1386 |
+
"runs": 29,
|
| 1387 |
+
"reward": 0.850483,
|
| 1388 |
+
"submitted": true,
|
| 1389 |
+
"feasible": true,
|
| 1390 |
+
"cost": 546,
|
| 1391 |
+
"optimal_cost": 534,
|
| 1392 |
+
"floor_cost": 518,
|
| 1393 |
+
"greedy_cost": 1519,
|
| 1394 |
+
"real_cost": 344,
|
| 1395 |
+
"hardness": {
|
| 1396 |
+
"runs": 29,
|
| 1397 |
+
"sets": 7,
|
| 1398 |
+
"stops": 182,
|
| 1399 |
+
"conflicts": 36,
|
| 1400 |
+
"single_track": 3,
|
| 1401 |
+
"headway": 20,
|
| 1402 |
+
"platforms": 1,
|
| 1403 |
+
"closed": 12,
|
| 1404 |
+
"decisions": 30,
|
| 1405 |
+
"holds": 22,
|
| 1406 |
+
"short_turns": 8,
|
| 1407 |
+
"cancels": 0,
|
| 1408 |
+
"turn_points": 1,
|
| 1409 |
+
"avoidable": 16,
|
| 1410 |
+
"optimal": 534,
|
| 1411 |
+
"greedy_gap": 0.8,
|
| 1412 |
+
"closed_min": 59,
|
| 1413 |
+
"slow_min": 0,
|
| 1414 |
+
"track_min": 0,
|
| 1415 |
+
"single_closed": true,
|
| 1416 |
+
"mid_line": false,
|
| 1417 |
+
"late_sets": 1
|
| 1418 |
+
},
|
| 1419 |
+
"violations": 0,
|
| 1420 |
+
"agent_turns": 4,
|
| 1421 |
+
"checks": 3,
|
| 1422 |
+
"seconds": 122.5,
|
| 1423 |
+
"input_tokens": 101088,
|
| 1424 |
+
"cache_read_tokens": 63335,
|
| 1425 |
+
"cache_write_tokens": 37737,
|
| 1426 |
+
"output_tokens": 14374,
|
| 1427 |
+
"served_by": [
|
| 1428 |
+
"claude-sonnet-5-5"
|
| 1429 |
+
],
|
| 1430 |
+
"end_reason": "submitted",
|
| 1431 |
+
"errors": 0,
|
| 1432 |
+
"plan": [
|
| 1433 |
+
{
|
| 1434 |
+
"train": "37457",
|
| 1435 |
+
"to": "HDG"
|
| 1436 |
+
},
|
| 1437 |
+
{
|
| 1438 |
+
"train": "37368",
|
| 1439 |
+
"from": "HDG"
|
| 1440 |
+
},
|
| 1441 |
+
{
|
| 1442 |
+
"train": "37357",
|
| 1443 |
+
"to": "HDG"
|
| 1444 |
+
},
|
| 1445 |
+
{
|
| 1446 |
+
"train": "37468",
|
| 1447 |
+
"from": "HDG"
|
| 1448 |
+
},
|
| 1449 |
+
{
|
| 1450 |
+
"train": "37459",
|
| 1451 |
+
"to": "HDG",
|
| 1452 |
+
"hold": {
|
| 1453 |
+
"ZH": "18:11",
|
| 1454 |
+
"FWD": "18:33"
|
| 1455 |
+
}
|
| 1456 |
+
},
|
| 1457 |
+
{
|
| 1458 |
+
"train": "37370",
|
| 1459 |
+
"from": "HDG"
|
| 1460 |
+
},
|
| 1461 |
+
{
|
| 1462 |
+
"train": "37359",
|
| 1463 |
+
"to": "HDG"
|
| 1464 |
+
},
|
| 1465 |
+
{
|
| 1466 |
+
"train": "37470",
|
| 1467 |
+
"from": "HDG",
|
| 1468 |
+
"hold": {
|
| 1469 |
+
"HDG": "19:09"
|
| 1470 |
+
}
|
| 1471 |
+
},
|
| 1472 |
+
{
|
| 1473 |
+
"train": "37361",
|
| 1474 |
+
"hold": {
|
| 1475 |
+
"GN": "18:43"
|
| 1476 |
+
}
|
| 1477 |
+
},
|
| 1478 |
+
{
|
| 1479 |
+
"train": "37461",
|
| 1480 |
+
"hold": {
|
| 1481 |
+
"HDG": "19:04"
|
| 1482 |
+
}
|
| 1483 |
+
},
|
| 1484 |
+
{
|
| 1485 |
+
"train": "37365",
|
| 1486 |
+
"hold": {
|
| 1487 |
+
"GN": "19:40"
|
| 1488 |
+
}
|
| 1489 |
+
}
|
| 1490 |
+
]
|
| 1491 |
+
},
|
| 1492 |
+
{
|
| 1493 |
+
"model": "claude-sonnet-5-5@high",
|
| 1494 |
+
"task_id": "gnlw-2025-04-07-1730-240m-compound-0",
|
| 1495 |
+
"split": "train",
|
| 1496 |
+
"difficulty": "compound",
|
| 1497 |
+
"day": "2025-04-07",
|
| 1498 |
+
"runs": 35,
|
| 1499 |
+
"reward": 0.735395,
|
| 1500 |
+
"submitted": true,
|
| 1501 |
+
"feasible": true,
|
| 1502 |
+
"cost": 637,
|
| 1503 |
+
"optimal_cost": 587,
|
| 1504 |
+
"floor_cost": 438,
|
| 1505 |
+
"greedy_cost": null,
|
| 1506 |
+
"real_cost": 94,
|
| 1507 |
+
"hardness": {
|
| 1508 |
+
"runs": 35,
|
| 1509 |
+
"sets": 7,
|
| 1510 |
+
"stops": 214,
|
| 1511 |
+
"conflicts": 32,
|
| 1512 |
+
"single_track": 9,
|
| 1513 |
+
"headway": 17,
|
| 1514 |
+
"platforms": 2,
|
| 1515 |
+
"closed": 4,
|
| 1516 |
+
"decisions": 25,
|
| 1517 |
+
"holds": 21,
|
| 1518 |
+
"short_turns": 4,
|
| 1519 |
+
"cancels": 0,
|
| 1520 |
+
"turn_points": 2,
|
| 1521 |
+
"avoidable": 149,
|
| 1522 |
+
"optimal": 587,
|
| 1523 |
+
"greedy_gap": 0.84,
|
| 1524 |
+
"closed_min": 30,
|
| 1525 |
+
"slow_min": 0,
|
| 1526 |
+
"track_min": 10,
|
| 1527 |
+
"single_closed": true,
|
| 1528 |
+
"mid_line": true,
|
| 1529 |
+
"late_sets": 1
|
| 1530 |
+
},
|
| 1531 |
+
"violations": 0,
|
| 1532 |
+
"agent_turns": 5,
|
| 1533 |
+
"checks": 4,
|
| 1534 |
+
"seconds": 299.9,
|
| 1535 |
+
"input_tokens": 199091,
|
| 1536 |
+
"cache_read_tokens": 133278,
|
| 1537 |
+
"cache_write_tokens": 65793,
|
| 1538 |
+
"output_tokens": 39094,
|
| 1539 |
+
"served_by": [
|
| 1540 |
+
"claude-sonnet-5-5"
|
| 1541 |
+
],
|
| 1542 |
+
"end_reason": "submitted",
|
| 1543 |
+
"errors": 0,
|
| 1544 |
+
"plan": [
|
| 1545 |
+
{
|
| 1546 |
+
"train": "37355",
|
| 1547 |
+
"to": "HDG",
|
| 1548 |
+
"hold": {
|
| 1549 |
+
"FWD": "18:03"
|
| 1550 |
+
}
|
| 1551 |
+
},
|
| 1552 |
+
{
|
| 1553 |
+
"train": "37457",
|
| 1554 |
+
"hold": {
|
| 1555 |
+
"DWE": "18:01",
|
| 1556 |
+
"FWD": "18:08"
|
| 1557 |
+
}
|
| 1558 |
+
},
|
| 1559 |
+
{
|
| 1560 |
+
"train": "37366",
|
| 1561 |
+
"hold": {
|
| 1562 |
+
"LW": "18:00",
|
| 1563 |
+
"FWD": "18:11"
|
| 1564 |
+
}
|
| 1565 |
+
},
|
| 1566 |
+
{
|
| 1567 |
+
"train": "37466",
|
| 1568 |
+
"from": "HDG",
|
| 1569 |
+
"hold": {
|
| 1570 |
+
"HDG": "18:11",
|
| 1571 |
+
"FWD": "18:14"
|
| 1572 |
+
}
|
| 1573 |
+
},
|
| 1574 |
+
{
|
| 1575 |
+
"train": "37459",
|
| 1576 |
+
"hold": {
|
| 1577 |
+
"GN": "17:57",
|
| 1578 |
+
"HDG": "18:37"
|
| 1579 |
+
}
|
| 1580 |
+
},
|
| 1581 |
+
{
|
| 1582 |
+
"train": "37359",
|
| 1583 |
+
"hold": {
|
| 1584 |
+
"ZH": "18:26",
|
| 1585 |
+
"FWD": "18:41"
|
| 1586 |
+
}
|
| 1587 |
+
},
|
| 1588 |
+
{
|
| 1589 |
+
"train": "37368",
|
| 1590 |
+
"hold": {
|
| 1591 |
+
"LW": "18:26"
|
| 1592 |
+
}
|
| 1593 |
+
},
|
| 1594 |
+
{
|
| 1595 |
+
"train": "37361",
|
| 1596 |
+
"hold": {
|
| 1597 |
+
"GN": "18:44",
|
| 1598 |
+
"FWD": "19:09"
|
| 1599 |
+
}
|
| 1600 |
+
},
|
| 1601 |
+
{
|
| 1602 |
+
"train": "37463",
|
| 1603 |
+
"hold": {
|
| 1604 |
+
"FWD": "19:26"
|
| 1605 |
+
}
|
| 1606 |
+
}
|
| 1607 |
+
]
|
| 1608 |
+
},
|
| 1609 |
+
{
|
| 1610 |
+
"model": "claude-sonnet-5-5@high",
|
| 1611 |
+
"task_id": "gnlw-2025-05-01-0800-180m-blockage-0",
|
| 1612 |
+
"split": "train",
|
| 1613 |
+
"difficulty": "blockage",
|
| 1614 |
+
"day": "2025-05-01",
|
| 1615 |
+
"runs": 30,
|
| 1616 |
+
"reward": 0.428932,
|
| 1617 |
+
"submitted": true,
|
| 1618 |
+
"feasible": true,
|
| 1619 |
+
"cost": 675,
|
| 1620 |
+
"optimal_cost": 543,
|
| 1621 |
+
"floor_cost": 432,
|
| 1622 |
+
"greedy_cost": 1278,
|
| 1623 |
+
"real_cost": 58,
|
| 1624 |
+
"hardness": {
|
| 1625 |
+
"runs": 30,
|
| 1626 |
+
"sets": 7,
|
| 1627 |
+
"stops": 200,
|
| 1628 |
+
"conflicts": 8,
|
| 1629 |
+
"single_track": 0,
|
| 1630 |
+
"headway": 0,
|
| 1631 |
+
"platforms": 0,
|
| 1632 |
+
"closed": 8,
|
| 1633 |
+
"decisions": 27,
|
| 1634 |
+
"holds": 21,
|
| 1635 |
+
"short_turns": 6,
|
| 1636 |
+
"cancels": 0,
|
| 1637 |
+
"turn_points": 2,
|
| 1638 |
+
"avoidable": 111,
|
| 1639 |
+
"optimal": 543,
|
| 1640 |
+
"greedy_gap": 0.784,
|
| 1641 |
+
"closed_min": 26,
|
| 1642 |
+
"slow_min": 0,
|
| 1643 |
+
"track_min": 0,
|
| 1644 |
+
"single_closed": false,
|
| 1645 |
+
"mid_line": true,
|
| 1646 |
+
"late_sets": 0
|
| 1647 |
+
},
|
| 1648 |
+
"violations": 0,
|
| 1649 |
+
"agent_turns": 5,
|
| 1650 |
+
"checks": 4,
|
| 1651 |
+
"seconds": 350.5,
|
| 1652 |
+
"input_tokens": 179882,
|
| 1653 |
+
"cache_read_tokens": 108749,
|
| 1654 |
+
"cache_write_tokens": 71113,
|
| 1655 |
+
"output_tokens": 44922,
|
| 1656 |
+
"served_by": [
|
| 1657 |
+
"claude-sonnet-5-5"
|
| 1658 |
+
],
|
| 1659 |
+
"end_reason": "submitted",
|
| 1660 |
+
"errors": 0,
|
| 1661 |
+
"plan": [
|
| 1662 |
+
{
|
| 1663 |
+
"train": "37419",
|
| 1664 |
+
"hold": {
|
| 1665 |
+
"GK": "08:36"
|
| 1666 |
+
},
|
| 1667 |
+
"to": "HDG"
|
| 1668 |
+
},
|
| 1669 |
+
{
|
| 1670 |
+
"train": "37319",
|
| 1671 |
+
"hold": {
|
| 1672 |
+
"GN": "08:32"
|
| 1673 |
+
},
|
| 1674 |
+
"to": "HDG"
|
| 1675 |
+
},
|
| 1676 |
+
{
|
| 1677 |
+
"train": "37421",
|
| 1678 |
+
"hold": {
|
| 1679 |
+
"GN": "08:35",
|
| 1680 |
+
"BP": "08:57"
|
| 1681 |
+
}
|
| 1682 |
+
},
|
| 1683 |
+
{
|
| 1684 |
+
"train": "37321",
|
| 1685 |
+
"hold": {
|
| 1686 |
+
"GN": "08:46",
|
| 1687 |
+
"BP": "09:04"
|
| 1688 |
+
}
|
| 1689 |
+
},
|
| 1690 |
+
{
|
| 1691 |
+
"train": "37426",
|
| 1692 |
+
"hold": {
|
| 1693 |
+
"DWE": "08:36"
|
| 1694 |
+
},
|
| 1695 |
+
"to": "ZH"
|
| 1696 |
+
},
|
| 1697 |
+
{
|
| 1698 |
+
"train": "37423",
|
| 1699 |
+
"from": "ZH",
|
| 1700 |
+
"hold": {
|
| 1701 |
+
"ZH": "09:04",
|
| 1702 |
+
"FWD": "09:28"
|
| 1703 |
+
}
|
| 1704 |
+
},
|
| 1705 |
+
{
|
| 1706 |
+
"train": "37328",
|
| 1707 |
+
"hold": {
|
| 1708 |
+
"FWD": "08:37",
|
| 1709 |
+
"BP": "08:49"
|
| 1710 |
+
}
|
| 1711 |
+
},
|
| 1712 |
+
{
|
| 1713 |
+
"train": "37428",
|
| 1714 |
+
"hold": {
|
| 1715 |
+
"FWD": "08:42",
|
| 1716 |
+
"BP": "08:52"
|
| 1717 |
+
}
|
| 1718 |
+
},
|
| 1719 |
+
{
|
| 1720 |
+
"train": "37330",
|
| 1721 |
+
"from": "HDG"
|
| 1722 |
+
},
|
| 1723 |
+
{
|
| 1724 |
+
"train": "37430",
|
| 1725 |
+
"from": "HDG",
|
| 1726 |
+
"hold": {
|
| 1727 |
+
"GK": "09:22"
|
| 1728 |
+
}
|
| 1729 |
+
},
|
| 1730 |
+
{
|
| 1731 |
+
"train": "37432",
|
| 1732 |
+
"hold": {
|
| 1733 |
+
"HDG": "09:31"
|
| 1734 |
+
}
|
| 1735 |
+
},
|
| 1736 |
+
{
|
| 1737 |
+
"train": "37323",
|
| 1738 |
+
"hold": {
|
| 1739 |
+
"GN": "09:12"
|
| 1740 |
+
}
|
| 1741 |
+
}
|
| 1742 |
+
]
|
| 1743 |
+
},
|
| 1744 |
+
{
|
| 1745 |
+
"model": "claude-sonnet-5-5@high",
|
| 1746 |
+
"task_id": "gnlw-2025-05-11-0800-180m-loop-0",
|
| 1747 |
+
"split": "train",
|
| 1748 |
+
"difficulty": "loop",
|
| 1749 |
+
"day": "2025-05-11",
|
| 1750 |
+
"runs": 15,
|
| 1751 |
+
"reward": 0.832784,
|
| 1752 |
+
"submitted": true,
|
| 1753 |
+
"feasible": true,
|
| 1754 |
+
"cost": 211,
|
| 1755 |
+
"optimal_cost": 194,
|
| 1756 |
+
"floor_cost": 149,
|
| 1757 |
+
"greedy_cost": null,
|
| 1758 |
+
"real_cost": 38,
|
| 1759 |
+
"hardness": {
|
| 1760 |
+
"runs": 15,
|
| 1761 |
+
"sets": 4,
|
| 1762 |
+
"stops": 86,
|
| 1763 |
+
"conflicts": 3,
|
| 1764 |
+
"single_track": 1,
|
| 1765 |
+
"headway": 0,
|
| 1766 |
+
"platforms": 2,
|
| 1767 |
+
"closed": 0,
|
| 1768 |
+
"decisions": 10,
|
| 1769 |
+
"holds": 10,
|
| 1770 |
+
"short_turns": 0,
|
| 1771 |
+
"cancels": 0,
|
| 1772 |
+
"turn_points": 0,
|
| 1773 |
+
"avoidable": 45,
|
| 1774 |
+
"optimal": 194,
|
| 1775 |
+
"greedy_gap": 0.8267,
|
| 1776 |
+
"closed_min": 0,
|
| 1777 |
+
"slow_min": 64,
|
| 1778 |
+
"track_min": 170,
|
| 1779 |
+
"single_closed": false,
|
| 1780 |
+
"mid_line": false,
|
| 1781 |
+
"late_sets": 0
|
| 1782 |
+
},
|
| 1783 |
+
"violations": 0,
|
| 1784 |
+
"agent_turns": 4,
|
| 1785 |
+
"checks": 3,
|
| 1786 |
+
"seconds": 76.3,
|
| 1787 |
+
"input_tokens": 56624,
|
| 1788 |
+
"cache_read_tokens": 36772,
|
| 1789 |
+
"cache_write_tokens": 19836,
|
| 1790 |
+
"output_tokens": 8659,
|
| 1791 |
+
"served_by": [
|
| 1792 |
+
"claude-sonnet-5-5"
|
| 1793 |
+
],
|
| 1794 |
+
"end_reason": "submitted",
|
| 1795 |
+
"errors": 0,
|
| 1796 |
+
"plan": [
|
| 1797 |
+
{
|
| 1798 |
+
"train": "37328",
|
| 1799 |
+
"hold": {
|
| 1800 |
+
"BP": "08:37"
|
| 1801 |
+
}
|
| 1802 |
+
},
|
| 1803 |
+
{
|
| 1804 |
+
"train": "37423",
|
| 1805 |
+
"hold": {
|
| 1806 |
+
"FWD": "09:34"
|
| 1807 |
+
}
|
| 1808 |
+
},
|
| 1809 |
+
{
|
| 1810 |
+
"train": "37427",
|
| 1811 |
+
"hold": {
|
| 1812 |
+
"FWD": "10:35",
|
| 1813 |
+
"HDG": "10:42"
|
| 1814 |
+
}
|
| 1815 |
+
}
|
| 1816 |
+
]
|
| 1817 |
+
},
|
| 1818 |
+
{
|
| 1819 |
+
"model": "claude-sonnet-5-5@high",
|
| 1820 |
+
"task_id": "gnlw-2025-05-20-1530-180m-loop-0",
|
| 1821 |
+
"split": "train",
|
| 1822 |
+
"difficulty": "loop",
|
| 1823 |
+
"day": "2025-05-20",
|
| 1824 |
+
"runs": 30,
|
| 1825 |
+
"reward": 0.828085,
|
| 1826 |
+
"submitted": true,
|
| 1827 |
+
"feasible": true,
|
| 1828 |
+
"cost": 144,
|
| 1829 |
+
"optimal_cost": 129,
|
| 1830 |
+
"floor_cost": 105,
|
| 1831 |
+
"greedy_cost": 803,
|
| 1832 |
+
"real_cost": 1630,
|
| 1833 |
+
"hardness": {
|
| 1834 |
+
"runs": 30,
|
| 1835 |
+
"sets": 7,
|
| 1836 |
+
"stops": 202,
|
| 1837 |
+
"conflicts": 7,
|
| 1838 |
+
"single_track": 1,
|
| 1839 |
+
"headway": 4,
|
| 1840 |
+
"platforms": 2,
|
| 1841 |
+
"closed": 0,
|
| 1842 |
+
"decisions": 21,
|
| 1843 |
+
"holds": 21,
|
| 1844 |
+
"short_turns": 0,
|
| 1845 |
+
"cancels": 0,
|
| 1846 |
+
"turn_points": 0,
|
| 1847 |
+
"avoidable": 24,
|
| 1848 |
+
"optimal": 129,
|
| 1849 |
+
"greedy_gap": 0.8,
|
| 1850 |
+
"closed_min": 0,
|
| 1851 |
+
"slow_min": 0,
|
| 1852 |
+
"track_min": 150,
|
| 1853 |
+
"single_closed": false,
|
| 1854 |
+
"mid_line": false,
|
| 1855 |
+
"late_sets": 0
|
| 1856 |
+
},
|
| 1857 |
+
"violations": 0,
|
| 1858 |
+
"agent_turns": 4,
|
| 1859 |
+
"checks": 3,
|
| 1860 |
+
"seconds": 169.6,
|
| 1861 |
+
"input_tokens": 111337,
|
| 1862 |
+
"cache_read_tokens": 67253,
|
| 1863 |
+
"cache_write_tokens": 44068,
|
| 1864 |
+
"output_tokens": 21782,
|
| 1865 |
+
"served_by": [
|
| 1866 |
+
"claude-sonnet-5-5"
|
| 1867 |
+
],
|
| 1868 |
+
"end_reason": "submitted",
|
| 1869 |
+
"errors": 0,
|
| 1870 |
+
"plan": [
|
| 1871 |
+
{
|
| 1872 |
+
"train": "37449",
|
| 1873 |
+
"hold": {
|
| 1874 |
+
"HDG": "16:01"
|
| 1875 |
+
}
|
| 1876 |
+
},
|
| 1877 |
+
{
|
| 1878 |
+
"train": "37358",
|
| 1879 |
+
"hold": {
|
| 1880 |
+
"LW": "15:55",
|
| 1881 |
+
"HDG": "16:03",
|
| 1882 |
+
"FWD": "16:10"
|
| 1883 |
+
}
|
| 1884 |
+
},
|
| 1885 |
+
{
|
| 1886 |
+
"train": "37349",
|
| 1887 |
+
"hold": {
|
| 1888 |
+
"BP": "15:59"
|
| 1889 |
+
}
|
| 1890 |
+
},
|
| 1891 |
+
{
|
| 1892 |
+
"train": "37458",
|
| 1893 |
+
"hold": {
|
| 1894 |
+
"BP": "16:19"
|
| 1895 |
+
}
|
| 1896 |
+
}
|
| 1897 |
+
]
|
| 1898 |
+
}
|
| 1899 |
+
]
|
| 1900 |
+
}
|
tasks/rail-v1-eval/manifest.json
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "rail-v1-eval",
|
| 3 |
+
"split": "eval",
|
| 4 |
+
"tasks": 45,
|
| 5 |
+
"file": "tasks.jsonl",
|
| 6 |
+
"tiers": {
|
| 7 |
+
"blockage": 20,
|
| 8 |
+
"compound": 17,
|
| 9 |
+
"loop": 4,
|
| 10 |
+
"real": 4
|
| 11 |
+
},
|
| 12 |
+
"sha256": "2b4347efbc1b0aef26225aeebd451e64f4a3bf427f41a0a7e4618795d5db034b",
|
| 13 |
+
"candidates": 134,
|
| 14 |
+
"days": [
|
| 15 |
+
"2025-01-20",
|
| 16 |
+
"2025-01-21",
|
| 17 |
+
"2025-01-22",
|
| 18 |
+
"2025-01-23",
|
| 19 |
+
"2025-01-25",
|
| 20 |
+
"2025-01-26",
|
| 21 |
+
"2025-02-11",
|
| 22 |
+
"2025-02-12",
|
| 23 |
+
"2025-02-13",
|
| 24 |
+
"2025-02-14",
|
| 25 |
+
"2025-02-15",
|
| 26 |
+
"2025-02-16",
|
| 27 |
+
"2025-03-10",
|
| 28 |
+
"2025-03-11",
|
| 29 |
+
"2025-03-12",
|
| 30 |
+
"2025-03-13",
|
| 31 |
+
"2025-03-16",
|
| 32 |
+
"2025-04-23",
|
| 33 |
+
"2025-04-25",
|
| 34 |
+
"2025-04-26",
|
| 35 |
+
"2025-04-27",
|
| 36 |
+
"2025-05-26",
|
| 37 |
+
"2025-05-27",
|
| 38 |
+
"2025-05-28",
|
| 39 |
+
"2025-05-30",
|
| 40 |
+
"2025-05-31",
|
| 41 |
+
"2025-06-22"
|
| 42 |
+
],
|
| 43 |
+
"proven_optimal": 45,
|
| 44 |
+
"runs": [
|
| 45 |
+
15,
|
| 46 |
+
38
|
| 47 |
+
],
|
| 48 |
+
"source": "Rijden de Treinen train archive and disruptions, 2024-2025 (CC BY 4.0); OpenStreetMap (ODbL); see data/gnlw/SOURCE.md",
|
| 49 |
+
"selected_from": "pool-eval",
|
| 50 |
+
"selection": {
|
| 51 |
+
"calibration_episodes": 16,
|
| 52 |
+
"features": [
|
| 53 |
+
"runs",
|
| 54 |
+
"conflicts",
|
| 55 |
+
"decisions",
|
| 56 |
+
"holds",
|
| 57 |
+
"short_turns",
|
| 58 |
+
"turn_points",
|
| 59 |
+
"avoidable",
|
| 60 |
+
"slow_min",
|
| 61 |
+
"track_min",
|
| 62 |
+
"single_closed"
|
| 63 |
+
],
|
| 64 |
+
"lambda": 3.0,
|
| 65 |
+
"weights": {
|
| 66 |
+
"intercept": 0.5618,
|
| 67 |
+
"runs": 0.0715,
|
| 68 |
+
"conflicts": 0.0631,
|
| 69 |
+
"decisions": -0.0708,
|
| 70 |
+
"holds": -0.0433,
|
| 71 |
+
"short_turns": -0.0466,
|
| 72 |
+
"turn_points": -0.1022,
|
| 73 |
+
"avoidable": 0.0245,
|
| 74 |
+
"slow_min": -0.0429,
|
| 75 |
+
"track_min": 0.1,
|
| 76 |
+
"single_closed": 0.0544
|
| 77 |
+
},
|
| 78 |
+
"loo_mae": 0.1343,
|
| 79 |
+
"loo_spearman": 0.6676,
|
| 80 |
+
"calibration_mean_reward": 0.5572,
|
| 81 |
+
"pool": 72,
|
| 82 |
+
"chosen": 45,
|
| 83 |
+
"per_tier": {
|
| 84 |
+
"blockage": 20,
|
| 85 |
+
"compound": 17,
|
| 86 |
+
"loop": 4,
|
| 87 |
+
"real": 4
|
| 88 |
+
},
|
| 89 |
+
"predicted_mean_chosen": 0.443,
|
| 90 |
+
"predicted_mean_pool": 0.5628
|
| 91 |
+
}
|
| 92 |
+
}
|
tasks/rail-v1-eval/selection.json
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"calibration_episodes": 16,
|
| 3 |
+
"features": [
|
| 4 |
+
"runs",
|
| 5 |
+
"conflicts",
|
| 6 |
+
"decisions",
|
| 7 |
+
"holds",
|
| 8 |
+
"short_turns",
|
| 9 |
+
"turn_points",
|
| 10 |
+
"avoidable",
|
| 11 |
+
"slow_min",
|
| 12 |
+
"track_min",
|
| 13 |
+
"single_closed"
|
| 14 |
+
],
|
| 15 |
+
"lambda": 3.0,
|
| 16 |
+
"weights": {
|
| 17 |
+
"intercept": 0.5618,
|
| 18 |
+
"runs": 0.0715,
|
| 19 |
+
"conflicts": 0.0631,
|
| 20 |
+
"decisions": -0.0708,
|
| 21 |
+
"holds": -0.0433,
|
| 22 |
+
"short_turns": -0.0466,
|
| 23 |
+
"turn_points": -0.1022,
|
| 24 |
+
"avoidable": 0.0245,
|
| 25 |
+
"slow_min": -0.0429,
|
| 26 |
+
"track_min": 0.1,
|
| 27 |
+
"single_closed": 0.0544
|
| 28 |
+
},
|
| 29 |
+
"loo_mae": 0.1343,
|
| 30 |
+
"loo_spearman": 0.6676,
|
| 31 |
+
"calibration_mean_reward": 0.5572,
|
| 32 |
+
"pool": 72,
|
| 33 |
+
"chosen": 45,
|
| 34 |
+
"per_tier": {
|
| 35 |
+
"blockage": 20,
|
| 36 |
+
"compound": 17,
|
| 37 |
+
"loop": 4,
|
| 38 |
+
"real": 4
|
| 39 |
+
},
|
| 40 |
+
"predicted_mean_chosen": 0.443,
|
| 41 |
+
"predicted_mean_pool": 0.5628,
|
| 42 |
+
"predictions": {
|
| 43 |
+
"gnlw-2025-02-12-0900-180m-real-0": 0.1894,
|
| 44 |
+
"gnlw-2025-04-26-0630-180m-blockage-0": 0.2287,
|
| 45 |
+
"gnlw-2025-03-16-1700-210m-compound-0": 0.2512,
|
| 46 |
+
"gnlw-2025-04-27-0830-210m-blockage-0": 0.2549,
|
| 47 |
+
"gnlw-2025-01-20-1800-180m-compound-0": 0.2581,
|
| 48 |
+
"gnlw-2025-01-26-1700-210m-compound-0": 0.273,
|
| 49 |
+
"gnlw-2025-02-14-1730-210m-blockage-0": 0.2856,
|
| 50 |
+
"gnlw-2025-02-15-0630-180m-blockage-0": 0.3048,
|
| 51 |
+
"gnlw-2025-04-27-0800-210m-compound-0": 0.3232,
|
| 52 |
+
"gnlw-2025-05-31-0900-180m-compound-0": 0.3594,
|
| 53 |
+
"gnlw-2025-01-25-1700-180m-blockage-0": 0.363,
|
| 54 |
+
"gnlw-2025-02-15-1630-150m-loop-0": 0.3773,
|
| 55 |
+
"gnlw-2025-05-30-0630-180m-blockage-0": 0.3973,
|
| 56 |
+
"gnlw-2025-02-13-0954-180m-real-0": 0.4004,
|
| 57 |
+
"gnlw-2025-02-11-0900-150m-blockage-0": 0.4051,
|
| 58 |
+
"gnlw-2025-01-23-0900-180m-compound-0": 0.4195,
|
| 59 |
+
"gnlw-2025-04-26-1700-180m-blockage-0": 0.4247,
|
| 60 |
+
"gnlw-2025-02-13-1530-180m-blockage-0": 0.4299,
|
| 61 |
+
"gnlw-2025-03-12-1730-210m-loop-0": 0.441,
|
| 62 |
+
"gnlw-2025-02-16-1730-240m-compound-0": 0.4457,
|
| 63 |
+
"gnlw-2025-05-26-0730-150m-blockage-0": 0.4553,
|
| 64 |
+
"gnlw-2025-01-25-0630-180m-loop-0": 0.4555,
|
| 65 |
+
"gnlw-2025-04-23-1630-150m-blockage-0": 0.4577,
|
| 66 |
+
"gnlw-2025-03-12-1530-180m-blockage-0": 0.4677,
|
| 67 |
+
"gnlw-2025-01-23-0800-210m-compound-0": 0.472,
|
| 68 |
+
"gnlw-2025-01-25-0730-180m-compound-0": 0.4929,
|
| 69 |
+
"gnlw-2025-05-27-0830-210m-blockage-0": 0.4951,
|
| 70 |
+
"gnlw-2025-05-26-1800-150m-blockage-0": 0.4972,
|
| 71 |
+
"gnlw-2025-03-13-0706-180m-real-0": 0.5019,
|
| 72 |
+
"gnlw-2025-06-22-1600-240m-compound-0": 0.5068,
|
| 73 |
+
"gnlw-2025-03-16-1630-180m-compound-0": 0.5089,
|
| 74 |
+
"gnlw-2025-03-10-0700-210m-blockage-0": 0.5156,
|
| 75 |
+
"gnlw-2025-02-15-0700-240m-compound-0": 0.5177,
|
| 76 |
+
"gnlw-2025-01-21-0626-180m-real-0": 0.525,
|
| 77 |
+
"gnlw-2025-02-16-1700-210m-compound-0": 0.5306,
|
| 78 |
+
"gnlw-2025-05-27-1700-180m-blockage-0": 0.5325,
|
| 79 |
+
"gnlw-2025-02-16-1530-210m-compound-0": 0.5331,
|
| 80 |
+
"gnlw-2025-01-22-1600-240m-compound-0": 0.5439,
|
| 81 |
+
"gnlw-2025-01-25-1800-150m-blockage-0": 0.5567,
|
| 82 |
+
"gnlw-2025-04-25-1730-210m-blockage-0": 0.5697,
|
| 83 |
+
"gnlw-2025-05-26-1630-150m-blockage-0": 0.5737,
|
| 84 |
+
"gnlw-2025-05-28-1830-210m-compound-0": 0.5761,
|
| 85 |
+
"gnlw-2025-01-22-1800-150m-loop-0": 0.5792,
|
| 86 |
+
"gnlw-2025-05-27-1830-180m-blockage-0": 0.5939,
|
| 87 |
+
"gnlw-2025-03-11-0730-150m-blockage-0": 0.5968,
|
| 88 |
+
"gnlw-2025-05-31-0800-210m-compound-0": 0.605,
|
| 89 |
+
"gnlw-2025-04-25-0700-210m-blockage-0": 0.6173,
|
| 90 |
+
"gnlw-2025-02-14-1830-180m-blockage-0": 0.6397,
|
| 91 |
+
"gnlw-2025-04-27-1700-210m-compound-0": 0.645,
|
| 92 |
+
"gnlw-2025-05-27-0730-150m-blockage-0": 0.6544,
|
| 93 |
+
"gnlw-2025-05-31-0630-180m-blockage-0": 0.6643,
|
| 94 |
+
"gnlw-2025-02-12-1830-180m-blockage-0": 0.6643,
|
| 95 |
+
"gnlw-2025-02-12-0700-210m-blockage-0": 0.6749,
|
| 96 |
+
"gnlw-2025-04-26-0630-210m-compound-0": 0.6751,
|
| 97 |
+
"gnlw-2025-04-25-0730-180m-compound-0": 0.6791,
|
| 98 |
+
"gnlw-2025-04-25-0630-180m-loop-0": 0.6867,
|
| 99 |
+
"gnlw-2025-04-27-1700-180m-blockage-0": 0.7217,
|
| 100 |
+
"gnlw-2025-02-12-1800-180m-compound-0": 0.7405,
|
| 101 |
+
"gnlw-2025-01-25-1830-180m-loop-0": 0.7664,
|
| 102 |
+
"gnlw-2025-03-12-0830-210m-loop-0": 0.7672,
|
| 103 |
+
"gnlw-2025-04-25-1800-180m-compound-0": 0.7813,
|
| 104 |
+
"gnlw-2025-03-14-0821-180m-real-0": 0.7952,
|
| 105 |
+
"gnlw-2025-02-15-0800-180m-loop-0": 0.8057,
|
| 106 |
+
"gnlw-2025-02-10-0830-210m-loop-0": 0.8268,
|
| 107 |
+
"gnlw-2025-01-25-0630-180m-blockage-0": 0.8333,
|
| 108 |
+
"gnlw-2025-02-14-1700-180m-loop-0": 0.834,
|
| 109 |
+
"gnlw-2025-02-14-0730-180m-compound-0": 0.8568,
|
| 110 |
+
"gnlw-2025-04-23-1700-180m-blockage-0": 0.8771,
|
| 111 |
+
"gnlw-2025-04-26-0700-240m-compound-0": 0.9031,
|
| 112 |
+
"gnlw-2025-06-22-0830-240m-compound-0": 0.9419,
|
| 113 |
+
"gnlw-2025-03-12-1700-210m-compound-0": 0.974,
|
| 114 |
+
"gnlw-2025-05-27-0830-240m-compound-0": 1.0029
|
| 115 |
+
}
|
| 116 |
+
}
|
tasks/rail-v1-eval/tasks.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
tasks/rail-v1-train/manifest.json
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "rail-v1-train",
|
| 3 |
+
"split": "train",
|
| 4 |
+
"tasks": 159,
|
| 5 |
+
"file": "tasks.jsonl",
|
| 6 |
+
"tiers": {
|
| 7 |
+
"blockage": 65,
|
| 8 |
+
"compound": 51,
|
| 9 |
+
"loop": 39,
|
| 10 |
+
"real": 4
|
| 11 |
+
},
|
| 12 |
+
"sha256": "4b1abf5acec175d2f24c5ec6ade8721a82932eda280d514ff3d9285e0a8501b2",
|
| 13 |
+
"candidates": 292,
|
| 14 |
+
"days": [
|
| 15 |
+
"2025-01-01",
|
| 16 |
+
"2025-01-02",
|
| 17 |
+
"2025-01-03",
|
| 18 |
+
"2025-01-04",
|
| 19 |
+
"2025-01-05",
|
| 20 |
+
"2025-01-07",
|
| 21 |
+
"2025-01-08",
|
| 22 |
+
"2025-01-13",
|
| 23 |
+
"2025-01-14",
|
| 24 |
+
"2025-01-15",
|
| 25 |
+
"2025-01-16",
|
| 26 |
+
"2025-01-19",
|
| 27 |
+
"2025-01-28",
|
| 28 |
+
"2025-01-29",
|
| 29 |
+
"2025-01-30",
|
| 30 |
+
"2025-02-01",
|
| 31 |
+
"2025-02-02",
|
| 32 |
+
"2025-02-03",
|
| 33 |
+
"2025-02-04",
|
| 34 |
+
"2025-02-05",
|
| 35 |
+
"2025-02-07",
|
| 36 |
+
"2025-02-08",
|
| 37 |
+
"2025-02-19",
|
| 38 |
+
"2025-02-20",
|
| 39 |
+
"2025-02-22",
|
| 40 |
+
"2025-02-24",
|
| 41 |
+
"2025-02-26",
|
| 42 |
+
"2025-02-27",
|
| 43 |
+
"2025-02-28",
|
| 44 |
+
"2025-03-03",
|
| 45 |
+
"2025-03-06",
|
| 46 |
+
"2025-03-09",
|
| 47 |
+
"2025-03-17",
|
| 48 |
+
"2025-03-19",
|
| 49 |
+
"2025-03-20",
|
| 50 |
+
"2025-03-22",
|
| 51 |
+
"2025-03-24",
|
| 52 |
+
"2025-03-25",
|
| 53 |
+
"2025-03-26",
|
| 54 |
+
"2025-03-27",
|
| 55 |
+
"2025-03-29",
|
| 56 |
+
"2025-03-30",
|
| 57 |
+
"2025-03-31",
|
| 58 |
+
"2025-04-01",
|
| 59 |
+
"2025-04-02",
|
| 60 |
+
"2025-04-03",
|
| 61 |
+
"2025-04-04",
|
| 62 |
+
"2025-04-05",
|
| 63 |
+
"2025-04-06",
|
| 64 |
+
"2025-04-07",
|
| 65 |
+
"2025-04-08",
|
| 66 |
+
"2025-04-09",
|
| 67 |
+
"2025-04-10",
|
| 68 |
+
"2025-04-11",
|
| 69 |
+
"2025-04-12",
|
| 70 |
+
"2025-04-14",
|
| 71 |
+
"2025-04-15",
|
| 72 |
+
"2025-04-17",
|
| 73 |
+
"2025-04-19",
|
| 74 |
+
"2025-04-29",
|
| 75 |
+
"2025-04-30",
|
| 76 |
+
"2025-05-01",
|
| 77 |
+
"2025-05-03",
|
| 78 |
+
"2025-05-04",
|
| 79 |
+
"2025-05-05",
|
| 80 |
+
"2025-05-06",
|
| 81 |
+
"2025-05-07",
|
| 82 |
+
"2025-05-08",
|
| 83 |
+
"2025-05-09",
|
| 84 |
+
"2025-05-11",
|
| 85 |
+
"2025-05-12",
|
| 86 |
+
"2025-05-13",
|
| 87 |
+
"2025-05-15",
|
| 88 |
+
"2025-05-17",
|
| 89 |
+
"2025-05-19",
|
| 90 |
+
"2025-05-20",
|
| 91 |
+
"2025-05-21",
|
| 92 |
+
"2025-06-02",
|
| 93 |
+
"2025-06-04",
|
| 94 |
+
"2025-06-06",
|
| 95 |
+
"2025-06-07",
|
| 96 |
+
"2025-06-08",
|
| 97 |
+
"2025-06-09",
|
| 98 |
+
"2025-06-10"
|
| 99 |
+
],
|
| 100 |
+
"proven_optimal": 159,
|
| 101 |
+
"runs": [
|
| 102 |
+
15,
|
| 103 |
+
38
|
| 104 |
+
],
|
| 105 |
+
"source": "Rijden de Treinen train archive and disruptions, 2024-2025 (CC BY 4.0); OpenStreetMap (ODbL); see data/gnlw/SOURCE.md"
|
| 106 |
+
}
|
tasks/rail-v1-train/tasks.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|