RailSimEnv / core /rail_core /baselines.py
bpHigh's picture
RailSimEnv v1: OpenEnv server, eval explorer, 3D replay and train graph
31252db verified
Raw History Blame Contribute Delete
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)}