Spaces:
Sleeping
Sleeping
| """Parse a request dict + product/van catalogues into solver inputs. | |
| The shape of the request dict matches `sample_request.json`. The catalogues | |
| (`products.json`, `vans.json`) are read from disk lazily — keeps startup | |
| cheap and avoids globals. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| _BASE = Path(__file__).parent | |
| DEFAULT_PRODUCTS = _BASE / "products.json" | |
| DEFAULT_VANS = _BASE / "vans.json" | |
| SERVICE_BASE_S = 180.0 # parking + paperwork buffer per stop | |
| SERVICE_PER_CELL_S = 10.0 # crate-handling time per unit cell | |
| class Depot: | |
| id: str | |
| lat: float | |
| lng: float | |
| open_s: int | |
| close_s: int | |
| class Driver: | |
| id: str | |
| shift_start_s: int | |
| shift_end_s: int | |
| class Fleet: | |
| num_vans: int | |
| capacity_kg: float | |
| capacity_cells: int | |
| class Stop: | |
| id: str | |
| lat: float | |
| lng: float | |
| t_open_s: int | |
| t_close_s: int | |
| delivery_cells: int | |
| delivery_kg: float | |
| pickup_cells: int | |
| pickup_kg: float | |
| service_time_s: float | |
| def _hms(s: str) -> int: | |
| h, m = s.split(":") | |
| return int(h) * 3600 + int(m) * 60 | |
| def _van_capacity_cells(spec: dict) -> int: | |
| cube = spec["cube_size_m"] | |
| return ( | |
| int(round(spec["length_m"] / cube)) | |
| * int(round(spec["width_m"] / cube)) | |
| * int(round(spec["height_m"] / cube)) | |
| ) | |
| def _van_spec(vans_path: Path, van_type: str) -> dict: | |
| data = json.loads(vans_path.read_text(encoding="utf-8")) | |
| for v in data["van_types"]: | |
| if v["type"] == van_type: | |
| return v | |
| raise KeyError(f"unknown van_type: {van_type}") | |
| def _line_cells(p: dict, qty: int) -> int: | |
| return p["length_cells"] * p["width_cells"] * p["height_cells"] * qty | |
| def load( | |
| req: dict, | |
| products_path: Path = DEFAULT_PRODUCTS, | |
| vans_path: Path = DEFAULT_VANS, | |
| ) -> tuple[Depot, Fleet, list[Driver], list[Stop]]: | |
| products = { | |
| p["id"]: p | |
| for p in json.loads(products_path.read_text(encoding="utf-8"))["products"] | |
| } | |
| spec = _van_spec(vans_path, req["fleet"]["van_type"]) | |
| depot = Depot( | |
| id=req["depot"]["id"], | |
| lat=req["depot"]["coords"]["lat"], | |
| lng=req["depot"]["coords"]["lng"], | |
| open_s=_hms(req["depot"]["open"]), | |
| close_s=_hms(req["depot"]["close"]), | |
| ) | |
| fleet = Fleet( | |
| num_vans=req["fleet"]["num_vans"], | |
| capacity_kg=float(spec["max_payload_kg"]), | |
| capacity_cells=_van_capacity_cells(spec), | |
| ) | |
| drivers = [ | |
| Driver(d["id"], _hms(d["shift_start"]), _hms(d["shift_end"])) | |
| for d in req["drivers"] | |
| ] | |
| stops: list[Stop] = [] | |
| for s in req["stops"]: | |
| d_cells = sum(_line_cells(products[ln["product_id"]], ln["qty"]) for ln in s["deliveries"]) | |
| d_kg = sum(products[ln["product_id"]]["weight_kg"] * ln["qty"] for ln in s["deliveries"]) | |
| p_cells = sum(_line_cells(products[ln["product_id"]], ln["qty"]) for ln in s.get("pickups", [])) | |
| p_kg = sum(products[ln["product_id"]]["weight_kg"] * ln["qty"] for ln in s.get("pickups", [])) | |
| stops.append(Stop( | |
| id=s["id"], | |
| lat=s["coords"]["lat"], lng=s["coords"]["lng"], | |
| t_open_s=_hms(s["time_window"]["open"]), | |
| t_close_s=_hms(s["time_window"]["close"]), | |
| delivery_cells=d_cells, delivery_kg=d_kg, | |
| pickup_cells=p_cells, pickup_kg=p_kg, | |
| service_time_s=SERVICE_BASE_S + SERVICE_PER_CELL_S * (d_cells + p_cells), | |
| )) | |
| return depot, fleet, drivers, stops | |
| def build_travel_matrix(depot: Depot, stops: list[Stop]): | |
| """Travel-time + distance matrix scoped to depot + the request's stops.""" | |
| from travel_time import build_matrix | |
| from graph_manager import get_or_build_graph | |
| points = [("DEPOT", depot.lat, depot.lng)] + [(s.id, s.lat, s.lng) for s in stops] | |
| return build_matrix(get_or_build_graph(), points) | |