Headway
1 Codebase

instances.py

delivery/instances.py · 140 lines · Download

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"})