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[dict[str, str]]]
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 build_action(assignments: list[tuple[str, str]]) -> ActionDict:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- return build_action(assignments)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 that tempt overcommitment.",
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, and fixed congestion pockets. Positioning and selective commitments matter.",
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 occasional cross-city long-haul requests later.",
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