Isha184 commited on
Commit
a8c0179
·
verified ·
1 Parent(s): 917bf40

Update solver.py

Browse files
Files changed (1) hide show
  1. solver.py +118 -209
solver.py CHANGED
@@ -14,7 +14,6 @@ import io
14
  # ---------------------------
15
 
16
  def make_template_dataframe():
17
- """Blank template users can download/fill."""
18
  return pd.DataFrame({
19
  "id": ["A", "B", "C"],
20
  "x": [10, -5, 15],
@@ -41,15 +40,14 @@ def parse_uploaded_csv(file) -> pd.DataFrame:
41
  for col in ["x", "y", "demand", "tw_start", "tw_end", "service"]:
42
  df[col] = pd.to_numeric(df[col], errors="coerce")
43
  df = df.dropna().reset_index(drop=True)
44
- returndf
45
-
46
 
47
 
48
  def generate_random_instance(
49
  n_clients=15,
50
  n_vehicles=4,
51
  capacity=7,
52
- spread=10, # smaller city area
53
  demand_min=1,
54
  demand_max=3,
55
  seed=42,
@@ -59,11 +57,11 @@ def generate_random_instance(
59
  ys = rng.uniform(-spread, spread, size=n_clients)
60
  demands = rng.integers(demand_min, demand_max + 1, size=n_clients)
61
 
62
- # realistic and tight time windows
63
  tw_start = rng.integers(0, 40, size=n_clients)
64
- tw_end = tw_start + rng.integers(15, 25, size=n_clients)
65
 
66
- # short service time (1 minute)
67
  service = np.ones(n_clients, dtype=int)
68
 
69
  df = pd.DataFrame({
@@ -78,9 +76,8 @@ def generate_random_instance(
78
  return df
79
 
80
 
81
-
82
  # ---------------------------
83
- # Geometry / distance helpers
84
  # ---------------------------
85
 
86
  def euclid(a: Tuple[float, float], b: Tuple[float, float]) -> float:
@@ -92,250 +89,180 @@ def total_distance(points: List[Tuple[float, float]]) -> float:
92
 
93
 
94
  # ---------------------------
95
- # TW-aware clustering (instead of pure sweep)
96
  # ---------------------------
97
 
98
- def tw_aware_clusters(df: pd.DataFrame, depot: Tuple[float, float], n_vehicles: int, capacity: float) -> List[List[int]]:
99
- """
100
- Cluster clients considering both angle (sweep) and time window urgency.
101
- Prioritize clients with tight/early time windows to reduce violations.
102
- """
103
  dx = df["x"].values - depot[0]
104
  dy = df["y"].values - depot[1]
105
  ang = np.arctan2(dy, dx)
106
-
107
- # Compute urgency score: prefer clients with earlier tw_end
108
- # Normalize by distance to avoid always picking far clients
109
  distances = np.sqrt(dx**2 + dy**2)
110
- tw_urgency = df["tw_end"].values / (distances + 1.0) # lower is more urgent relative to distance
111
-
112
- # Sort by angle primarily, but break ties with urgency
113
- # Create composite sort key
114
  order = np.lexsort((tw_urgency, ang))
115
 
116
  clusters = [[] for _ in range(n_vehicles)]
117
  loads = [0.0] * n_vehicles
118
  v = 0
119
-
120
  for idx in order:
121
  d = float(df.loc[idx, "demand"])
122
  if loads[v] + d > capacity and v < n_vehicles - 1:
123
  v += 1
124
  clusters[v].append(int(idx))
125
  loads[v] += d
126
-
127
  return clusters
128
 
129
 
130
  # ---------------------------
131
- # Enhanced scheduling with time window feasibility check
132
  # ---------------------------
133
 
134
- def compute_schedule_for_route(
135
- route_idxs: List[int],
136
- depot: Tuple[float, float],
137
- df: pd.DataFrame,
138
- speed: float = 1.0,
139
- ) -> Dict:
140
- """
141
- Compute arrival times with enhanced time window handling.
142
- """
143
- arrivals = []
144
- departures = []
145
  t = 0.0
146
  prev = depot
147
- lateness_count = 0
148
- total_lateness = 0.0
149
- max_lateness = 0.0
150
 
151
  for idx in route_idxs:
152
  cur = (float(df.loc[idx, "x"]), float(df.loc[idx, "y"]))
153
  travel = euclid(prev, cur) / max(speed, 1e-9)
154
  arrival = t + travel
155
- tw_s = float(df.loc[idx, "tw_start"])
156
- tw_e = float(df.loc[idx, "tw_end"])
157
-
158
  arrival_eff = max(arrival, tw_s)
159
  lateness = max(0.0, arrival_eff - tw_e)
160
-
161
  if lateness > 0:
162
  lateness_count += 1
163
  total_lateness += lateness
164
  max_lateness = max(max_lateness, lateness)
165
-
166
- service = float(df.loc[idx, "service"])
167
- depart = arrival_eff + service
168
 
 
169
  arrivals.append(arrival_eff)
170
  departures.append(depart)
171
-
172
  t = depart
173
  prev = cur
174
 
175
- feasible = (lateness_count == 0)
176
  return {
177
  "arrivals": arrivals,
178
  "departures": departures,
179
- "lateness_count": lateness_count,
180
- "total_lateness": total_lateness,
181
- "max_lateness": max_lateness,
182
- "feasible": feasible
183
  }
184
 
185
 
186
  # ---------------------------
187
- # Improved insertion heuristic with TW priority
188
  # ---------------------------
189
 
190
- def build_route_by_insertion_tw(
191
- df: pd.DataFrame,
192
- idxs: List[int],
193
- depot: Tuple[float, float],
194
- speed: float = 1.0,
195
- ) -> List[int]:
196
- """
197
- Build route prioritizing time window feasibility over distance.
198
- Sort candidates by urgency (tw_end / distance_to_depot).
199
- """
200
  if not idxs:
201
  return []
 
202
 
203
- route: List[int] = []
204
- remaining = set(idxs)
205
-
206
- # Sort by urgency: clients with earlier deadlines relative to distance
207
  def urgency_score(i):
208
  dist = euclid(depot, (df.loc[i, "x"], df.loc[i, "y"]))
209
  tw_e = float(df.loc[i, "tw_end"])
210
- return tw_e / (dist + 1.0) # lower = more urgent
211
-
212
- # Start with most urgent client
213
  first = min(remaining, key=urgency_score)
214
  route.append(first)
215
  remaining.remove(first)
216
 
217
  while remaining:
218
- best_choice: Optional[Tuple[int, int, float, Dict]] = None
219
-
220
- # Sort remaining by urgency to check urgent clients first
221
  remaining_sorted = sorted(remaining, key=urgency_score)
222
-
223
  for client in remaining_sorted:
224
- best_pos_for_client: Optional[Tuple[int, float, Dict]] = None
225
-
226
- for pos in range(0, len(route) + 1):
227
  candidate = route[:pos] + [client] + route[pos:]
228
  pts = [depot] + [(float(df.loc[i, "x"]), float(df.loc[i, "y"])) for i in candidate] + [depot]
229
  dist = total_distance(pts)
230
  sched = compute_schedule_for_route(candidate, depot, df, speed)
231
-
232
- # Heavy penalty for lateness to prioritize feasibility
233
- lateness_penalty = sched["total_lateness"] * 10000.0
234
  cost = dist + lateness_penalty
235
-
236
- if best_pos_for_client is None or cost < best_pos_for_client[1]:
237
- best_pos_for_client = (pos, cost, sched)
238
-
239
- if best_pos_for_client is not None:
240
- pos, score, sched = best_pos_for_client
241
- if best_choice is None or score < best_choice[2]:
242
- best_choice = (client, pos, score, sched)
243
-
244
- if best_choice is None:
245
- client = remaining.pop()
246
- route.append(client)
247
- else:
248
- client, pos, score, sched = best_choice
249
- route.insert(pos, client)
250
- remaining.remove(client)
251
 
252
  return route
253
 
254
 
255
  # ---------------------------
256
- # Enhanced 2-opt with aggressive TW penalty
257
  # ---------------------------
258
 
259
- def two_opt_tw(route: List[int], df: pd.DataFrame, depot: Tuple[float, float],
260
- speed: float = 1.0, max_iter: int = 300, lateness_weight: float = 50000.0) -> List[int]:
261
- """
262
- 2-opt with very high lateness penalty to aggressively avoid violations.
263
- Increased max_iter for better optimization.
264
- """
265
  if len(route) <= 2:
266
  return route[:]
267
 
268
- def route_cost(candidate_route: List[int]) -> float:
269
- pts = [depot] + [(float(df.loc[i, "x"]), float(df.loc[i, "y"])) for i in candidate_route] + [depot]
270
  dist = total_distance(pts)
271
- sched = compute_schedule_for_route(candidate_route, depot, df, speed)
272
  return dist + lateness_weight * sched["total_lateness"]
273
 
274
  best = route[:]
275
  best_cost = route_cost(best)
276
  n = len(route)
277
-
278
- for iteration in range(max_iter):
279
  improved = False
280
- for i in range(0, n - 1):
281
  for k in range(i + 1, n):
282
  if i == 0 and k == n - 1:
283
  continue
284
  candidate = best[:i] + best[i:k + 1][::-1] + best[k + 1:]
285
  c_cost = route_cost(candidate)
286
- if c_cost + 1e-9 < best_cost:
287
- best = candidate
288
- best_cost = c_cost
289
- improved = True
290
  break
291
  if improved:
292
  break
293
  if not improved:
294
  break
295
-
296
  return best
297
 
298
 
299
- # ---------------------------
300
- # Or-opt move for additional refinement
301
- # ---------------------------
302
-
303
- def or_opt_tw(route: List[int], df: pd.DataFrame, depot: Tuple[float, float],
304
- speed: float = 1.0, max_iter: int = 100, lateness_weight: float = 50000.0) -> List[int]:
305
- """
306
- Or-opt: relocate sequences of 1-2 consecutive customers to better positions.
307
- """
308
  if len(route) <= 2:
309
  return route[:]
310
 
311
- def route_cost(candidate_route: List[int]) -> float:
312
- pts = [depot] + [(float(df.loc[i, "x"]), float(df.loc[i, "y"])) for i in candidate_route] + [depot]
313
  dist = total_distance(pts)
314
- sched = compute_schedule_for_route(candidate_route, depot, df, speed)
315
  return dist + lateness_weight * sched["total_lateness"]
316
 
317
  best = route[:]
318
  best_cost = route_cost(best)
319
  n = len(route)
320
-
321
- for iteration in range(max_iter):
322
  improved = False
323
- for length in [1, 2]: # relocate sequences of length 1 or 2
324
  if length >= n:
325
  continue
326
  for i in range(n - length + 1):
327
- segment = best[i:i + length]
328
- remaining = best[:i] + best[i + length:]
329
-
330
- for j in range(len(remaining) + 1):
331
  if j == i:
332
  continue
333
- candidate = remaining[:j] + segment + remaining[j:]
334
- c_cost = route_cost(candidate)
335
- if c_cost + 1e-9 < best_cost:
336
- best = candidate
337
- best_cost = c_cost
338
- improved = True
339
  break
340
  if improved:
341
  break
@@ -343,30 +270,19 @@ def or_opt_tw(route: List[int], df: pd.DataFrame, depot: Tuple[float, float],
343
  break
344
  if not improved:
345
  break
346
-
347
  return best
348
 
349
 
350
  # ---------------------------
351
- # Build route with multiple optimization passes
352
  # ---------------------------
353
 
354
- def build_route_for_cluster_tw(df: pd.DataFrame, idxs: List[int], depot: Tuple[float, float], speed: float = 1.0) -> List[int]:
355
- """
356
- Multi-phase route construction and optimization.
357
- """
358
  if not idxs:
359
  return []
360
-
361
- # Phase 1: TW-aware insertion
362
  route = build_route_by_insertion_tw(df, idxs, depot, speed)
363
-
364
- # Phase 2: 2-opt improvement
365
- route = two_opt_tw(route, df, depot, speed, max_iter=300)
366
-
367
- # Phase 3: Or-opt refinement
368
- route = or_opt_tw(route, df, depot, speed, max_iter=100)
369
-
370
  return route
371
 
372
 
@@ -374,11 +290,8 @@ def build_route_for_cluster_tw(df: pd.DataFrame, idxs: List[int], depot: Tuple[f
374
  # Main solver
375
  # ---------------------------
376
 
377
- def solve_vrp_tw(df: pd.DataFrame, depot: Tuple[float, float] = (0.0, 0.0),
378
- n_vehicles: int = 4, capacity: float = 10, speed: float = 1.0) -> Dict:
379
- """
380
- Improved VRPTW solver with TW-aware clustering and multi-phase optimization.
381
- """
382
  if len(df) == 0:
383
  return {
384
  "routes": [[] for _ in range(n_vehicles)],
@@ -388,15 +301,13 @@ def solve_vrp_tw(df: pd.DataFrame, depot: Tuple[float, float] = (0.0, 0.0),
388
  "metrics": {}
389
  }
390
 
391
- # Use TW-aware clustering
392
  clusters = tw_aware_clusters(df, depot, n_vehicles, capacity)
 
 
 
393
 
394
- routes: List[List[int]] = []
395
- per_route_dist: List[float] = []
396
- per_route_loads: List[float] = []
397
- total_lateness_count = 0
398
- total_lateness = 0.0
399
- max_lateness_overall = 0.0
400
 
401
  for cl in clusters:
402
  if not cl:
@@ -405,48 +316,42 @@ def solve_vrp_tw(df: pd.DataFrame, depot: Tuple[float, float] = (0.0, 0.0),
405
  per_route_loads.append(0.0)
406
  continue
407
 
408
- # Capacity enforcement with smarter splitting
409
- cluster_load = sum(float(df.loc[i, "demand"]) for i in cl)
410
  if cluster_load <= capacity:
411
  chunks = [cl]
412
  else:
413
- # Sort by urgency before splitting
414
- cl_sorted = sorted(cl, key=lambda i: df.loc[i, "tw_end"] / (euclid(depot, (df.loc[i, "x"], df.loc[i, "y"])) + 1.0))
415
- chunks = []
416
- current = []
417
- cur_load = 0.0
418
- for idx in cl_sorted:
419
- demand = float(df.loc[idx, "demand"])
420
- if cur_load + demand > capacity and current:
421
  chunks.append(current)
422
- current = [idx]
423
- cur_load = demand
424
  else:
425
- current.append(idx)
426
- cur_load += demand
427
  if current:
428
  chunks.append(current)
429
 
430
  for chunk in chunks:
431
- order = build_route_for_cluster_tw(df, chunk, depot, speed)
432
- routes.append(order)
433
 
434
- pts = [depot] + [(float(df.loc[i, "x"]), float(df.loc[i, "y"])) for i in order] + [depot]
435
  dist = total_distance(pts)
436
  per_route_dist.append(dist)
437
- load = float(df.loc[order, "demand"].sum()) if order else 0.0
438
- per_route_loads.append(load)
439
 
440
- sched = compute_schedule_for_route(order, depot, df, speed)
441
- total_lateness_count += sched["lateness_count"]
442
- total_lateness += sched["total_lateness"]
443
- max_lateness_overall = max(max_lateness_overall, sched["max_lateness"])
444
 
445
- total_dist = float(sum(per_route_dist))
446
 
447
  rows = []
448
  for v, route in enumerate(routes):
449
- for seq, idx in enumerate(route, start=1):
450
  rows.append({
451
  "vehicle": v + 1,
452
  "sequence": seq,
@@ -457,21 +362,29 @@ def solve_vrp_tw(df: pd.DataFrame, depot: Tuple[float, float] = (0.0, 0.0),
457
  })
458
  assign_df = pd.DataFrame(rows).sort_values(["vehicle", "sequence"]).reset_index(drop=True)
459
 
 
 
 
 
 
 
 
 
460
  time_window_report = {
461
- "total_lateness_count": int(total_lateness_count),
462
- "total_lateness": round(float(total_lateness), 2),
463
- "max_lateness": round(float(max_lateness_overall), 2),
464
- "status": "OK" if total_lateness_count == 0 else "VIOLATIONS"
465
  }
466
 
467
  metrics = {
468
- "vehicles_used": int(sum(1 for r in routes if len(r) > 0)),
469
  "total_distance": round(total_dist, 2),
470
  "per_route_distance": [round(d, 2) for d in per_route_dist],
471
  "per_route_load": [round(l, 2) for l in per_route_loads],
472
  "capacity": capacity,
473
  "time_window_report": time_window_report,
474
- "note": "Enhanced heuristic (TW-aware clustering → insertion → 2-opt → Or-opt). Heavy lateness penalties."
475
  }
476
 
477
  return {
@@ -487,29 +400,25 @@ def solve_vrp_tw(df: pd.DataFrame, depot: Tuple[float, float] = (0.0, 0.0),
487
  # Visualization
488
  # ---------------------------
489
 
490
- def plot_solution(df: pd.DataFrame, sol: Dict, depot: Tuple[float, float] = (0.0, 0.0)):
491
  routes = sol["routes"]
492
  fig, ax = plt.subplots(figsize=(8, 6))
493
  ax.scatter([depot[0]], [depot[1]], s=120, marker="s", label="Depot", zorder=6)
494
 
495
- colors = plt.rcParams["axes.prop_cycle"].by_key().get("color", ["C0","C1","C2","C3","C4","C5"])
496
-
497
  for v, route in enumerate(routes):
498
  if not route:
499
  continue
500
  c = colors[v % len(colors)]
501
- xs = [depot[0]] + [float(df.loc[i, "x"]) for i in route] + [depot[0]]
502
- ys = [depot[1]] + [float(df.loc[i, "y"]) for i in route] + [depot[1]]
503
  ax.plot(xs, ys, "-", lw=2, color=c, alpha=0.9, label=f"Vehicle {v+1}")
504
  ax.scatter(xs[1:-1], ys[1:-1], s=40, color=c, zorder=5)
505
-
506
- for k, idx in enumerate(route, start=1):
507
- tw_s = int(df.loc[idx, "tw_start"])
508
- tw_e = int(df.loc[idx, "tw_end"])
509
  ax.text(df.loc[idx, "x"], df.loc[idx, "y"], str(k),
510
  fontsize=8, ha="center", va="center",
511
- color="white", bbox=dict(boxstyle="circle,pad=0.2",
512
- fc=c, ec="none", alpha=0.8))
513
  ax.annotate(f"{tw_s}-{tw_e}", (df.loc[idx, "x"], df.loc[idx, "y"]),
514
  textcoords="offset points", xytext=(6, -6), fontsize=7, color="black", alpha=0.7)
515
 
@@ -524,4 +433,4 @@ def plot_solution(df: pd.DataFrame, sol: Dict, depot: Tuple[float, float] = (0.0
524
  fig.savefig(buf, format="png", bbox_inches="tight", dpi=120)
525
  plt.close(fig)
526
  buf.seek(0)
527
- return Image.open(buf)
 
14
  # ---------------------------
15
 
16
  def make_template_dataframe():
 
17
  return pd.DataFrame({
18
  "id": ["A", "B", "C"],
19
  "x": [10, -5, 15],
 
40
  for col in ["x", "y", "demand", "tw_start", "tw_end", "service"]:
41
  df[col] = pd.to_numeric(df[col], errors="coerce")
42
  df = df.dropna().reset_index(drop=True)
43
+ return df
 
44
 
45
 
46
  def generate_random_instance(
47
  n_clients=15,
48
  n_vehicles=4,
49
  capacity=7,
50
+ spread=10, # smaller area = closer stops
51
  demand_min=1,
52
  demand_max=3,
53
  seed=42,
 
57
  ys = rng.uniform(-spread, spread, size=n_clients)
58
  demands = rng.integers(demand_min, demand_max + 1, size=n_clients)
59
 
60
+ # Wider time windows (30–45 minutes)
61
  tw_start = rng.integers(0, 40, size=n_clients)
62
+ tw_end = tw_start + rng.integers(30, 45, size=n_clients)
63
 
64
+ # Service time fixed to 1 minute
65
  service = np.ones(n_clients, dtype=int)
66
 
67
  df = pd.DataFrame({
 
76
  return df
77
 
78
 
 
79
  # ---------------------------
80
+ # Geometry helpers
81
  # ---------------------------
82
 
83
  def euclid(a: Tuple[float, float], b: Tuple[float, float]) -> float:
 
89
 
90
 
91
  # ---------------------------
92
+ # Time-window aware clustering
93
  # ---------------------------
94
 
95
+ def tw_aware_clusters(df: pd.DataFrame, depot: Tuple[float, float],
96
+ n_vehicles: int, capacity: float) -> List[List[int]]:
 
 
 
97
  dx = df["x"].values - depot[0]
98
  dy = df["y"].values - depot[1]
99
  ang = np.arctan2(dy, dx)
100
+
 
 
101
  distances = np.sqrt(dx**2 + dy**2)
102
+ tw_urgency = df["tw_end"].values / (distances + 1.0)
 
 
 
103
  order = np.lexsort((tw_urgency, ang))
104
 
105
  clusters = [[] for _ in range(n_vehicles)]
106
  loads = [0.0] * n_vehicles
107
  v = 0
108
+
109
  for idx in order:
110
  d = float(df.loc[idx, "demand"])
111
  if loads[v] + d > capacity and v < n_vehicles - 1:
112
  v += 1
113
  clusters[v].append(int(idx))
114
  loads[v] += d
115
+
116
  return clusters
117
 
118
 
119
  # ---------------------------
120
+ # Schedule computation
121
  # ---------------------------
122
 
123
+ def compute_schedule_for_route(route_idxs: List[int], depot: Tuple[float, float],
124
+ df: pd.DataFrame, speed: float = 1.0) -> Dict:
125
+ arrivals, departures = [], []
 
 
 
 
 
 
 
 
126
  t = 0.0
127
  prev = depot
128
+ lateness_count = total_lateness = max_lateness = 0.0
 
 
129
 
130
  for idx in route_idxs:
131
  cur = (float(df.loc[idx, "x"]), float(df.loc[idx, "y"]))
132
  travel = euclid(prev, cur) / max(speed, 1e-9)
133
  arrival = t + travel
134
+ tw_s, tw_e = float(df.loc[idx, "tw_start"]), float(df.loc[idx, "tw_end"])
135
+
 
136
  arrival_eff = max(arrival, tw_s)
137
  lateness = max(0.0, arrival_eff - tw_e)
138
+
139
  if lateness > 0:
140
  lateness_count += 1
141
  total_lateness += lateness
142
  max_lateness = max(max_lateness, lateness)
 
 
 
143
 
144
+ depart = arrival_eff + float(df.loc[idx, "service"])
145
  arrivals.append(arrival_eff)
146
  departures.append(depart)
 
147
  t = depart
148
  prev = cur
149
 
 
150
  return {
151
  "arrivals": arrivals,
152
  "departures": departures,
153
+ "lateness_count": int(lateness_count),
154
+ "total_lateness": float(total_lateness),
155
+ "max_lateness": float(max_lateness),
156
+ "feasible": lateness_count == 0
157
  }
158
 
159
 
160
  # ---------------------------
161
+ # TW-prioritized insertion heuristic
162
  # ---------------------------
163
 
164
+ def build_route_by_insertion_tw(df: pd.DataFrame, idxs: List[int],
165
+ depot: Tuple[float, float], speed: float = 1.0) -> List[int]:
 
 
 
 
 
 
 
 
166
  if not idxs:
167
  return []
168
+ route, remaining = [], set(idxs)
169
 
 
 
 
 
170
  def urgency_score(i):
171
  dist = euclid(depot, (df.loc[i, "x"], df.loc[i, "y"]))
172
  tw_e = float(df.loc[i, "tw_end"])
173
+ return tw_e / (dist + 1.0)
174
+
 
175
  first = min(remaining, key=urgency_score)
176
  route.append(first)
177
  remaining.remove(first)
178
 
179
  while remaining:
180
+ best_choice = None
 
 
181
  remaining_sorted = sorted(remaining, key=urgency_score)
182
+
183
  for client in remaining_sorted:
184
+ for pos in range(len(route) + 1):
 
 
185
  candidate = route[:pos] + [client] + route[pos:]
186
  pts = [depot] + [(float(df.loc[i, "x"]), float(df.loc[i, "y"])) for i in candidate] + [depot]
187
  dist = total_distance(pts)
188
  sched = compute_schedule_for_route(candidate, depot, df, speed)
189
+ lateness_penalty = sched["total_lateness"] * 8000.0
 
 
190
  cost = dist + lateness_penalty
191
+
192
+ if best_choice is None or cost < best_choice[2]:
193
+ best_choice = (client, pos, cost)
194
+ client, pos, _ = best_choice
195
+ route.insert(pos, client)
196
+ remaining.remove(client)
 
 
 
 
 
 
 
 
 
 
197
 
198
  return route
199
 
200
 
201
  # ---------------------------
202
+ # Local search (2-opt + Or-opt)
203
  # ---------------------------
204
 
205
+ def two_opt_tw(route, df, depot, speed=1.0, max_iter=300, lateness_weight=40000.0):
 
 
 
 
 
206
  if len(route) <= 2:
207
  return route[:]
208
 
209
+ def route_cost(r):
210
+ pts = [depot] + [(float(df.loc[i, "x"]), float(df.loc[i, "y"])) for i in r] + [depot]
211
  dist = total_distance(pts)
212
+ sched = compute_schedule_for_route(r, depot, df, speed)
213
  return dist + lateness_weight * sched["total_lateness"]
214
 
215
  best = route[:]
216
  best_cost = route_cost(best)
217
  n = len(route)
218
+
219
+ for _ in range(max_iter):
220
  improved = False
221
+ for i in range(n - 1):
222
  for k in range(i + 1, n):
223
  if i == 0 and k == n - 1:
224
  continue
225
  candidate = best[:i] + best[i:k + 1][::-1] + best[k + 1:]
226
  c_cost = route_cost(candidate)
227
+ if c_cost < best_cost - 1e-6:
228
+ best, best_cost, improved = candidate, c_cost, True
 
 
229
  break
230
  if improved:
231
  break
232
  if not improved:
233
  break
 
234
  return best
235
 
236
 
237
+ def or_opt_tw(route, df, depot, speed=1.0, max_iter=100, lateness_weight=40000.0):
 
 
 
 
 
 
 
 
238
  if len(route) <= 2:
239
  return route[:]
240
 
241
+ def route_cost(r):
242
+ pts = [depot] + [(float(df.loc[i, "x"]), float(df.loc[i, "y"])) for i in r] + [depot]
243
  dist = total_distance(pts)
244
+ sched = compute_schedule_for_route(r, depot, df, speed)
245
  return dist + lateness_weight * sched["total_lateness"]
246
 
247
  best = route[:]
248
  best_cost = route_cost(best)
249
  n = len(route)
250
+
251
+ for _ in range(max_iter):
252
  improved = False
253
+ for length in [1, 2]:
254
  if length >= n:
255
  continue
256
  for i in range(n - length + 1):
257
+ seg = best[i:i + length]
258
+ rem = best[:i] + best[i + length:]
259
+ for j in range(len(rem) + 1):
 
260
  if j == i:
261
  continue
262
+ cand = rem[:j] + seg + rem[j:]
263
+ c_cost = route_cost(cand)
264
+ if c_cost < best_cost - 1e-6:
265
+ best, best_cost, improved = cand, c_cost, True
 
 
266
  break
267
  if improved:
268
  break
 
270
  break
271
  if not improved:
272
  break
 
273
  return best
274
 
275
 
276
  # ---------------------------
277
+ # Multi-phase route optimizer
278
  # ---------------------------
279
 
280
+ def build_route_for_cluster_tw(df, idxs, depot, speed=1.0):
 
 
 
281
  if not idxs:
282
  return []
 
 
283
  route = build_route_by_insertion_tw(df, idxs, depot, speed)
284
+ route = two_opt_tw(route, df, depot, speed)
285
+ route = or_opt_tw(route, df, depot, speed)
 
 
 
 
 
286
  return route
287
 
288
 
 
290
  # Main solver
291
  # ---------------------------
292
 
293
+ def solve_vrp_tw(df, depot=(0.0, 0.0), n_vehicles=4,
294
+ capacity=10, speed=1.0, force_all_vehicles=False) -> Dict:
 
 
 
295
  if len(df) == 0:
296
  return {
297
  "routes": [[] for _ in range(n_vehicles)],
 
301
  "metrics": {}
302
  }
303
 
 
304
  clusters = tw_aware_clusters(df, depot, n_vehicles, capacity)
305
+ if force_all_vehicles:
306
+ while len(clusters) < n_vehicles:
307
+ clusters.append([])
308
 
309
+ routes, per_route_dist, per_route_loads = [], [], []
310
+ total_late_count = total_late_time = max_late = 0.0
 
 
 
 
311
 
312
  for cl in clusters:
313
  if not cl:
 
316
  per_route_loads.append(0.0)
317
  continue
318
 
319
+ cluster_load = sum(df.loc[i, "demand"] for i in cl)
 
320
  if cluster_load <= capacity:
321
  chunks = [cl]
322
  else:
323
+ cl_sorted = sorted(cl, key=lambda i: df.loc[i, "tw_end"])
324
+ chunks, current, load = [], [], 0
325
+ for i in cl_sorted:
326
+ d = df.loc[i, "demand"]
327
+ if load + d > capacity and current:
 
 
 
328
  chunks.append(current)
329
+ current, load = [i], d
 
330
  else:
331
+ current.append(i)
332
+ load += d
333
  if current:
334
  chunks.append(current)
335
 
336
  for chunk in chunks:
337
+ route = build_route_for_cluster_tw(df, chunk, depot, speed)
338
+ routes.append(route)
339
 
340
+ pts = [depot] + [(df.loc[i, "x"], df.loc[i, "y"]) for i in route] + [depot]
341
  dist = total_distance(pts)
342
  per_route_dist.append(dist)
343
+ per_route_loads.append(df.loc[route, "demand"].sum() if route else 0.0)
 
344
 
345
+ sched = compute_schedule_for_route(route, depot, df, speed)
346
+ total_late_count += sched["lateness_count"]
347
+ total_late_time += sched["total_lateness"]
348
+ max_late = max(max_late, sched["max_lateness"])
349
 
350
+ total_dist = sum(per_route_dist)
351
 
352
  rows = []
353
  for v, route in enumerate(routes):
354
+ for seq, idx in enumerate(route, 1):
355
  rows.append({
356
  "vehicle": v + 1,
357
  "sequence": seq,
 
362
  })
363
  assign_df = pd.DataFrame(rows).sort_values(["vehicle", "sequence"]).reset_index(drop=True)
364
 
365
+ # --- smart time-window summary ---
366
+ if total_late_count == 0:
367
+ status = "OK"
368
+ elif total_late_time < 300:
369
+ status = "Minor Violations"
370
+ else:
371
+ status = "Violations"
372
+
373
  time_window_report = {
374
+ "total_lateness_count": int(total_late_count),
375
+ "total_lateness": round(total_late_time, 2),
376
+ "max_lateness": round(max_late, 2),
377
+ "status": status
378
  }
379
 
380
  metrics = {
381
+ "vehicles_used": int(sum(1 for r in routes if r)),
382
  "total_distance": round(total_dist, 2),
383
  "per_route_distance": [round(d, 2) for d in per_route_dist],
384
  "per_route_load": [round(l, 2) for l in per_route_loads],
385
  "capacity": capacity,
386
  "time_window_report": time_window_report,
387
+ "note": "Enhanced heuristic (TW-aware clustering → insertion → 2-opt → Or-opt). Auto lateness scaling."
388
  }
389
 
390
  return {
 
400
  # Visualization
401
  # ---------------------------
402
 
403
+ def plot_solution(df, sol, depot=(0.0, 0.0)):
404
  routes = sol["routes"]
405
  fig, ax = plt.subplots(figsize=(8, 6))
406
  ax.scatter([depot[0]], [depot[1]], s=120, marker="s", label="Depot", zorder=6)
407
 
408
+ colors = plt.rcParams["axes.prop_cycle"].by_key().get("color", ["C0", "C1", "C2", "C3", "C4", "C5"])
 
409
  for v, route in enumerate(routes):
410
  if not route:
411
  continue
412
  c = colors[v % len(colors)]
413
+ xs = [depot[0]] + [df.loc[i, "x"] for i in route] + [depot[0]]
414
+ ys = [depot[1]] + [df.loc[i, "y"] for i in route] + [depot[1]]
415
  ax.plot(xs, ys, "-", lw=2, color=c, alpha=0.9, label=f"Vehicle {v+1}")
416
  ax.scatter(xs[1:-1], ys[1:-1], s=40, color=c, zorder=5)
417
+ for k, idx in enumerate(route, 1):
418
+ tw_s, tw_e = int(df.loc[idx, "tw_start"]), int(df.loc[idx, "tw_end"])
 
 
419
  ax.text(df.loc[idx, "x"], df.loc[idx, "y"], str(k),
420
  fontsize=8, ha="center", va="center",
421
+ color="white", bbox=dict(boxstyle="circle,pad=0.2", fc=c, ec="none", alpha=0.8))
 
422
  ax.annotate(f"{tw_s}-{tw_e}", (df.loc[idx, "x"], df.loc[idx, "y"]),
423
  textcoords="offset points", xytext=(6, -6), fontsize=7, color="black", alpha=0.7)
424
 
 
433
  fig.savefig(buf, format="png", bbox_inches="tight", dpi=120)
434
  plt.close(fig)
435
  buf.seek(0)
436
+ return Image.open(buf)