Isha184 commited on
Commit
ff0c3eb
·
verified ·
1 Parent(s): 5ac2f2d

Update solver.py

Browse files
Files changed (1) hide show
  1. solver.py +324 -38
solver.py CHANGED
@@ -1,44 +1,330 @@
1
- import numpy as np
2
  import math
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
- def euclidean(a, b):
5
- return math.dist(a, b)
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
- def clarke_wright(coords, demands, vehicle_capacity):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  """
9
- coords: list of (x,y), first is depot
10
- demands: list of demands (first is 0 for depot)
 
 
 
 
 
 
11
  """
12
- N = len(coords) - 1
13
- depot = 0
14
-
15
- # Distance matrix
16
- dist = [[euclidean(coords[i], coords[j]) for j in range(len(coords))] for i in range(len(coords))]
17
-
18
- # Start with each customer in its own route
19
- routes = [[0, i, 0] for i in range(1, N+1)]
20
- loads = [demands[i] for i in range(1, N+1)]
21
-
22
- # Compute savings
23
- savings = []
24
- for i in range(1, N+1):
25
- for j in range(i+1, N+1):
26
- s = dist[i][0] + dist[j][0] - dist[i][j]
27
- savings.append((s, i, j))
28
- savings.sort(reverse=True)
29
-
30
- # Merge routes
31
- for s, i, j in savings:
32
- r_i = next((r for r in routes if r[1] == i or r[-2] == i), None)
33
- r_j = next((r for r in routes if r[1] == j or r[-2] == j), None)
34
-
35
- if r_i is None or r_j is None or r_i == r_j:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  continue
37
-
38
- if r_i[-2] == i and r_j[1] == j and (sum(demands[k] for k in r_i[1:-1]+r_j[1:-1]) <= vehicle_capacity):
39
- new_route = r_i[:-1] + r_j[1:]
40
- routes.remove(r_i)
41
- routes.remove(r_j)
42
- routes.append(new_route)
43
-
44
- return routes, dist
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import math
2
+ import random
3
+ from typing import Dict, List, Tuple
4
+
5
+ import matplotlib.pyplot as plt
6
+ import numpy as np
7
+ import pandas as pd
8
+
9
+
10
+ # ---------------------------
11
+ # Data utils
12
+ # ---------------------------
13
+
14
+ def make_template_dataframe():
15
+ """Blank template users can download/fill."""
16
+ return pd.DataFrame(
17
+ {
18
+ "id": ["A", "B", "C"],
19
+ "x": [10, -5, 15],
20
+ "y": [4, -12, 8],
21
+ "demand": [1, 2, 1],
22
+ "tw_start": [0, 0, 0], # optional: earliest arrival (soft)
23
+ "tw_end": [9999, 9999, 9999], # optional: latest arrival (soft)
24
+ "service": [0, 0, 0], # optional: service time at stop
25
+ }
26
+ )
27
+
28
+ def parse_uploaded_csv(file) -> pd.DataFrame:
29
+ df = pd.read_csv(file.name if hasattr(file, "name") else file)
30
+ required = {"id", "x", "y", "demand"}
31
+ missing = required - set(df.columns)
32
+ if missing:
33
+ raise ValueError(f"Missing required columns: {sorted(missing)}")
34
+
35
+ # fill optional columns if absent
36
+ if "tw_start" not in df.columns:
37
+ df["tw_start"] = 0
38
+ if "tw_end" not in df.columns:
39
+ df["tw_end"] = 999999
40
+ if "service" not in df.columns:
41
+ df["service"] = 0
42
+
43
+ # Normalize types
44
+ df["id"] = df["id"].astype(str)
45
+ for col in ["x", "y", "demand", "tw_start", "tw_end", "service"]:
46
+ df[col] = pd.to_numeric(df[col], errors="coerce")
47
+ df = df.dropna()
48
+ df.reset_index(drop=True, inplace=True)
49
+ return df
50
+
51
+ def generate_random_instance(
52
+ n_clients=30,
53
+ n_vehicles=4,
54
+ capacity=10,
55
+ spread=50,
56
+ demand_min=1,
57
+ demand_max=3,
58
+ seed=42,
59
+ ) -> pd.DataFrame:
60
+ rng = np.random.default_rng(seed)
61
+ xs = rng.uniform(-spread, spread, size=n_clients)
62
+ ys = rng.uniform(-spread, spread, size=n_clients)
63
+ demands = rng.integers(demand_min, demand_max + 1, size=n_clients)
64
+
65
+ df = pd.DataFrame(
66
+ {
67
+ "id": [f"C{i+1}" for i in range(n_clients)],
68
+ "x": xs,
69
+ "y": ys,
70
+ "demand": demands,
71
+ "tw_start": np.zeros(n_clients, dtype=float),
72
+ "tw_end": np.full(n_clients, 999999.0),
73
+ "service": np.zeros(n_clients, dtype=float),
74
+ }
75
+ )
76
+ return df
77
+
78
+
79
+ # ---------------------------
80
+ # Geometry / distance helpers
81
+ # ---------------------------
82
+
83
+ def euclid(a: Tuple[float, float], b: Tuple[float, float]) -> float:
84
+ return float(math.hypot(a[0] - b[0], a[1] - b[1]))
85
+
86
+ def total_distance(points: List[Tuple[float, float]]) -> float:
87
+ return sum(euclid(points[i], points[i + 1]) for i in range(len(points) - 1))
88
+
89
+
90
+ # ---------------------------
91
+ # Sweep clustering (angle-based split)
92
+ # ---------------------------
93
+
94
+ def sweep_clusters(
95
+ df: pd.DataFrame,
96
+ depot: Tuple[float, float],
97
+ n_vehicles: int,
98
+ capacity: float,
99
+ ) -> List[List[int]]:
100
+ """
101
+ Assign clients to vehicles by angular sweep around the depot, roughly balancing
102
+ capacity (sum of 'demand').
103
+ Returns indices (row numbers) per cluster.
104
+ """
105
+ dx = df["x"].values - depot[0]
106
+ dy = df["y"].values - depot[1]
107
+ ang = np.arctan2(dy, dx)
108
+ order = np.argsort(ang)
109
+
110
+ clusters: List[List[int]] = [[] for _ in range(n_vehicles)]
111
+ loads = [0.0] * n_vehicles
112
+ v = 0
113
+ for idx in order:
114
+ d = float(df.loc[idx, "demand"])
115
+ # if adding to current vehicle exceeds capacity *by a lot*, move to next
116
+ if loads[v] + d > capacity and v < n_vehicles - 1:
117
+ v += 1
118
+ clusters[v].append(int(idx))
119
+ loads[v] += d
120
+
121
+ return clusters
122
+
123
+
124
+ # ---------------------------
125
+ # Route construction + 2-opt
126
+ # ---------------------------
127
 
128
+ def nearest_neighbor_route(
129
+ pts: List[Tuple[float, float]],
130
+ start_idx: int = 0,
131
+ ) -> List[int]:
132
+ n = len(pts)
133
+ unvisited = set(range(n))
134
+ route = [start_idx]
135
+ unvisited.remove(start_idx)
136
+ while unvisited:
137
+ last = route[-1]
138
+ nxt = min(unvisited, key=lambda j: euclid(pts[last], pts[j]))
139
+ route.append(nxt)
140
+ unvisited.remove(nxt)
141
+ return route
142
 
143
+ def two_opt(route: List[int], pts: List[Tuple[float, float]], max_iter=200) -> List[int]:
144
+ best = route[:]
145
+ best_len = total_distance([pts[i] for i in best])
146
+ n = len(route)
147
+ improved = True
148
+ it = 0
149
+ while improved and it < max_iter:
150
+ improved = False
151
+ it += 1
152
+ for i in range(1, n - 2):
153
+ for k in range(i + 1, n - 1):
154
+ new_route = best[:i] + best[i:k + 1][::-1] + best[k + 1:]
155
+ new_len = total_distance([pts[i] for i in new_route])
156
+ if new_len + 1e-9 < best_len:
157
+ best, best_len = new_route, new_len
158
+ improved = True
159
+ if improved is False:
160
+ break
161
+ return best
162
+
163
+ def build_route_for_cluster(
164
+ df: pd.DataFrame,
165
+ idxs: List[int],
166
+ depot: Tuple[float, float],
167
+ ) -> List[int]:
168
+ """
169
+ Build a TSP tour over cluster points and return client indices in visiting order.
170
+ Returns client indices (not including the depot) but representing the order.
171
+ """
172
+ # Local point list: depot at 0, then cluster in order
173
+ pts = [depot] + [(float(df.loc[i, "x"]), float(df.loc[i, "y"])) for i in idxs]
174
+ # Greedy tour over all nodes
175
+ rr = nearest_neighbor_route(pts, start_idx=0)
176
+ # Ensure route starts at 0 and ends at 0 conceptually; we'll remove the 0s later
177
+ # Optimize with 2-opt, but keep depot fixed by converting to a path that starts at 0
178
+ rr = two_opt(rr, pts)
179
+ # remove the depot index 0 from the sequence (keep order of clients)
180
+ order = [idxs[i - 1] for i in rr if i != 0]
181
+ return order
182
+
183
+
184
+ # ---------------------------
185
+ # Solve wrapper
186
+ # ---------------------------
187
+
188
+ def solve_vrp(
189
+ df: pd.DataFrame,
190
+ depot: Tuple[float, float] = (0.0, 0.0),
191
+ n_vehicles: int = 4,
192
+ capacity: float = 10,
193
+ speed: float = 1.0,
194
+ ) -> Dict:
195
  """
196
+ Returns:
197
+ {
198
+ 'routes': List[List[int]] (row indices of df),
199
+ 'total_distance': float,
200
+ 'per_route_distance': List[float],
201
+ 'assignments_table': pd.DataFrame,
202
+ 'metrics': dict
203
+ }
204
  """
205
+ # 1) cluster
206
+ clusters = sweep_clusters(df, depot=depot, n_vehicles=n_vehicles, capacity=capacity)
207
+
208
+ # 2) route per cluster
209
+ routes: List[List[int]] = []
210
+ per_route_dist: List[float] = []
211
+ soft_tw_violations = 0
212
+ per_route_loads: List[float] = []
213
+
214
+ for cl in clusters:
215
+ if len(cl) == 0:
216
+ routes.append([])
217
+ per_route_dist.append(0.0)
218
+ per_route_loads.append(0.0)
219
+ continue
220
+ order = build_route_for_cluster(df, cl, depot)
221
+ routes.append(order)
222
+
223
+ # compute distance with depot as start/end
224
+ pts = [depot] + [(df.loc[i, "x"], df.loc[i, "y"]) for i in order] + [depot]
225
+ dist = total_distance(pts)
226
+ per_route_dist.append(dist)
227
+
228
+ # capacity + soft TW check
229
+ load = float(df.loc[order, "demand"].sum()) if len(order) else 0.0
230
+ per_route_loads.append(load)
231
+
232
+ # simple arrival time simulation (speed distance units per time)
233
+ t = 0.0
234
+ prev = depot
235
+ for i in order:
236
+ cur = (df.loc[i, "x"], df.loc[i, "y"])
237
+ t += euclid(prev, cur) / max(speed, 1e-9)
238
+ tw_s = float(df.loc[i, "tw_start"])
239
+ tw_e = float(df.loc[i, "tw_end"])
240
+ if t < tw_s:
241
+ t = tw_s # wait
242
+ if t > tw_e:
243
+ soft_tw_violations += 1
244
+ t += float(df.loc[i, "service"])
245
+ prev = cur
246
+ # back to depot time is irrelevant for TW in this simple model
247
+
248
+ total_dist = float(sum(per_route_dist))
249
+
250
+ # Build assignment table
251
+ rows = []
252
+ for v, route in enumerate(routes):
253
+ for seq, idx in enumerate(route, start=1):
254
+ rows.append(
255
+ {
256
+ "vehicle": v + 1,
257
+ "sequence": seq,
258
+ "id": df.loc[idx, "id"],
259
+ "x": float(df.loc[idx, "x"]),
260
+ "y": float(df.loc[idx, "y"]),
261
+ "demand": float(df.loc[idx, "demand"]),
262
+ }
263
+ )
264
+ assign_df = pd.DataFrame(rows).sort_values(["vehicle", "sequence"]).reset_index(drop=True)
265
+
266
+ metrics = {
267
+ "vehicles_used": int(sum(1 for r in routes if len(r) > 0)),
268
+ "total_distance": round(total_dist, 3),
269
+ "per_route_distance": [round(d, 3) for d in per_route_dist],
270
+ "per_route_load": per_route_loads,
271
+ "capacity": capacity,
272
+ "soft_time_window_violations": int(soft_tw_violations),
273
+ "note": "Heuristic solution (sweep → greedy → 2-opt). TW are soft (informational).",
274
+ }
275
+
276
+ return {
277
+ "routes": routes,
278
+ "total_distance": total_dist,
279
+ "per_route_distance": per_route_dist,
280
+ "assignments_table": assign_df,
281
+ "metrics": metrics,
282
+ }
283
+
284
+
285
+ # ---------------------------
286
+ # Visualization
287
+ # ---------------------------
288
+
289
+ def plot_solution(
290
+ df: pd.DataFrame,
291
+ sol: Dict,
292
+ depot: Tuple[float, float] = (0.0, 0.0),
293
+ ):
294
+ routes = sol["routes"]
295
+
296
+ fig, ax = plt.subplots(figsize=(7.5, 6.5))
297
+ ax.scatter([depot[0]], [depot[1]], s=120, marker="s", label="Depot", zorder=5)
298
+
299
+ # color cycle
300
+ colors = plt.rcParams["axes.prop_cycle"].by_key().get("color", ["C0","C1","C2","C3","C4","C5"])
301
+
302
+ for v, route in enumerate(routes):
303
+ if not route:
304
  continue
305
+ c = colors[v % len(colors)]
306
+ xs = [depot[0]] + [float(df.loc[i, "x"]) for i in route] + [depot[0]]
307
+ ys = [depot[1]] + [float(df.loc[i, "y"]) for i in route] + [depot[1]]
308
+ ax.plot(xs, ys, "-", lw=2, color=c, alpha=0.9, label=f"Vehicle {v+1}")
309
+ ax.scatter(xs[1:-1], ys[1:-1], s=36, color=c, zorder=4)
310
+
311
+ # label sequence numbers lightly
312
+ for k, idx in enumerate(route, start=1):
313
+ ax.text(
314
+ float(df.loc[idx, "x"]),
315
+ float(df.loc[idx, "y"]),
316
+ str(k),
317
+ fontsize=8,
318
+ ha="center",
319
+ va="center",
320
+ color="white",
321
+ bbox=dict(boxstyle="circle,pad=0.2", fc=c, ec="none", alpha=0.7),
322
+ )
323
+
324
+ ax.set_title("Ride-Sharing / CVRP Routes (Heuristic)")
325
+ ax.set_xlabel("X")
326
+ ax.set_ylabel("Y")
327
+ ax.grid(True, alpha=0.25)
328
+ ax.legend(loc="best", fontsize=8, framealpha=0.9)
329
+ ax.set_aspect("equal", adjustable="box")
330
+ return fig