PortSimEnv / core /berth_core /solve.py
AdithyaSK's picture
AdithyaSK HF Staff
Upload folder using huggingface_hub
a1e3907 verified
Raw History Blame Contribute Delete
9.27 kB
"""Reference plans: the naive re-plan a hurried planner would make, and the CP-SAT optimum.
Both handle the crane rules when a task has them (crane count per ship, crane pool with outages, movements per hour).
"""
from __future__ import annotations
from .model import MOVE_PENALTY, Plan, Task
class _Ledger:
"""Occupied rectangles, cranes in use per hour and movements per hour, for greedy placement."""
def __init__(self, task: Task):
self.task = task
self.rects = [(b.start, b.end, b.first, b.last) for b in task.blocks]
self.use: dict[int, int] = {}
self.moves: dict[int, int] = {}
for b in task.blocks:
if b.kind == "alongside":
for t in range(b.start, b.end):
self.use[t] = self.use.get(t, 0) + b.cranes
self.moves[b.end] = self.moves.get(b.end, 0) + 1
def _times(self, s, h: int, cranes: int | None) -> tuple[int, int]:
work = s.handling_for(cranes)
dep = self.task.departure(s, h + work) if self.task.cranes else h + work
return h + work, dep
def fits(self, s, h: int, sec: int, cranes: int | None) -> bool:
finish, dep = self._times(s, h, cranes)
last = sec + s.sections - 1
if any(h < r[1] and r[0] < dep and sec <= r[3] and r[2] <= last for r in self.rects):
return False
if self.task.cranes:
if any(a <= h < b for a, b in self.task.no_move_windows(s)):
return False
if any(self.use.get(t, 0) + cranes > self.task.crane_pool_at(t) for t in range(h, finish)):
return False
cap = self.task.rules.get("max_moves_per_hour")
if cap and (self.moves.get(h, 0) + 1 > cap or self.moves.get(dep, 0) + 1 > cap):
return False
return True
def add(self, s, h: int, sec: int, cranes: int | None) -> None:
finish, dep = self._times(s, h, cranes)
self.rects.append((h, dep, sec, sec + s.sections - 1))
if self.task.cranes:
for t in range(h, finish):
self.use[t] = self.use.get(t, 0) + cranes
self.moves[h] = self.moves.get(h, 0) + 1
self.moves[dep] = self.moves.get(dep, 0) + 1
def naive_replan(task: Task) -> Plan:
"""Keep each ship's published slot if it still works; otherwise give it the earliest hour, then the section
closest to its planned one, that is free. Ships are taken in published order (unscheduled calls by arrival).
On crane-rule tasks every ship keeps its standard crane count."""
led = _Ledger(task)
plan: Plan = {}
order = sorted(task.ships, key=lambda s: (s.planned_hour if s.planned_hour is not None else s.arrival, s.id))
for s in order:
cranes = s.std_cranes if task.cranes else None
anchor = s.planned_section if s.planned_section is not None else task.first_section
h = max(s.arrival, s.planned_hour if s.planned_hour is not None else s.arrival)
positions = sorted(range(task.first_section, task.last_section - s.sections + 2), key=lambda k: (abs(k - anchor), k))
while True:
sec = next((k for k in positions if led.fits(s, h, k, cranes)), None)
if sec is not None:
break
h += 1
led.add(s, h, sec, cranes)
plan[s.id] = (h, sec) + ((cranes,) if task.cranes else ())
return plan
def optimal_plan(task: Task, time_limit: float = 60.0, workers: int = 8, hint: Plan | None = None):
"""Minimum-cost plan by CP-SAT (time x quay rectangles with NoOverlap2D; on crane-rule tasks one optional
interval per crane count, a cumulative crane pool and a cumulative movement limit). Returns
(plan, cost, bound, status)."""
from ortools.sat.python import cp_model
horizon = max(s.arrival for s in task.ships) + sum(s.handling_for(s.min_cranes if task.cranes else None)
for s in task.ships) + max([b.end for b in task.blocks] + [0])
md = cp_model.CpModel()
xs, ys, terms, T, M, C = [], [], [], {}, {}, {}
crane_iv, crane_dem, move_iv = [], [], []
for s in task.ships:
t = md.NewIntVar(s.arrival, horizon, f"t{s.id}")
m = md.NewIntVar(task.first_section, task.last_section - s.sections + 1, f"m{s.id}")
end = md.NewIntVar(s.arrival, horizon * 2, f"e{s.id}")
pres = {}
if not task.cranes:
stay = s.handling
xs.append(md.NewIntervalVar(t, stay, t + stay, f"x{s.id}"))
ys.append(md.NewIntervalVar(m, s.sections, m + s.sections, f"y{s.id}"))
md.Add(end == t + stay)
else:
finish = md.NewIntVar(s.arrival, horizon * 2, f"f{s.id}")
for k in range(s.min_cranes, s.max_cranes + 1):
p = md.NewBoolVar(f"p{s.id}_{k}")
pres[k] = p
work = s.handling_for(k)
md.Add(finish == t + work).OnlyEnforceIf(p)
crane_iv.append(md.NewOptionalIntervalVar(t, work, t + work, p, f"c{s.id}_{k}"))
crane_dem.append(k)
md.AddExactlyOne(pres.values())
# departure: the finish hour, or the end of a no-movement window the finish falls in (wait alongside)
inside = []
for a, b in task.no_move_windows(s):
lt, ge = md.NewBoolVar(""), md.NewBoolVar("") # berth hour outside [a, b)
md.Add(t <= a - 1).OnlyEnforceIf(lt)
md.Add(t >= b).OnlyEnforceIf(ge)
md.AddBoolOr([lt, ge])
w = md.NewBoolVar(f"w{s.id}_{a}")
md.Add(finish >= a).OnlyEnforceIf(w)
md.Add(finish <= b - 1).OnlyEnforceIf(w)
below, above = md.NewBoolVar(""), md.NewBoolVar("")
md.Add(finish <= a - 1).OnlyEnforceIf(below)
md.Add(finish >= b).OnlyEnforceIf(above)
md.AddBoolOr([w, below, above])
md.Add(end == b).OnlyEnforceIf(w)
inside.append(w)
if inside:
none = md.NewBoolVar(f"n{s.id}")
md.AddBoolAnd([x.Not() for x in inside]).OnlyEnforceIf(none)
md.AddBoolOr(inside + [none])
md.Add(end == finish).OnlyEnforceIf(none)
else:
md.Add(end == finish)
size = md.NewIntVar(1, horizon * 2, f"sz{s.id}")
md.Add(size == end - t)
xs.append(md.NewIntervalVar(t, size, end, f"x{s.id}"))
ys.append(md.NewIntervalVar(m, s.sections, m + s.sections, f"y{s.id}"))
move_iv.append(md.NewFixedSizeIntervalVar(t, 1, f"mb{s.id}"))
move_iv.append(md.NewFixedSizeIntervalVar(end, 1, f"md{s.id}"))
late = md.NewIntVar(0, horizon * 2, f"late{s.id}")
md.Add(late >= end - s.due)
terms.append(s.sections * s.weight * late)
if s.berth_deadline is not None and s.deadline_penalty:
over = md.NewIntVar(0, horizon * 2, f"over{s.id}")
md.Add(over >= t - s.berth_deadline)
terms.append(s.deadline_penalty * over)
if s.planned_section is not None:
moved = md.NewBoolVar(f"moved{s.id}")
md.Add(m == s.planned_section).OnlyEnforceIf(moved.Not())
terms.append(MOVE_PENALTY * moved)
T[s.id], M[s.id], C[s.id] = t, m, pres
for k, b in enumerate(task.blocks):
bx = md.NewFixedSizeIntervalVar(b.start, b.end - b.start, f"bx{k}")
xs.append(bx)
ys.append(md.NewFixedSizeIntervalVar(b.first, b.last - b.first + 1, f"by{k}"))
if task.cranes and b.kind == "alongside":
if b.cranes:
crane_iv.append(bx)
crane_dem.append(b.cranes)
move_iv.append(md.NewFixedSizeIntervalVar(b.end, 1, f"bm{k}"))
md.AddNoOverlap2D(xs, ys)
if task.cranes:
for k, o in enumerate(task.rules.get("crane_outages", [])):
crane_iv.append(md.NewFixedSizeIntervalVar(o["start"], o["end"] - o["start"], f"out{k}"))
crane_dem.append(int(o["cranes"]))
md.AddCumulative(crane_iv, crane_dem, int(task.rules["crane_pool"]))
cap = task.rules.get("max_moves_per_hour")
if cap:
md.AddCumulative(move_iv, [1] * len(move_iv), int(cap))
md.Minimize(sum(terms))
if hint:
for sid, entry in hint.items():
md.AddHint(T[sid], entry[0])
md.AddHint(M[sid], entry[1])
if task.cranes and len(entry) > 2:
for k, p in C[sid].items():
md.AddHint(p, int(k == entry[2]))
solver = cp_model.CpSolver()
solver.parameters.max_time_in_seconds = time_limit
solver.parameters.num_workers = workers
status = solver.Solve(md)
name = solver.StatusName(status)
if name not in ("OPTIMAL", "FEASIBLE"):
return None, None, None, name
plan: Plan = {}
for s in task.ships:
entry = (solver.Value(T[s.id]), solver.Value(M[s.id]))
if task.cranes:
entry += (next(k for k, p in C[s.id].items() if solver.Value(p)),)
plan[s.id] = entry
return plan, int(round(solver.ObjectiveValue())), int(round(solver.BestObjectiveBound())), name