Spaces:
Sleeping
Sleeping
Download core/rail_core/ops.py from bpHigh/RailSimEnv: direct link, hf CLI and curl.
- Browser
- Download file 6.75 kB
-
https://huggingface.co/spaces/bpHigh/RailSimEnv/resolve/main/core/rail_core/ops.py
- Command line
-
hf download hf://spaces/bpHigh/RailSimEnv/core/rail_core/ops.py
-
curl -L -o ops.py https://huggingface.co/spaces/bpHigh/RailSimEnv/resolve/main/core/rail_core/ops.py
6.75 kB
| """Disruptions and tiers: what can go wrong on the line, from the 2025 record, and how tasks are drawn. | |
| A disruption in the Rijden de Treinen record has a cause, a start, an end and the stations it was linked to. Its | |
| effect in the model depends on the cause (an assumption, stated in SOURCE.md): | |
| broken down train, collision, emergency call, defective track, person or animal on the track, power failure | |
| -> the sections between the outermost stations named are closed for the whole disruption | |
| signal failure, level crossing failure | |
| -> trains run on sight there: every section in the span takes `SLOW_EXTRA` minutes longer | |
| points failure | |
| -> one track out of use at the passing station(s) in the span (no crossing there if it had two) | |
| A disruption linked to every station of the line has no usable location; such causes are replayed on a section | |
| drawn with the task's seed. | |
| """ | |
| from __future__ import annotations | |
| import random | |
| from .model import Closure | |
| from .network import Network | |
| SLOW_EXTRA = 4 | |
| CLOSE = ("broken down", "collision", "emergency", "defective railway track", "person", "animal", "power failure", | |
| "damaged", "fire", "accident", "vandalism", "copper theft") | |
| SLOW = ("signal", "level crossing") | |
| POINTS = ("points",) | |
| LOOPS = ("HDG", "FWD", "BP", "ZH") | |
| def effect(cause: str) -> str: | |
| c = cause.lower() | |
| if any(k in c for k in POINTS) and not any(k in c for k in SLOW): | |
| return "track" | |
| if any(k in c for k in SLOW): | |
| return "slow" | |
| return "closed" | |
| def span(net: Network, codes: list[str]) -> list[str] | None: | |
| """[west, east] of the stations named, or None when the record names (nearly) the whole line.""" | |
| idx = sorted(net.index[c] for c in codes if c in net.index) | |
| if len(idx) < 2 or (idx[0] == 0 and idx[-1] == len(net.points) - 1 and len(idx) >= 8): | |
| return None | |
| return [net.points[idx[0]].code, net.points[idx[-1]].code] | |
| def closures_for(net: Network, cause: str, codes: list[str], start: int, end: int, rng: random.Random, | |
| real: bool = True) -> list[Closure]: | |
| kind = effect(cause) | |
| where = span(net, codes) | |
| if where is None: # no location in the record: draw one | |
| where = _draw_span(net, kind, rng) | |
| if kind == "track": | |
| lo, hi = net.index[where[0]], net.index[where[-1]] | |
| loops = [p.code for p in net.points[lo:hi + 1] if p.code in LOOPS] or [rng.choice(LOOPS)] | |
| return [Closure("track", [c], start, end, cause=cause, real=real) for c in loops[:1]] | |
| if kind == "slow": | |
| return [Closure("slow", where, start, end, extra=SLOW_EXTRA, cause=cause, real=real)] | |
| return [Closure("closed", where, start, end, cause=cause, real=real)] | |
| def _draw_span(net: Network, kind: str, rng: random.Random) -> list[str]: | |
| stations = [p.code for p in net.points if p.kind != "junction"] | |
| if kind == "track": | |
| c = rng.choice(LOOPS) | |
| return [c, c] | |
| i = rng.randrange(len(stations) - 1) | |
| j = min(len(stations) - 1, i + rng.choice((1, 1, 2))) | |
| return [stations[i], stations[j]] | |
| # ---------------------------------------------------------------- tiers and candidates | |
| import csv | |
| from datetime import date as _date | |
| from functools import lru_cache | |
| from .data import LINE_DIR, days as _days, disruptions as _real_disruptions | |
| from .model import hhmm | |
| # Eval days lie in these ISO weeks of 2025 (42 days); train days are every other day of January-June. | |
| HELD_OUT = {4, 7, 11, 17, 22, 25} | |
| PEAKS = [(6 * 60 + 30, 9 * 60), (15 * 60 + 30, 18 * 60 + 30)] | |
| LATE_CAUSES = ("a technical inspection", "a late driver", "a door fault being fixed", "a cleaning crew overrun", | |
| "coupling problems") | |
| TIERS = { | |
| # window minutes; what is seeded (each disruption's cause and duration drawn from the 2024-2025 record) | |
| "real": dict(window=(150, 210)), # the record's own disruption, on its own day | |
| "blockage": dict(window=(150, 210), closed=1, late=(0, 1)), | |
| "loop": dict(window=(150, 210), track=1, slow=(0, 1), late=(0, 1)), | |
| "compound": dict(window=(180, 240), closed=1, track=(0, 1), slow=(0, 1), late=(1, 3)), | |
| } | |
| def split_of(day: str) -> str: | |
| return "eval" if _date.fromisoformat(day).isocalendar()[1] in HELD_OUT else "train" | |
| def split_days(split: str) -> list[str]: | |
| return [d for d in _days() if split_of(d) == split] | |
| def profiles() -> list[dict]: | |
| with open(LINE_DIR / "disruption_profiles.csv", encoding="utf-8") as f: | |
| rows = [r for r in csv.DictReader(f) if r["duration_minutes"]] | |
| for r in rows: | |
| r["effect"] = effect(r["cause_en"]) | |
| r["minutes"] = max(10, min(240, round(float(r["duration_minutes"])))) | |
| return rows | |
| def real_disruptions(split: str | None = None) -> list[dict]: | |
| """The record's own disruptions on the line, as (day, start, end, cause, stations).""" | |
| out = [] | |
| for d in _real_disruptions(): | |
| day = d["start_time"][:10] | |
| if split and split_of(day) != split: | |
| continue | |
| hh, mm = int(d["start_time"][11:13]), int(d["start_time"][14:16]) | |
| start = hh * 60 + mm | |
| dur = round(float(d["duration_minutes"] or 0)) | |
| if dur < 15 or not 5 * 60 <= start <= 22 * 60: | |
| continue | |
| out.append({"day": day, "start": start, "end": start + dur, "cause": d["cause_en"], | |
| "stations": [c.strip() for c in d["rdt_station_codes"].split(",")]}) | |
| return out | |
| def draw(rng: random.Random, kind: str) -> dict: | |
| pool = [p for p in profiles() if p["effect"] == kind] or profiles() | |
| return rng.choice(pool) | |
| def seeded_closures(net: Network, tier: str, start: int, window: int, rng: random.Random) -> tuple[list, list[str]]: | |
| """Closures for a seeded tier, starting within the first half hour of the window.""" | |
| r = TIERS[tier] | |
| out: list[Closure] = [] | |
| def count(key): | |
| v = r.get(key, 0) | |
| return rng.randint(*v) if isinstance(v, tuple) else v | |
| for kind, key in (("closed", "closed"), ("track", "track"), ("slow", "slow")): | |
| for _ in range(count(key)): | |
| p = draw(rng, kind) | |
| t0 = start + rng.choice((0, 0, 5, 10, 20, 30)) | |
| minutes = min(p["minutes"], window - 20) if kind == "closed" else p["minutes"] | |
| codes = [c.strip() for c in p["rdt_station_codes"].split(",")] | |
| where = span(net, codes) or _draw_span(net, kind, rng) | |
| out += closures_for(net, p["cause_en"], where, t0, t0 + minutes, rng, real=False) | |
| notes = [] | |
| return out, notes | |
| def candidate_id(day: str, start: int, window: int, tier: str, seed: int) -> str: | |
| return f"gnlw-{day}-{hhmm(start).replace(':', '')}-{window}m-{tier}-{seed}" | |