Spaces:
Running
Running
Download core/berth_core/build.py from FineEnvs/PortSimEnv: direct link, hf CLI and curl.
- Browser
- Download file 4.89 kB
-
https://huggingface.co/spaces/FineEnvs/PortSimEnv/resolve/main/core/berth_core/build.py
- Command line
-
hf download hf://spaces/FineEnvs/PortSimEnv/core/berth_core/build.py
-
curl -L -o build.py https://huggingface.co/spaces/FineEnvs/PortSimEnv/resolve/main/core/berth_core/build.py
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() | |