Rishav commited on
Commit ·
d2ece61
1
Parent(s): 9ba4f8a
Refine rejection heuristics and trap orders
Browse files
src/delivery_dispatch/policies.py
CHANGED
|
@@ -4,7 +4,7 @@ from dataclasses import dataclass
|
|
| 4 |
from typing import Any
|
| 5 |
|
| 6 |
|
| 7 |
-
ActionDict = dict[str, list[
|
| 8 |
Point = tuple[int, int]
|
| 9 |
|
| 10 |
|
|
@@ -21,6 +21,16 @@ class CandidateAssignment:
|
|
| 21 |
feasible_now: bool
|
| 22 |
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
def _as_point(value: Any) -> Point:
|
| 25 |
x, y = value
|
| 26 |
return int(x), int(y)
|
|
@@ -107,6 +117,13 @@ def _idle_agents(state: dict[str, Any]) -> list[dict[str, Any]]:
|
|
| 107 |
return [agent for agent in state.get("agents", []) if agent.get("status") == "idle"]
|
| 108 |
|
| 109 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
def _baseline_candidate(
|
| 111 |
agent: dict[str, Any],
|
| 112 |
order: dict[str, Any],
|
|
@@ -175,12 +192,67 @@ def _score_candidate(
|
|
| 175 |
)
|
| 176 |
|
| 177 |
|
| 178 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
return {
|
| 180 |
"assignments": [
|
| 181 |
{"agent_id": agent_id, "order_id": order_id}
|
| 182 |
for agent_id, order_id in assignments
|
| 183 |
-
]
|
|
|
|
| 184 |
}
|
| 185 |
|
| 186 |
|
|
@@ -243,12 +315,41 @@ def target_policy(state: dict[str, Any]) -> ActionDict:
|
|
| 243 |
chosen_orders.add(candidate.order_id)
|
| 244 |
assignments.append((candidate.agent_id, candidate.order_id))
|
| 245 |
|
| 246 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
|
| 248 |
|
| 249 |
__all__ = [
|
| 250 |
"ActionDict",
|
| 251 |
"CandidateAssignment",
|
|
|
|
| 252 |
"baseline_policy",
|
| 253 |
"build_action",
|
| 254 |
"estimate_job_cost",
|
|
|
|
| 4 |
from typing import Any
|
| 5 |
|
| 6 |
|
| 7 |
+
ActionDict = dict[str, list[Any]]
|
| 8 |
Point = tuple[int, int]
|
| 9 |
|
| 10 |
|
|
|
|
| 21 |
feasible_now: bool
|
| 22 |
|
| 23 |
|
| 24 |
+
@dataclass(frozen=True)
|
| 25 |
+
class CandidateRejection:
|
| 26 |
+
order_id: str
|
| 27 |
+
score: float
|
| 28 |
+
estimated_best_cost: int
|
| 29 |
+
slack_to_cutoff: int
|
| 30 |
+
reward_value: float
|
| 31 |
+
has_idle_agents: bool
|
| 32 |
+
|
| 33 |
+
|
| 34 |
def _as_point(value: Any) -> Point:
|
| 35 |
x, y = value
|
| 36 |
return int(x), int(y)
|
|
|
|
| 117 |
return [agent for agent in state.get("agents", []) if agent.get("status") == "idle"]
|
| 118 |
|
| 119 |
|
| 120 |
+
def _service_cutoff(order: dict[str, Any]) -> int:
|
| 121 |
+
cutoff = order.get("service_cutoff_time")
|
| 122 |
+
if cutoff is not None:
|
| 123 |
+
return int(cutoff)
|
| 124 |
+
return int(order["deadline"])
|
| 125 |
+
|
| 126 |
+
|
| 127 |
def _baseline_candidate(
|
| 128 |
agent: dict[str, Any],
|
| 129 |
order: dict[str, Any],
|
|
|
|
| 192 |
)
|
| 193 |
|
| 194 |
|
| 195 |
+
def _score_rejection_candidate(
|
| 196 |
+
order: dict[str, Any],
|
| 197 |
+
idle_agents: list[dict[str, Any]],
|
| 198 |
+
congested_zones: set[Point],
|
| 199 |
+
current_time: int,
|
| 200 |
+
) -> CandidateRejection:
|
| 201 |
+
reward_value = float(order["reward_value"])
|
| 202 |
+
cutoff = _service_cutoff(order)
|
| 203 |
+
estimated_costs = [
|
| 204 |
+
estimate_job_cost(
|
| 205 |
+
_as_point(agent["location"]),
|
| 206 |
+
_as_point(order["pickup_location"]),
|
| 207 |
+
_as_point(order["drop_location"]),
|
| 208 |
+
congested_zones,
|
| 209 |
+
)
|
| 210 |
+
for agent in idle_agents
|
| 211 |
+
]
|
| 212 |
+
estimated_best_cost = min(estimated_costs) if estimated_costs else 999
|
| 213 |
+
best_finish = current_time + estimated_best_cost
|
| 214 |
+
slack_to_cutoff = cutoff - best_finish
|
| 215 |
+
reject_pressure = 0.0
|
| 216 |
+
|
| 217 |
+
if not idle_agents:
|
| 218 |
+
reject_pressure += 4.0
|
| 219 |
+
if slack_to_cutoff < 0:
|
| 220 |
+
reject_pressure += 9.0 + (2.2 * abs(slack_to_cutoff))
|
| 221 |
+
elif slack_to_cutoff <= 1:
|
| 222 |
+
reject_pressure += 4.5
|
| 223 |
+
|
| 224 |
+
if reward_value <= 8:
|
| 225 |
+
reject_pressure += 2.4
|
| 226 |
+
elif reward_value <= 10:
|
| 227 |
+
reject_pressure += 1.6
|
| 228 |
+
elif reward_value >= 16:
|
| 229 |
+
reject_pressure -= 2.2
|
| 230 |
+
|
| 231 |
+
reward_density = reward_value / max(estimated_best_cost, 1)
|
| 232 |
+
if reward_density < 1.0:
|
| 233 |
+
reject_pressure += 3.6
|
| 234 |
+
elif reward_density < 1.35:
|
| 235 |
+
reject_pressure += 2.4
|
| 236 |
+
elif reward_density > 2.5:
|
| 237 |
+
reject_pressure -= 1.0
|
| 238 |
+
|
| 239 |
+
return CandidateRejection(
|
| 240 |
+
order_id=str(order["order_id"]),
|
| 241 |
+
score=reject_pressure,
|
| 242 |
+
estimated_best_cost=estimated_best_cost,
|
| 243 |
+
slack_to_cutoff=slack_to_cutoff,
|
| 244 |
+
reward_value=reward_value,
|
| 245 |
+
has_idle_agents=bool(idle_agents),
|
| 246 |
+
)
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
def build_action(assignments: list[tuple[str, str]], rejections: list[str] | None = None) -> ActionDict:
|
| 250 |
return {
|
| 251 |
"assignments": [
|
| 252 |
{"agent_id": agent_id, "order_id": order_id}
|
| 253 |
for agent_id, order_id in assignments
|
| 254 |
+
],
|
| 255 |
+
"rejections": list(rejections or []),
|
| 256 |
}
|
| 257 |
|
| 258 |
|
|
|
|
| 315 |
chosen_orders.add(candidate.order_id)
|
| 316 |
assignments.append((candidate.agent_id, candidate.order_id))
|
| 317 |
|
| 318 |
+
remaining_orders = [
|
| 319 |
+
order
|
| 320 |
+
for order in available_orders
|
| 321 |
+
if str(order["order_id"]) not in chosen_orders
|
| 322 |
+
]
|
| 323 |
+
rejection_candidates = [
|
| 324 |
+
_score_rejection_candidate(order, idle_agents, congested_zones, current_time)
|
| 325 |
+
for order in remaining_orders
|
| 326 |
+
]
|
| 327 |
+
rejection_candidates.sort(
|
| 328 |
+
key=lambda item: (
|
| 329 |
+
-item.score,
|
| 330 |
+
item.slack_to_cutoff,
|
| 331 |
+
item.estimated_best_cost,
|
| 332 |
+
item.order_id,
|
| 333 |
+
)
|
| 334 |
+
)
|
| 335 |
+
|
| 336 |
+
rejections: list[str] = []
|
| 337 |
+
for candidate in rejection_candidates:
|
| 338 |
+
if candidate.score < 6.5:
|
| 339 |
+
continue
|
| 340 |
+
if candidate.reward_value >= 16 and candidate.slack_to_cutoff >= -2:
|
| 341 |
+
continue
|
| 342 |
+
if candidate.reward_value >= 12 and candidate.slack_to_cutoff >= 0 and candidate.estimated_best_cost <= 8:
|
| 343 |
+
continue
|
| 344 |
+
rejections.append(candidate.order_id)
|
| 345 |
+
|
| 346 |
+
return build_action(assignments, rejections)
|
| 347 |
|
| 348 |
|
| 349 |
__all__ = [
|
| 350 |
"ActionDict",
|
| 351 |
"CandidateAssignment",
|
| 352 |
+
"CandidateRejection",
|
| 353 |
"baseline_policy",
|
| 354 |
"build_action",
|
| 355 |
"estimate_job_cost",
|
src/delivery_dispatch/scenarios.py
CHANGED
|
@@ -61,11 +61,12 @@ def build_high_demand_scenario() -> Scenario:
|
|
| 61 |
OrderState(order_id="o14", created_at=28, pickup_location=(7, 7), drop_location=(9, 7), reward_value=12, deadline=37),
|
| 62 |
OrderState(order_id="o15", created_at=34, pickup_location=(4, 1), drop_location=(5, 3), reward_value=8, deadline=42),
|
| 63 |
OrderState(order_id="o16", created_at=36, pickup_location=(8, 7), drop_location=(9, 5), reward_value=14, deadline=43),
|
|
|
|
| 64 |
),
|
| 65 |
episode_horizon=60,
|
| 66 |
-
briefing="Demand arrives faster than the fleet can comfortably absorb, including short hotspot bursts
|
| 67 |
-
dispatch_objective="Balance urgency, value density, and capacity; skipping the wrong job should hurt later throughput.",
|
| 68 |
-
known_future_signal="Expect bursty arrivals around the upper-right hotspot near t=17-22 and again late in the episode.",
|
| 69 |
)
|
| 70 |
|
| 71 |
|
|
@@ -109,11 +110,12 @@ def build_hotspot_congestion_scenario() -> Scenario:
|
|
| 109 |
OrderState(order_id="o16", created_at=47, pickup_location=(12, 11), drop_location=(14, 14), reward_value=21, deadline=56),
|
| 110 |
OrderState(order_id="o17", created_at=56, pickup_location=(13, 11), drop_location=(14, 13), reward_value=21, deadline=66),
|
| 111 |
OrderState(order_id="o18", created_at=63, pickup_location=(10, 3), drop_location=(6, 1), reward_value=10, deadline=74),
|
|
|
|
| 112 |
),
|
| 113 |
episode_horizon=80,
|
| 114 |
-
briefing="Large city with hotspot bursts, tight premium orders,
|
| 115 |
-
dispatch_objective="Trade off local urgent jobs against future hotspot bursts while avoiding long congested detours that block the fleet.",
|
| 116 |
-
known_future_signal="Expect premium hotspot spikes near t=18-20 and t=47, plus
|
| 117 |
)
|
| 118 |
|
| 119 |
|
|
|
|
| 61 |
OrderState(order_id="o14", created_at=28, pickup_location=(7, 7), drop_location=(9, 7), reward_value=12, deadline=37),
|
| 62 |
OrderState(order_id="o15", created_at=34, pickup_location=(4, 1), drop_location=(5, 3), reward_value=8, deadline=42),
|
| 63 |
OrderState(order_id="o16", created_at=36, pickup_location=(8, 7), drop_location=(9, 5), reward_value=14, deadline=43),
|
| 64 |
+
OrderState(order_id="o17", created_at=19, pickup_location=(1, 1), drop_location=(9, 9), reward_value=5, deadline=25),
|
| 65 |
),
|
| 66 |
episode_horizon=60,
|
| 67 |
+
briefing="Demand arrives faster than the fleet can comfortably absorb, including short hotspot bursts, premium clusters, and occasional low-value long-haul distractions.",
|
| 68 |
+
dispatch_objective="Balance urgency, value density, and capacity; skipping the wrong job should hurt later throughput, but some requests are not worth tying up the fleet for.",
|
| 69 |
+
known_future_signal="Expect bursty arrivals around the upper-right hotspot near t=17-22 and again late in the episode, with one low-yield cross-city distraction in the middle.",
|
| 70 |
)
|
| 71 |
|
| 72 |
|
|
|
|
| 110 |
OrderState(order_id="o16", created_at=47, pickup_location=(12, 11), drop_location=(14, 14), reward_value=21, deadline=56),
|
| 111 |
OrderState(order_id="o17", created_at=56, pickup_location=(13, 11), drop_location=(14, 13), reward_value=21, deadline=66),
|
| 112 |
OrderState(order_id="o18", created_at=63, pickup_location=(10, 3), drop_location=(6, 1), reward_value=10, deadline=74),
|
| 113 |
+
OrderState(order_id="o19", created_at=32, pickup_location=(1, 2), drop_location=(14, 14), reward_value=6, deadline=40),
|
| 114 |
),
|
| 115 |
episode_horizon=80,
|
| 116 |
+
briefing="Large city with hotspot bursts, tight premium orders, fixed congestion pockets, and a few low-yield long-haul traps. Positioning and selective commitments matter.",
|
| 117 |
+
dispatch_objective="Trade off local urgent jobs against future hotspot bursts while avoiding long congested detours and low-value commitments that block the fleet.",
|
| 118 |
+
known_future_signal="Expect premium hotspot spikes near t=18-20 and t=47, plus an unattractive cross-city request around t=32 and other long-haul work later.",
|
| 119 |
)
|
| 120 |
|
| 121 |
|