PortSimEnv / core /berth_core /build.py
AdithyaSK's picture
AdithyaSK HF Staff
Upload folder using huggingface_hub
a1e3907 verified
Raw History Blame Contribute Delete
4.89 kB
"""Build the task pack.
python -m berth_core.build --out ../tasks/berth-v1 --target 100
Every (quay, ISO week 2-51, difficulty) with seed 0 is a candidate; seeds 1, 2, ... are added until enough pass the
filter. Levels: easy, medium, hard (closures, late ships, crane overruns, extra calls) and expert (the other quay's
traffic diverted here on top). A candidate is kept when the CP-SAT optimum is proven and the naive re-plan leaves room to improve (naive cost
at least MIN_RATIO x optimum and MIN_GAP above it). Weeks divisible by 5 form the test split. Selection is
deterministic: the same data and arguments rebuild the same pack.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import time
from concurrent.futures import ProcessPoolExecutor, as_completed
from pathlib import Path
from .data import QUAYS, load_calls
from .generate import build_task
MIN_RATIO = 1.15
MIN_GAP = 15
TEST_WEEK_MOD = 5
TEST_SHARE = 0.2
_CALLS = None
def _one(args):
global _CALLS
if _CALLS is None:
_CALLS = load_calls()
quay, week, seed, difficulty, time_limit = args
split = "test" if week % TEST_WEEK_MOD == 0 else "train"
t = time.time()
task, stats = build_task(_CALLS, quay, week, seed, difficulty, split, time_limit=time_limit, workers=2)
stats.update(quay=quay, week=week, seed=seed, difficulty=difficulty, seconds=round(time.time() - t, 1))
if task is not None:
naive, opt = stats["naive"], stats["optimal"]
if naive < MIN_RATIO * opt or naive - opt < MIN_GAP:
stats["reason"] = f"naive {naive} too close to optimum {opt}"
task = None
return (task.to_dict() if task else None), stats
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--out", required=True)
ap.add_argument("--target", type=int, default=100)
ap.add_argument("--max-seeds", type=int, default=3)
ap.add_argument("--time-limit", type=float, default=60.0)
ap.add_argument("--procs", type=int, default=4)
ap.add_argument("--levels", default="easy,medium,hard,expert")
args = ap.parse_args()
out = Path(args.out)
out.mkdir(parents=True, exist_ok=True)
kept: dict[str, dict] = {}
log = []
levels = args.levels.split(",")
per_difficulty = {d: args.target // len(levels) + (1 if i < args.target % len(levels) else 0) for i, d in enumerate(levels)}
for seed in range(args.max_seeds):
need = {d: per_difficulty[d] - sum(1 for t in kept.values() if t["difficulty"] == d) for d in per_difficulty}
if all(v <= 0 for v in need.values()):
break
jobs = [(q, w, seed, d, args.time_limit) for q in QUAYS for w in range(2, 52) for d in per_difficulty if need[d] > 0]
with ProcessPoolExecutor(args.procs) as ex:
futs = [ex.submit(_one, j) for j in jobs]
for i, f in enumerate(as_completed(futs), 1):
task, stats = f.result()
log.append(stats)
if task:
kept[task["task_id"]] = task
if i % 25 == 0:
print(f"seed {seed}: {i}/{len(jobs)} candidates, {len(kept)} kept", flush=True)
# Deterministic selection spread over the year: within each level, test and train pools ordered by a hash of
# the task id; TEST_SHARE of each level comes from the held-out weeks.
chosen = []
key = lambda t: hashlib.sha1(t["task_id"].encode()).hexdigest()
for d, n in per_difficulty.items():
pool = [t for t in kept.values() if t["difficulty"] == d]
test = sorted((t for t in pool if t["split"] == "test"), key=key)
train = sorted((t for t in pool if t["split"] == "train"), key=key)
n_test = min(len(test), round(n * TEST_SHARE))
chosen += test[:n_test] + train[: n - n_test]
chosen.sort(key=lambda t: (t["split"], t["quay"], t["week"], t["difficulty"]))
body = "".join(json.dumps(t, ensure_ascii=False, separators=(",", ":")) + "\n" for t in chosen).encode()
(out / "tasks.jsonl").write_bytes(body)
splits = {}
for t in chosen:
splits.setdefault(t["split"], {}).setdefault(t["difficulty"], 0)
splits[t["split"]][t["difficulty"]] += 1
manifest = {
"name": out.name, "tasks": len(chosen), "splits": splits, "sha256": hashlib.sha256(body).hexdigest(),
"source": "Port of Barcelona open data, 2024 container calls at quays 36A and 24B (CC BY-SA 4.0)",
"filter": {"min_ratio": MIN_RATIO, "min_gap": MIN_GAP, "test_weeks": f"ISO week % {TEST_WEEK_MOD} == 0"},
"candidates": len(log), "kept_before_selection": len(kept),
}
(out / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n")
(out / "build_log.jsonl").write_text("".join(json.dumps(s) + "\n" for s in log))
print(json.dumps(manifest, indent=2))
if __name__ == "__main__":
main()