Spaces:
Sleeping
Sleeping
File size: 6,609 Bytes
4e22ad8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | """KPIs, explanations, load profile, warehouse-staging recommendations.
Everything here turns a `FleetPlan` from `solver.py` into the human-readable
add-ons that make the demo pitch land:
- per-van capacity-over-time profile (returnables visualization)
- per-stop arrival reasons
- fleet-level KPIs vs the naive baseline (savings %, CO2, utilization)
- warehouse staging recommendations for warehouse prep
"""
from __future__ import annotations
from dataclasses import dataclass
from loader import Depot, Driver, Fleet, Stop
from solver import FleetPlan, VanPlan
from travel_time import TravelMatrix
# UK DEFRA factor for diesel light commercial vehicle, kg CO2 per km.
CO2_KG_PER_KM = 0.20
@dataclass
class LoadPoint:
after_stop: str
cells: int
kg: int
@dataclass
class KPIs:
fleet_drive_min: float
baseline_drive_min: float
savings_pct: float
fleet_km: float
baseline_km: float
co2_kg_saved: float
driver_utilization_pct: float
capacity_utilization_pct: float
stops_per_van: list[int]
feasible_vans: int
total_vans: int
def _hm(s: int) -> str:
h, m = divmod(int(s) // 60, 60)
return f"{h:02d}:{m:02d}"
def load_profile(van: VanPlan, stops_by_id: dict[str, Stop]) -> list[LoadPoint]:
"""Truck departs depot fully loaded with all deliveries; load decreases as
we drop off and increases as we collect empties."""
if not van.stops:
return []
cells = sum(stops_by_id[p.id].delivery_cells for p in van.stops)
kg = sum(stops_by_id[p.id].delivery_kg for p in van.stops)
profile = [LoadPoint("DEPOT", int(cells), int(round(kg)))]
for p in van.stops:
s = stops_by_id[p.id]
cells = cells - s.delivery_cells + s.pickup_cells
kg = kg - s.delivery_kg + s.pickup_kg
profile.append(LoadPoint(p.id, int(cells), int(round(kg))))
return profile
def explain_van(van: VanPlan, stops_by_id: dict[str, Stop], driver: Driver) -> list[str]:
"""One line per visited stop, naming the driver-readable reason."""
out: list[str] = []
if not van.stops:
out.append(f"{driver.id} idle (no stops in this run).")
return out
first = van.stops[0]
s_first = stops_by_id[first.id]
idle_min = max(0, (first.arrival_s - driver.shift_start_s) // 60)
win_w_min = (s_first.t_close_s - s_first.t_open_s) // 60
if idle_min > 30:
out.append(
f"{driver.id} departs {_hm(driver.shift_start_s)}, parks at {first.id} "
f"by {_hm(first.arrival_s)} — its {win_w_min}min window opens at "
f"{_hm(s_first.t_open_s)}, so the truck waits then unloads at the open."
)
else:
out.append(
f"{driver.id} hits {first.id} at {_hm(first.arrival_s)} — "
f"first stop is the closest with an open window."
)
for prev_p, cur_p in zip(van.stops, van.stops[1:]):
s_cur = stops_by_id[cur_p.id]
slack_min = max(0, (s_cur.t_close_s - cur_p.arrival_s) // 60)
out.append(
f" → {cur_p.id} at {_hm(cur_p.arrival_s)}, "
f"{slack_min}min before window {_hm(s_cur.t_close_s)} closes."
)
out.append(
f" → returns to depot. Peak load {van.peak_cells} cells / "
f"{van.peak_kg} kg ({van.travel_s // 60} min driving)."
)
return out
def _van_distance_km(van: VanPlan, matrix: TravelMatrix) -> float:
if not van.stops:
return 0.0
depot_i = matrix.index_of("DEPOT")
total_m = 0.0
prev = depot_i
for sp in van.stops:
i = matrix.index_of(sp.id)
total_m += float(matrix.dist_m[prev, i])
prev = i
total_m += float(matrix.dist_m[prev, depot_i])
return total_m / 1000.0
def compute_kpis(
plan: FleetPlan,
baseline: FleetPlan,
fleet: Fleet,
drivers: list[Driver],
matrix: TravelMatrix,
) -> KPIs:
drive_min = plan.drive_s / 60
base_drive_min = baseline.drive_s / 60
savings = (
((base_drive_min - drive_min) / base_drive_min * 100)
if base_drive_min > 0 else 0.0
)
fleet_km = sum(_van_distance_km(v, matrix) for v in plan.vans)
base_km = sum(_van_distance_km(v, matrix) for v in baseline.vans)
co2_saved = max(0.0, (base_km - fleet_km) * CO2_KG_PER_KM)
busy = sum(v.total_s for v in plan.vans)
shift = sum(d.shift_end_s - d.shift_start_s for d in drivers)
driver_util = (busy / shift * 100) if shift > 0 else 0.0
used = [v for v in plan.vans if v.stops]
cap_util = (
sum(v.peak_cells / fleet.capacity_cells for v in used) / len(used) * 100
if used else 0.0
)
return KPIs(
fleet_drive_min=round(drive_min, 2),
baseline_drive_min=round(base_drive_min, 2),
savings_pct=round(savings, 1),
fleet_km=round(fleet_km, 2),
baseline_km=round(base_km, 2),
co2_kg_saved=round(co2_saved, 2),
driver_utilization_pct=round(driver_util, 1),
capacity_utilization_pct=round(cap_util, 1),
stops_per_van=[len(v.stops) for v in plan.vans],
feasible_vans=sum(1 for v in plan.vans if v.feasible),
total_vans=len(plan.vans),
)
def warehouse_prep(
plan: FleetPlan,
request_stops: list[dict],
stops_by_id: dict[str, Stop],
) -> list[str]:
"""Per-van staging hints, derived from each van's actual delivery list."""
recs: list[str] = []
by_id = {s["id"]: s for s in request_stops}
for v in plan.vans:
if not v.stops:
continue
sku_qty: dict[str, int] = {}
total_returnable = 0
for sp in v.stops:
req = by_id[sp.id]
for line in req.get("deliveries", []):
sku_qty[line["product_id"]] = sku_qty.get(line["product_id"], 0) + line["qty"]
for line in req.get("pickups", []):
total_returnable += line["qty"]
top = sorted(sku_qty.items(), key=lambda x: -x[1])[:3]
recs.append(
f"{v.driver_id}: stage "
f"{', '.join(f'{q}× {p}' for p, q in top)} "
f"({sum(sku_qty.values())} units total)"
)
if total_returnable:
first = stops_by_id[v.stops[0].id]
recs.append(
f" reserve ~{first.pickup_cells} returnable cells near {v.driver_id}'s door "
f"(pickups start at {v.stops[0].id})"
)
return recs
|