1"""Generated data: seeded order / van generator and instance (de)serialisation.2 3Real vs generated: travel times come from real taxi trips; everything here (orders,4time windows, demands, service times, van capacities and shifts) is synthetic and fully5determined by the seed. See docs/data_dictionary.md for every field.6"""7from __future__ import annotations8 9import json10import math11from dataclasses import asdict, dataclass, field12from pathlib import Path13 14import numpy as np15 16DEFAULT_SHIFTS = [(8 * 60, 17 * 60), (9 * 60, 18 * 60), (10 * 60, 19 * 60)]17WINDOW_WIDTHS = (30, 45, 60, 90, 120)18 19 20@dataclass21class Order:22 id: int23 zone: int # zone index 0..Z-124 earliest: float # minutes after midnight25 latest: float26 demand: int # load units27 service: float # minutes spent at the stop28 29 30@dataclass31class Van:32 id: int33 capacity: int34 shift_start: float35 shift_end: float36 max_stops: int = 1037 38 39@dataclass40class Instance:41 orders: list42 vans: list43 depot: int44 weekday: int45 name: str = "instance"46 seed: int = 047 return_to_depot: bool = True48 meta: dict = field(default_factory=dict)49 50 def order(self, oid: int) -> Order:51 return self._by_id[oid]52 53 def __post_init__(self):54 self._by_id = {o.id: o for o in self.orders}55 56 # ---- io57 def to_dict(self):58 return {"name": self.name, "seed": self.seed, "depot": self.depot, "weekday": self.weekday,59 "return_to_depot": self.return_to_depot, "meta": self.meta,60 "orders": [asdict(o) for o in self.orders], "vans": [asdict(v) for v in self.vans]}61 62 @classmethod63 def from_dict(cls, d):64 return cls([Order(**o) for o in d["orders"]], [Van(**v) for v in d["vans"]],65 d["depot"], d["weekday"], d.get("name", "instance"), d.get("seed", 0),66 d.get("return_to_depot", True), d.get("meta", {}))67 68 def save(self, path: Path):69 Path(path).write_text(json.dumps(self.to_dict(), indent=1))70 71 @classmethod72 def load(cls, path: Path):73 return cls.from_dict(json.loads(Path(path).read_text()))74 75 def widened(self, minutes: float, only=None) -> "Instance":76 """Copy with `latest` pushed back by `minutes` (all orders, or only ids in `only`)."""77 d = self.to_dict()78 for o in d["orders"]:79 if only is None or o["id"] in only:80 o["latest"] += minutes81 d["name"] = f"{self.name}+{int(minutes)}min"82 return Instance.from_dict(d)83 84 85def _r15(x):86 return 15 * round(x / 15)87 88 89def generate_instance(n_orders: int, seed: int, n_zones: int = 20, n_vans: int = 3,90 zone_weights=None, depot: int | None = None, weekday: int | None = None,91 cap_slack: float = 1.25, max_stops: int = 10,92 day=(9 * 60, 17 * 60), widths=WINDOW_WIDTHS,93 shifts=DEFAULT_SHIFTS, return_to_depot: bool = True) -> Instance:94 """Dispatcher-style instance: n orders with windows inside `day`, n_vans vans."""95 rng = np.random.default_rng(seed)96 w = None if zone_weights is None else np.asarray(zone_weights, float) / np.sum(zone_weights)97 depot = int(rng.integers(n_zones)) if depot is None else depot98 weekday = int(rng.integers(5)) if weekday is None else weekday99 orders = []100 for k in range(n_orders):101 width = float(rng.choice(widths))102 earliest = float(_r15(rng.uniform(day[0], day[1] - width)))103 orders.append(Order(104 id=k, zone=int(rng.choice(n_zones, p=w)),105 earliest=earliest, latest=earliest + width,106 demand=int(rng.choice([1, 2, 3], p=[0.5, 0.3, 0.2])),107 service=float(rng.choice([5, 7, 10]))))108 total = sum(o.demand for o in orders)109 cap = max(3, math.ceil(total * cap_slack / n_vans))110 vans = [Van(v, cap, *shifts[v % len(shifts)], max_stops=max_stops) for v in range(n_vans)]111 return Instance(orders, vans, depot, weekday, f"n{n_orders}_s{seed}", seed, return_to_depot,112 {"generator": "generate_instance", "cap_slack": cap_slack})113 114 115def generate_route_instance(n_stops: int, seed: int, tm, n_zones: int = 20, start: float = 8 * 60,116 return_to_depot: bool = True) -> Instance:117 """Single-van instance that is feasible by construction (for the search experiment).118 119 A hidden random tour is simulated with the time model; each window is placed so that120 the hidden tour's arrival lies inside it. The optimal tour is usually a different one.121 """122 rng = np.random.default_rng(seed)123 depot = int(rng.integers(n_zones))124 zones = rng.choice(n_zones, size=n_stops, replace=True)125 t, cur, orders = start, depot, []126 for k in range(n_stops):127 z = int(zones[k])128 t += tm.tt(cur, z, t) + float(rng.uniform(0, 25)) # idle gap spreads windows out129 width = float(rng.choice([60, 90, 120]))130 earliest = t - float(rng.uniform(0, width))131 service = float(rng.choice([5, 7, 10]))132 orders.append(Order(k, z, round(earliest, 1), round(earliest + width, 1), 1, service))133 t, cur = t + service, z134 order_perm = rng.permutation(n_stops) # hide the construction order135 orders = [orders[i] for i in order_perm]136 for new_id, o in enumerate(orders):137 o.id = new_id138 van = Van(0, n_stops, start, 23 * 60, max_stops=n_stops)139 return Instance(orders, [van], depot, 1, f"route{n_stops}_s{seed}", seed, return_to_depot,140 {"generator": "generate_route_instance"})