Files
Enterprise/t_alns_rrd_reproduction/src/problem.py
2026-06-02 21:58:34 +08:00

181 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Problem definition for the DVRPTW-TA (Dynamic Vehicle Routing Problem
with Time Windows and Traffic Awareness).
Defines Route, Solution, and ProblemContext classes aligned with
the paper's formulation (§3.1).
"""
import copy
import hashlib
from typing import Dict, List, Optional, Tuple
import numpy as np
class Route:
"""A single vehicle's delivery route: sequence of customer nodes.
Format: [0, c1, c2, ..., ck, 0] where 0 is the depot.
"""
def __init__(self, vehicle_id: int, nodes: Optional[List[int]] = None):
self.vehicle_id = vehicle_id
self.nodes: List[int] = nodes if nodes is not None else [0, 0]
@property
def customers(self) -> List[int]:
"""Return customer nodes only (excluding depot(s))."""
return [n for n in self.nodes if n != 0]
@property
def n_customers(self) -> int:
return len(self.customers)
def total_demand(self, customers: Dict[int, "Customer"]) -> float:
"""Sum of demand for all customers on this route."""
return sum(customers[n].demand_kg for n in self.customers if n in customers)
def is_capacity_feasible(self, capacity: float, customers: Dict[int, "Customer"]) -> bool:
return self.total_demand(customers) <= capacity
def insert(self, customer_id: int, position: int):
"""Insert a customer at the given position (after depot start)."""
# position is 1-based index in the customer sequence
assert 1 <= position <= len(self.customers) + 1
# Find insertion point in self.nodes (skip leading depot)
insert_at = position
self.nodes.insert(insert_at, customer_id)
def remove(self, customer_id: int) -> bool:
"""Remove a customer from the route. Returns True if found."""
if customer_id in self.nodes:
self.nodes.remove(customer_id)
return True
return False
def copy(self) -> "Route":
return Route(self.vehicle_id, list(self.nodes))
def __repr__(self) -> str:
return f"Route(v{self.vehicle_id}: {self.nodes})"
class Solution:
"""A complete routing solution with m routes (one per vehicle)."""
def __init__(self, n_vehicles: int):
self.n_vehicles = n_vehicles
self.routes: List[Route] = [Route(k) for k in range(n_vehicles)]
def copy(self) -> "Solution":
s = Solution(self.n_vehicles)
s.routes = [r.copy() for r in self.routes]
return s
def find_route(self, customer_id: int) -> Optional[int]:
"""Find which vehicle (route index) serves a customer."""
for k, route in enumerate(self.routes):
if customer_id in route.nodes:
return k
return None
def find_position(self, customer_id: int) -> Optional[Tuple[int, int]]:
"""Find (vehicle_idx, position_in_nodes) for a customer."""
for k, route in enumerate(self.routes):
try:
pos = route.nodes.index(customer_id)
return (k, pos)
except ValueError:
continue
return None
def unassigned_customers(self, all_customer_ids: set) -> set:
"""Return set of customer IDs not assigned to any route."""
assigned = set()
for route in self.routes:
assigned.update(route.customers)
return all_customer_ids - assigned
def get_assignments(self) -> Dict[int, int]:
"""Return {customer_id: vehicle_id} for all assigned customers."""
result = {}
for k, route in enumerate(self.routes):
for c in route.customers:
result[c] = k
return result
def total_customers(self) -> int:
return sum(len(r.customers) for r in self.routes)
def __repr__(self) -> str:
parts = [f"Solution({self.n_vehicles} vehicles, {self.total_customers()} customers)"]
for r in self.routes:
parts.append(f" {r}")
return "\n".join(parts)
class ProblemContext:
"""Bundles all problem instance data for efficient access."""
def __init__(
self,
customers: Dict[int, "Customer"],
depot: "Depot",
traffic: "TrafficData",
n_vehicles: int,
vehicle_capacity: float,
op_start: float = 360.0,
op_end: float = 1080.0,
n_intervals: int = 12,
):
self.customers = customers
self.depot = depot
self.traffic = traffic
self.n_vehicles = n_vehicles
self.vehicle_capacity = vehicle_capacity
self.op_start = op_start
self.op_end = op_end
self.n_intervals = n_intervals
self.n_nodes = 1 + len(customers)
@property
def customer_ids(self) -> set:
return set(self.customers.keys())
@property
def interval_duration(self) -> float:
return (self.op_end - self.op_start) / self.n_intervals
def time_to_interval(self, minutes: float) -> int:
"""Map time to interval h.
The paper discretizes traffic feeds for 6:00-18:00. Customer windows may
extend to 20:00, so departures after the final traffic bucket use
hold-last-value semantics instead of extrapolating unobserved traffic.
"""
minutes = max(self.op_start, min(minutes, self.op_end - 1))
return int((minutes - self.op_start) / self.interval_duration)
def get_travel_time(self, i: int, j: int, depart_minutes: float) -> float:
"""Get time-dependent travel time for arc (i,j) at departure time."""
if i == j:
return 0.0
h = self.time_to_interval(depart_minutes)
val = self.traffic.travel_time[i, j, h]
return val if not np.isinf(val) else np.inf
def get_congestion_penalty(self, i: int, j: int, depart_minutes: float) -> float:
"""Get congestion penalty ρ_ij(T_i) from the traffic tensor."""
if i == j:
return 0.0
h = self.time_to_interval(depart_minutes)
return self.traffic.congestion_penalty[i, j, h]
def get_risk_adjusted_time(self, i: int, j: int, depart_minutes: float,
beta: float = 0.3) -> float:
"""Get risk-adjusted travel time: t'_ij = t_ij + β·η_ij (Eq.10)."""
t = self.get_travel_time(i, j, depart_minutes)
h = self.time_to_interval(depart_minutes)
eta = self.traffic.uncertainty[i, j, h]
return t + beta * eta