Spaces:
Running
Running
File size: 8,290 Bytes
a1e3907 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | """Feasibility and cost of a plan. Pure Python: the server grades with this and needs no solver."""
from __future__ import annotations
from dataclasses import dataclass, field
from .model import MOVE_PENALTY, Plan, Task
@dataclass
class ShipResult:
ship: int
berth_hour: int | None
section: int | None
departure: int | None
delay_h: int
moved: bool
cost: int
problems: list[str] = field(default_factory=list)
cranes: int | None = None
@dataclass
class PlanResult:
feasible: bool
ships: list[ShipResult]
cost: int | None # None when the plan is infeasible
delay_cost: int | None
moves: int | None
@property
def clean_fraction(self) -> float:
return sum(1 for s in self.ships if not s.problems) / max(1, len(self.ships))
@property
def violations(self) -> list[dict]:
return [{"ship": s.ship, "problem": p} for s in self.ships for p in s.problems]
def _span(a0: int, a1: int) -> str:
return f"{a0}" if a0 == a1 else f"{a0}-{a1}"
def _hours(hs: list[int]) -> str:
"""[3, 4, 5, 9] -> '3-5, 9'"""
out, run = [], []
for h in sorted(set(hs)):
if run and h == run[-1] + 1:
run.append(h)
else:
if run:
out.append(_span(run[0], run[-1]))
run = [h]
if run:
out.append(_span(run[0], run[-1]))
return ", ".join(out[:4]) + (" …" if len(out) > 4 else "")
def evaluate(task: Task, plan: Plan) -> PlanResult:
"""Check every ship of `plan` against the task's rules and price it.
Rules: berth at or after arrival; stay within the quay; never share a section-hour with another ship, a ship
already alongside, or a closed section. On crane-rule tasks also: cranes within the ship's min-max, cranes in use
at every hour within the pool (less any outage), and at most max_moves_per_hour berthings + departures per hour.
Cost: sum over ships of sections x hours late against the planned departure, plus MOVE_PENALTY for each
scheduled ship placed on different sections than planned."""
lo, hi = task.first_section, task.last_section
out: list[ShipResult] = []
rects: dict[int, tuple[int, int, int, int]] = {}
for s in task.ships:
if s.id not in plan:
out.append(ShipResult(s.id, None, None, None, 0, False, 0, ["missing from the plan"]))
continue
entry = plan[s.id]
h, sec = entry[0], entry[1]
c = entry[2] if task.cranes and len(entry) > 2 else (s.std_cranes if task.cranes else None)
bad_cranes = task.cranes and not (s.min_cranes <= c <= s.max_cranes)
work = s.handling_for(s.std_cranes if bad_cranes else c) # never divide by a zero or negative count
dep = task.departure(s, h + work) if task.cranes else h + work
stay = dep - h
r = ShipResult(s.id, h, sec, dep, max(0, dep - s.due),
s.planned_section is not None and sec != s.planned_section, 0, cranes=c)
if task.cranes:
for a, b in task.no_move_windows(s):
if a <= h < b:
r.problems.append(f"berths at hour {h}, inside the no-movement window {a}-{b}")
if bad_cranes:
r.problems.append(f"gets {c} cranes but can be worked by {s.min_cranes}-{s.max_cranes}")
if h < s.arrival:
r.problems.append(f"berths at hour {h} but cannot arrive before hour {s.arrival}")
last = sec + s.sections - 1
if sec < lo or last > hi:
r.problems.append(f"needs sections {_span(sec, last)} but the quay has sections {lo}-{hi}")
for b in task.blocks:
if sec <= b.last and b.first <= last and h < b.end and b.start < h + stay:
what = f"{b.label} (alongside)" if b.kind == "alongside" else "closed sections"
r.problems.append(f"overlaps {what} {_span(b.first, b.last)} during hours {b.start}-{b.end}")
rects[s.id] = (h, h + stay, sec, last, h + work)
out.append(r)
ids = sorted(rects)
for i, a in enumerate(ids):
t0, t1, m0, m1 = rects[a][:4]
for b in ids[i + 1:]:
u0, u1, n0, n1 = rects[b][:4]
if t0 < u1 and u0 < t1 and m0 <= n1 and n0 <= m1:
hrs, secs = f"{max(t0, u0)}-{min(t1, u1)}", _span(max(m0, n0), min(m1, n1))
out[a].problems.append(f"overlaps ship {b} ({task.ships[b].name}) in sections {secs} during hours {hrs}")
out[b].problems.append(f"overlaps ship {a} ({task.ships[a].name}) in sections {secs} during hours {hrs}")
if task.cranes:
_crane_and_move_limits(task, out, rects)
feasible = not any(r.problems for r in out)
for r in out:
if r.berth_hour is not None:
r.cost = ship_delay_cost(task.ships[r.ship], r.berth_hour, r.delay_h) + (MOVE_PENALTY if r.moved else 0)
if not feasible:
return PlanResult(False, out, None, None, None)
delay = sum(ship_delay_cost(task.ships[r.ship], r.berth_hour, r.delay_h) for r in out)
moves = sum(r.moved for r in out)
return PlanResult(True, out, delay + MOVE_PENALTY * moves, delay, moves)
def ship_delay_cost(s, berth_hour: int, delay_h: int) -> int:
"""sections x hours late x weight, plus the emergency penalty for berthing after a genuine deadline."""
cost = s.sections * delay_h * s.weight
if s.berth_deadline is not None and berth_hour > s.berth_deadline:
cost += s.deadline_penalty * (berth_hour - s.berth_deadline)
return cost
def _crane_and_move_limits(task: Task, out: list[ShipResult], rects: dict) -> None:
"""Crane pool and movement limit, hour by hour; a ship involved in an over-limit hour gets one problem line."""
use: dict[int, int] = {}
moves: dict[int, list[int]] = {}
fixed_moves: dict[int, int] = {}
for b in task.blocks:
if b.kind == "alongside":
for t in range(b.start, b.end):
use[t] = use.get(t, 0) + b.cranes
fixed_moves[b.end] = fixed_moves.get(b.end, 0) + 1
for sid, (t0, t1, _, _, tw) in rects.items():
c = out[sid].cranes or 0
for t in range(t0, tw): # cranes work until the ship finishes, not while it waits for the wind
use[t] = use.get(t, 0) + c
moves.setdefault(t0, []).append(sid)
moves.setdefault(t1, []).append(sid)
over = {t for t, u in use.items() if u > task.crane_pool_at(t)}
if over:
for sid, (t0, t1, _, _, tw) in rects.items():
hs = [t for t in range(t0, tw) if t in over]
if hs:
worst = max(hs, key=lambda t: use[t] - task.crane_pool_at(t))
out[sid].problems.append(
f"cranes over the pool at hours {_hours(hs)} (e.g. hour {worst}: {use[worst]} in use, "
f"{task.crane_pool_at(worst)} available)")
cap = task.rules.get("max_moves_per_hour")
if cap:
for t, ids in sorted(moves.items()):
n = len(ids) + fixed_moves.get(t, 0)
if n > cap:
for sid in ids:
kind = "berths" if rects[sid][0] == t else "leaves"
out[sid].problems.append(f"{kind} at hour {t}, when {n} ships move (limit {cap} per hour)")
def plan_cost(task: Task, plan: Plan) -> int:
res = evaluate(task, plan)
if not res.feasible:
raise ValueError("plan is infeasible: " + "; ".join(v["problem"] for v in res.violations[:3]))
return res.cost
def unavoidable_cost(task: Task) -> int:
"""A lower bound on any plan's cost: each ship on its own, docking at its earliest legal hour (its arrival, or the
end of a no-movement window it arrives in) with the most cranes it can take. No plan can do better for that ship,
so the sum is a floor under the optimum; the reward measures mistakes against the cost above this floor."""
total = 0
for s in task.ships:
h = s.arrival
if task.cranes:
for a, b in task.no_move_windows(s):
if a <= h < b:
h = b
dep = task.departure(s, h + s.handling_for(s.max_cranes))
else:
dep = h + s.handling
total += ship_delay_cost(s, h, max(0, dep - s.due))
return total
|