Spaces:
Sleeping
Sleeping
Download core/rail_core/baselines.py from bpHigh/RailSimEnv: direct link, hf CLI and curl.
- Browser
- Download file 11.6 kB
-
https://huggingface.co/spaces/bpHigh/RailSimEnv/resolve/main/core/rail_core/baselines.py
- Command line
-
hf download hf://spaces/bpHigh/RailSimEnv/core/rail_core/baselines.py
-
curl -L -o baselines.py https://huggingface.co/spaces/bpHigh/RailSimEnv/resolve/main/core/rail_core/baselines.py
11.6 kB
| """Cheap dispatching policies: the lazy bounds a task must stay hard against, and the solver's starting point. | |
| naive change nothing: every train runs as scheduled and as early as it can (usually breaks the rules) | |
| greedy first come, first served: trains are planned one at a time in the order they are ready to leave; each | |
| takes the earliest times that clear every train already planned (holding at its scheduled stops), and | |
| a train whose path is closed for more than `turn_after` minutes is short-turned at the last turn point | |
| before the closure, its train starting back from there. This is how a dispatcher works by rule of | |
| thumb; it never cancels and never makes a train wait to let a later one through. | |
| greedy_fast the same, but fast trains (Sneltrein) are planned before stopping trains that are ready at the | |
| same time (within 10 minutes): priority to the fast service | |
| """ | |
| from __future__ import annotations | |
| import heapq | |
| from collections import defaultdict | |
| from .check import evaluate, network, run_dir | |
| from .model import Order, Plan, Run, Task | |
| MAXWAIT = 240 | |
| STEP_BUDGET = 60_000 # departure times tried per run before the greedy gives up on it | |
| class _Book: | |
| """What is already planned: section occupancies and station presences.""" | |
| def __init__(self, task: Task): | |
| self.task = task | |
| self.net = network(task) | |
| self.sec: dict[int, list] = defaultdict(list) # (dir, enter, exit) | |
| self.pres: dict[str, list] = defaultdict(list) # (dir, t0, t1) | |
| self.hf, self.ho = task.rule("h_follow"), task.rule("h_opp") | |
| self.closed = [c for c in task.closures if c.kind == "closed"] | |
| self.tracks_out = [c for c in task.closures if c.kind == "track"] | |
| self.slow = [c for c in task.closures if c.kind == "slow"] | |
| def section_ok(self, sec: int, d: int, t0: int, t1: int, history: bool = False) -> bool: | |
| s = self.net.sections[sec] | |
| for dd, a, b in self.sec[sec]: | |
| if dd == d: | |
| if (t0 >= a and (t0 - a < self.hf or t1 - b < self.hf)) or \ | |
| (t0 < a and (a - t0 < self.hf or b - t1 < self.hf)): | |
| return False | |
| elif s.single and not (b + self.ho <= t0 or t1 + self.ho <= a): | |
| return False | |
| if not history: | |
| for c in self.closed: | |
| lo, hi = sorted((self.net.index[c.where[0]], self.net.index[c.where[-1]])) | |
| if lo <= sec < hi and t0 < c.end and t1 > c.start: | |
| return False | |
| return True | |
| def station_ok(self, code: str, d: int, t0: int, t1: int) -> bool: | |
| p = self.net.by_code[code] | |
| if p.tracks is None or p.kind == "junction": | |
| return True | |
| others = [(dd, a, b) for dd, a, b in self.pres[code] if not (b < t0 or a > t1)] | |
| if p.directional: | |
| return not any(dd in (d, 0) for dd, _, _ in others) | |
| # peak count over [t0, t1] including this train | |
| times = sorted({t0} | {a for _, a, _ in others if t0 <= a <= t1} | | |
| {c.start for c in self.tracks_out if c.where[0] == code and t0 <= c.start <= t1}) | |
| for t in times: | |
| n = 1 + sum(1 for _, a, b in others if a <= t <= b) | |
| cap = p.tracks - sum(1 for c in self.tracks_out if c.where[0] == code and c.start <= t < c.end) | |
| if n > cap: | |
| return False | |
| return True | |
| def slow_extra(self, sec: int, t: int) -> int: | |
| x = 0 | |
| for c in self.slow: | |
| lo, hi = sorted((self.net.index[c.where[0]], self.net.index[c.where[-1]])) | |
| if lo <= sec < hi and c.start <= t < c.end: | |
| x = max(x, c.extra) | |
| return x | |
| def _closure_wait(book: _Book, r: Run, i: int, t: int) -> int: | |
| """Minutes a train entering route[i] -> route[i+1] at t would wait for a closure.""" | |
| sec = book.net.section(r.route[i], r.route[i + 1]) | |
| w = 0 | |
| for c in book.closed: | |
| lo, hi = sorted((book.net.index[c.where[0]], book.net.index[c.where[-1]])) | |
| if lo <= sec < hi and c.start <= t + r.seg[i] and t < c.end: | |
| w = max(w, c.end - t) | |
| return w | |
| def _path(book: _Book, r: Run, first: int, last: int, a0: int | None, ready: int, wait_from: int | None): | |
| """Earliest conflict-free times for one run over route[first..last] given what is booked: (arr, dep, stopped) | |
| or None. a0: the fixed arrival at `first` of a run already moving (else the run starts at `first`, where its | |
| train has stood since `wait_from` if that is a station with few tracks).""" | |
| net, task = book.net, book.task | |
| L = task.rule("stop_loss") | |
| d = run_dir(net, r) | |
| fail: set = set() | |
| steps = [0] | |
| arr: dict[int, int] = {} | |
| dep: dict[int, int] = {} | |
| stopped: dict[int, bool] = {} | |
| def go(i: int, a: int) -> bool: | |
| if i == last: | |
| arr[i], stopped[i] = a, True | |
| return True | |
| if (i, a) in fail: | |
| return False | |
| code = r.route[i] | |
| p = net.by_code[code] | |
| starting = i == first and a0 is None | |
| if starting: | |
| d0 = max(ready, r.dep[i]) | |
| elif r.stops[i]: | |
| d0 = max(a, r.dep[i]) | |
| else: | |
| d0 = a | |
| if i == first and a0 is not None: | |
| d0 = max(d0, task.start) | |
| sec = net.section(code, r.route[i + 1]) | |
| for t in range(d0, d0 + MAXWAIT): | |
| steps[0] += 1 | |
| if steps[0] > STEP_BUDGET: | |
| return False | |
| if starting: | |
| a_here, acc, halt, t0 = None, (0 if r.stops[i] else L), True, wait_from | |
| elif r.stops[i] or (i == first and a0 is not None and t > a and False): | |
| a_here, acc, halt, t0 = a, 0, True, a | |
| elif t == a: | |
| a_here, acc, halt, t0 = a, 0, False, a | |
| else: | |
| break # greedy only waits where the train has a scheduled stop | |
| if t0 is not None and not book.station_ok(code, d, t0, t): | |
| if halt: | |
| break # a longer wait here only makes it worse | |
| continue | |
| nxt = t + r.seg[i] + acc + book.slow_extra(sec, t) | |
| if r.arr[i + 1] is not None: | |
| nxt = max(nxt, r.arr[i + 1]) | |
| if i + 1 == last and not r.stops[last]: | |
| nxt += L | |
| if not book.section_ok(sec, d, t, nxt): | |
| continue | |
| if a_here is not None: | |
| arr[i] = a_here | |
| dep[i], stopped[i] = t, halt | |
| if go(i + 1, nxt): | |
| return True | |
| fail.add((i, a)) | |
| return False | |
| if not go(first, a0 if a0 is not None else 0): | |
| return None | |
| return arr, dep, stopped | |
| def greedy(task: Task, fast_first: bool = False, turn_after: int = 20) -> Plan: | |
| book = _Book(task) | |
| net = book.net | |
| runs = task.run_map | |
| turn = task.rule("turn_min") | |
| plan: Plan = {} | |
| for r in task.runs: # trains between points when the task starts | |
| if r.started and r.fixed_from is not None: | |
| k = r.fixed_to | |
| book.sec[net.section(r.route[k - 1], r.route[k])].append((run_dir(net, r), r.fixed_from, r.fixed_arr)) | |
| state = {} | |
| for u in task.units: | |
| finite = u.at is not None and net.by_code[u.at].tracks is not None | |
| state[u.id] = {"k": 0, "ready": u.ready, "wait_from": task.start if finite else None, "start_at": None, | |
| "park": None} | |
| if finite: | |
| stay = (0, task.start, task.end + 1440) | |
| book.pres[u.at].append(stay) | |
| state[u.id]["park"] = (u.at, stay) | |
| heap = [] | |
| units = {u.id: u for u in task.units} | |
| def push(u): | |
| s = state[u.id] | |
| if s["k"] >= len(u.runs): | |
| return | |
| r = runs[u.runs[s["k"]]] | |
| t = r.fixed_arr if r.started else max(s["ready"] or 0, r.dep[0]) | |
| pri = (t // 10, 0 if r.kind == "fast" else 1, t) if fast_first else (t, 0, 0) | |
| heapq.heappush(heap, (pri, u.id)) | |
| for u in task.units: | |
| push(u) | |
| while heap: | |
| _, uid = heapq.heappop(heap) | |
| u = units[uid] | |
| s = state[uid] | |
| r = runs[u.runs[s["k"]]] | |
| if s["park"]: # its provisional stay is replaced by the real one below | |
| code, stay = s["park"] | |
| book.pres[code].remove(stay) | |
| s["park"] = None | |
| has_next = s["k"] + 1 < len(u.runs) | |
| first = r.fixed_to if r.started else (r.idx(s["start_at"]) if s["start_at"] else 0) | |
| last = len(r.route) - 1 | |
| if has_next: # short-turn before a long closure, if its train has a run to come back on | |
| nxt = runs[u.runs[s["k"] + 1]] | |
| t = r.fixed_arr if r.started else max(s["ready"] or 0, r.dep[first]) | |
| for i in range(first, last): | |
| if i > first and r.stops[i]: | |
| t = max(t, r.dep[i]) | |
| if _closure_wait(book, r, i, t) > turn_after: | |
| lo = first if r.started else first + 1 | |
| # only where the next run can start (some runs work part of the line, e.g. Groningen - Zuidhorn) | |
| cands = [j for j in range(lo, i + 1) if net.by_code[r.route[j]].turn | |
| and r.route[j] in nxt.route[:-1]] | |
| if cands: | |
| last = cands[-1] | |
| break | |
| t += r.seg[i] | |
| a0 = r.fixed_arr if r.started else None | |
| wait_from = None if r.started else s["wait_from"] | |
| res = _path(book, r, first, last, a0, s["ready"] or 0, wait_from) | |
| if res is None and last != len(r.route) - 1: | |
| last = len(r.route) - 1 | |
| res = _path(book, r, first, last, a0, s["ready"] or 0, wait_from) | |
| if res is None: | |
| plan[r.id] = Order(start=r.route[first] if (not r.started and first > 0) else None) | |
| s["k"] += 1 | |
| s["ready"], s["wait_from"], s["start_at"] = None, None, None | |
| push(u) | |
| continue | |
| arr, dep, stopped = res | |
| d = run_dir(net, r) | |
| if wait_from is not None: | |
| book.pres[r.route[first]].append((d, wait_from, dep[first])) | |
| for i in range(first, last): | |
| book.sec[net.section(r.route[i], r.route[i + 1])].append((d, dep[i], arr[i + 1])) | |
| if (i > first or r.started) and net.by_code[r.route[i]].tracks is not None: | |
| book.pres[r.route[i]].append((d, arr[i], dep[i])) | |
| plan[r.id] = Order(start=r.route[first] if (not r.started and first > 0) else None, | |
| end=r.route[last] if last < len(r.route) - 1 else None, | |
| hold={r.route[i]: dep[i] for i in range(first, last) | |
| if net.by_code[r.route[i]].kind != "junction"}) | |
| end_code = r.route[last] | |
| s["k"] += 1 | |
| s["ready"] = arr[last] + turn | |
| s["start_at"] = end_code if last < len(r.route) - 1 else None | |
| p_end = net.by_code[end_code] | |
| if p_end.tracks is not None and p_end.kind != "junction": | |
| s["wait_from"] = arr[last] | |
| stay = (0, arr[last], task.end + 1440) | |
| book.pres[end_code].append(stay) | |
| s["park"] = (end_code, stay) | |
| else: | |
| s["wait_from"] = None | |
| push(u) | |
| from .solve import _tidy | |
| return _tidy(task, plan) | |
| def naive(task: Task) -> Plan: | |
| return {} | |
| POLICIES = {"naive": naive, "greedy": greedy, "greedy_fast": lambda t: greedy(t, fast_first=True)} | |