TechAvenger commited on
Commit
44b3df5
·
verified ·
1 Parent(s): b3fe321

Upload 9 files

Browse files
Files changed (9) hide show
  1. .gitignore +2 -0
  2. API +1 -0
  3. API.py +2 -0
  4. chip_routing.py +372 -0
  5. chip_routingv2.py +780 -0
  6. chip_routingv3.py +911 -0
  7. client.py +262 -0
  8. requirements.txt +3 -0
  9. requirenments.txt +1 -0
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ API
2
+ API.py
API ADDED
@@ -0,0 +1 @@
 
 
1
+ API = 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
API.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ def API():
2
+ return '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'
chip_routing.py ADDED
@@ -0,0 +1,372 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ chip_routing_cuopt.py
3
+ ─────────────────────────────────────────────────────────────────────────────
4
+ Real-world chip routing optimizer using NVIDIA cuOpt.
5
+
6
+ Context
7
+ ───────
8
+ In chip/PCB routing, we model the problem as a Vehicle Routing Problem (VRP):
9
+ • "Vehicles" → routing agents (one per metal layer: M1, M2, …)
10
+ • "Tasks" → signal nets / vias that must be connected
11
+ • "Locations" → grid nodes on the routing fabric
12
+ • "Cost" → wire length + layer-change penalty + congestion weight
13
+ • "Capacity" → track utilization budget per routing channel
14
+
15
+ Layers
16
+ Layer 1 (M1) – horizontal preferred (lower resistance for power rails)
17
+ Layer 2 (M2) – vertical preferred (signal routing)
18
+
19
+ The cost matrix encodes Manhattan distance + congestion surcharge between
20
+ every pair of grid nodes. Travel-time matrix models signal delay (RC).
21
+
22
+ Usage
23
+ ─────
24
+ pip install requests
25
+ python chip_routing_cuopt.py
26
+
27
+ Output
28
+ ──────
29
+ Prints the optimised route sequence per layer-agent with estimated
30
+ wire-length and timing slack.
31
+ """
32
+
33
+ import json
34
+ import sys
35
+ import time
36
+
37
+ import requests
38
+
39
+ import API
40
+
41
+ # ─── Configuration ───────────────────────────────────────────────────────────
42
+
43
+ API_KEY = API.API()
44
+ #print(API_KEY)
45
+ INVOKE_URL = "https://optimize.api.nvidia.com/v1/nvidia/cuopt"
46
+ FETCH_URL_FMT = "https://optimize.api.nvidia.com/v1/status/{}"
47
+ POLL_INTERVAL_S = 1.0 # seconds between status polls
48
+ MAX_WAIT_S = 120 # give up after this many seconds
49
+
50
+ HEADERS = {
51
+ "Authorization": f"Bearer {API_KEY}",
52
+ "Accept": "application/json",
53
+ "Content-Type": "application/json",
54
+ }
55
+
56
+ # ─── Chip / Grid Definition ──────────────────────────────────────────────────
57
+
58
+ # 5×5 routing grid → 25 nodes, indexed row-major: node = row*5 + col
59
+ GRID_ROWS = 5
60
+ GRID_COLS = 5
61
+ N_NODES = GRID_ROWS * GRID_COLS # 25
62
+
63
+ # Metal layers modelled as separate "vehicle types" in cuOpt
64
+ LAYERS = {
65
+ "M1": 1, # horizontal preferred – lower unit cost on H-edges
66
+ "M2": 2, # vertical preferred – lower unit cost on V-edges
67
+ }
68
+
69
+ # Congestion map: (node_index) → surcharge added to every edge touching it.
70
+ # Represents hot-spots from prior global routing passes.
71
+ CONGESTION = {
72
+ 6: 2, # centre-left cluster
73
+ 7: 3,
74
+ 12: 4, # middle – heavily congested
75
+ 13: 3,
76
+ 18: 2,
77
+ }
78
+
79
+ # Signal nets to route: each net needs a connection visit at its sink node.
80
+ # Format: (net_id, source_node, sink_node, demand_M1, demand_M2, earliest, latest)
81
+ NETS = [
82
+ # net_id src sink d_M1 d_M2 tw_early tw_late
83
+ ("CLK", 0, 24, 1, 1, 0, 8),
84
+ ("VDD", 0, 20, 2, 1, 0, 9),
85
+ ("DATA_A", 4, 16, 1, 1, 1, 7),
86
+ ("DATA_B", 4, 21, 1, 1, 1, 8),
87
+ ("RESET", 0, 14, 1, 1, 0, 6),
88
+ ("OUT_X", 20, 24, 1, 1, 2, 9),
89
+ ("OUT_Y", 5, 23, 1, 1, 2, 9),
90
+ ]
91
+
92
+ # Router agents: one per layer. They start / end at node 0 (top-left pad ring).
93
+ ROUTERS = [
94
+ # router_id layer cap_M1 cap_M2 time_window max_cost max_time
95
+ ("M1_router", "M1", 6, 4, (0, 10), 30, 12),
96
+ ("M2_router", "M2", 4, 6, (0, 10), 30, 12),
97
+ ]
98
+
99
+ # ─── Cost / Delay Matrix Builder ─────────────────────────────────────────────
100
+
101
+ def node_to_rc(n: int):
102
+ return divmod(n, GRID_COLS) # (row, col)
103
+
104
+ def manhattan(a: int, b: int) -> int:
105
+ r1, c1 = node_to_rc(a)
106
+ r2, c2 = node_to_rc(b)
107
+ return abs(r1 - r2) + abs(c1 - c2)
108
+
109
+ def edge_congestion(a: int, b: int) -> int:
110
+ """Sum congestion surcharges for endpoints."""
111
+ return CONGESTION.get(a, 0) + CONGESTION.get(b, 0)
112
+
113
+ def build_cost_matrix(layer_id: int) -> list[list[int]]:
114
+ """
115
+ Cost = Manhattan distance + congestion penalty.
116
+ M1 (layer 1) pays extra for vertical moves (prefers horizontal).
117
+ M2 (layer 2) pays extra for horizontal moves (prefers vertical).
118
+ """
119
+ n = N_NODES
120
+ mat = [[0] * n for _ in range(n)]
121
+ for a in range(n):
122
+ for b in range(n):
123
+ if a == b:
124
+ continue
125
+ ra, ca = node_to_rc(a)
126
+ rb, cb = node_to_rc(b)
127
+ h_dist = abs(ca - cb)
128
+ v_dist = abs(ra - rb)
129
+ if layer_id == 1: # M1 horizontal preferred
130
+ layer_penalty = v_dist # penalise vertical hops
131
+ else: # M2 vertical preferred
132
+ layer_penalty = h_dist # penalise horizontal hops
133
+ base = h_dist + v_dist + layer_penalty
134
+ cong = edge_congestion(a, b)
135
+ mat[a][b] = max(1, base + cong)
136
+ return mat
137
+
138
+ def build_delay_matrix() -> list[list[int]]:
139
+ """
140
+ Signal delay ∝ wire length (RC). Congestion adds extra delay (buffering).
141
+ Layer-independent for timing analysis.
142
+ """
143
+ n = N_NODES
144
+ mat = [[0] * n for _ in range(n)]
145
+ for a in range(n):
146
+ for b in range(n):
147
+ if a == b:
148
+ continue
149
+ dist = manhattan(a, b)
150
+ cong = edge_congestion(a, b) // 2 # partial delay impact
151
+ mat[a][b] = max(1, dist + cong)
152
+ return mat
153
+
154
+ # ─── cuOpt Payload Builder ────────────────────────────────────────────────────
155
+
156
+ def build_payload() -> dict:
157
+ cost_M1 = build_cost_matrix(layer_id=1)
158
+ cost_M2 = build_cost_matrix(layer_id=2)
159
+ delay = build_delay_matrix()
160
+
161
+ # cuOpt expects matrices keyed by vehicle-type id (as string)
162
+ cost_matrix_data = {
163
+ "1": cost_M1, # M1_router
164
+ "2": cost_M2, # M2_router
165
+ }
166
+ delay_matrix_data = {
167
+ "1": delay,
168
+ "2": delay,
169
+ }
170
+
171
+ n_routers = len(ROUTERS)
172
+ n_nets = len(NETS)
173
+
174
+ # Fleet (routing agents / layers)
175
+ # cuOpt capacities shape: [n_capacity_dims][n_vehicles]
176
+ vehicle_locations = [[0, 0]] * n_routers # start + end at node 0
177
+ vehicle_ids = [r[0] for r in ROUTERS]
178
+ capacities = [
179
+ [r[2] for r in ROUTERS], # dim-0: M1 track budget per router
180
+ [r[3] for r in ROUTERS], # dim-1: M2 track budget per router
181
+ ]
182
+ vehicle_time_wins = [list(r[4]) for r in ROUTERS]
183
+ vehicle_max_costs = [r[5] for r in ROUTERS]
184
+ vehicle_max_times = [r[6] for r in ROUTERS]
185
+ vehicle_types = [LAYERS[r[1]] for r in ROUTERS]
186
+
187
+ # Tasks (nets to route)
188
+ task_locations = [n[2] for n in NETS] # sink nodes
189
+ task_ids = [n[0] for n in NETS]
190
+ # cuOpt demand shape: [n_capacity_dims][n_tasks]
191
+ demand = [
192
+ [net[3] for net in NETS], # dim-0: M1 track demand per net (index 3)
193
+ [net[4] for net in NETS], # dim-1: M2 track demand per net (index 4)
194
+ ]
195
+ task_tw = [[n[5], n[6]] for n in NETS]
196
+ service_times = [0] * n_nets # routing is instantaneous post-opt
197
+
198
+ payload = {
199
+ "action": "cuOpt_OptimizedRouting",
200
+ "data": {
201
+ "cost_matrix_data": {"data": cost_matrix_data},
202
+ "travel_time_matrix_data": {"data": delay_matrix_data},
203
+ "fleet_data": {
204
+ "vehicle_locations": vehicle_locations,
205
+ "vehicle_ids": vehicle_ids,
206
+ "capacities": capacities,
207
+ "vehicle_time_windows": vehicle_time_wins,
208
+ "vehicle_types": vehicle_types,
209
+ "vehicle_max_costs": vehicle_max_costs,
210
+ "vehicle_max_times": vehicle_max_times,
211
+ "skip_first_trips": [False] * n_routers,
212
+ "drop_return_trips": [True] * n_routers, # no need to return to origin
213
+ "min_vehicles": 1,
214
+ },
215
+ "task_data": {
216
+ "task_locations": task_locations,
217
+ "task_ids": task_ids,
218
+ "demand": demand,
219
+ "task_time_windows": task_tw,
220
+ "service_times": service_times,
221
+ },
222
+ "solver_config": {
223
+ "time_limit": 5, # seconds – increase for larger chips
224
+ "objectives": {
225
+ "cost": 2, # minimise wire length + congestion
226
+ "travel_time": 1, # minimise signal delay
227
+ "variance_route_size": 1, # balance load across layers
228
+ "variance_route_service_time": 0,
229
+ "prize": 0,
230
+ },
231
+ "verbose_mode": False,
232
+ "error_logging": True,
233
+ },
234
+ },
235
+ "client_version": "chip_router_v1.0",
236
+ }
237
+ return payload
238
+
239
+ # ─── Result Interpreter ───────────────────────────────────────────────────────
240
+
241
+ def interpret_results(response_body: dict):
242
+ """Pretty-print the optimised routing schedule."""
243
+ print("\n" + "═" * 60)
244
+ print(" CHIP ROUTING OPTIMISATION RESULTS")
245
+ print("═" * 60)
246
+
247
+ if "error" in response_body:
248
+ print(f" ✗ Solver error: {response_body['error']}")
249
+ return
250
+
251
+ # Dump raw response if --debug flag passed
252
+ if "--debug" in sys.argv:
253
+ print(json.dumps(response_body, indent=2))
254
+
255
+ routes = response_body.get("response", {}).get("solver_response", {})
256
+ if not routes:
257
+ print(" No routes returned.")
258
+ print(json.dumps(response_body, indent=2))
259
+ return
260
+
261
+ vehicle_data = routes.get("vehicle_data", {})
262
+ # solution_cost holds the actual total objective value
263
+ solution_cost = routes.get("solution_cost", routes.get("total_objective", 0))
264
+ router_by_name = {r[0]: r for r in ROUTERS}
265
+
266
+ # Build task lookup: task string id → net name
267
+ # cuOpt uses the task_ids we supplied, but also inserts "Depot" entries
268
+ task_name_map = {n[0]: n[0] for n in NETS} # "CLK" -> "CLK", etc.
269
+
270
+ # Compute real wire costs from the cost matrices we built
271
+ cost_M1 = build_cost_matrix(layer_id=1)
272
+ cost_M2 = build_cost_matrix(layer_id=2)
273
+ layer_cost_matrix = {"M1": cost_M1, "M2": cost_M2}
274
+
275
+ total_wirelength = 0
276
+
277
+ for veh_id, data in vehicle_data.items():
278
+ router = router_by_name.get(str(veh_id))
279
+ router_name = router[0] if router else str(veh_id)
280
+ layer_name = router[1] if router else "?"
281
+ cmat = layer_cost_matrix.get(layer_name, cost_M1)
282
+
283
+ task_seq = data.get("task_id", [])
284
+ route_nodes = data.get("route", [])
285
+ arrivals = data.get("arrival_stamp", [])
286
+
287
+ # Filter out depot stops (task id == "Depot" or node == 0 at start/end)
288
+ stops = []
289
+ for idx, t in enumerate(task_seq):
290
+ node = route_nodes[idx] if idx < len(route_nodes) else None
291
+ arr = arrivals[idx] if idx < len(arrivals) else "?"
292
+ if str(t) == "Depot":
293
+ continue
294
+ stops.append((t, node, arr))
295
+
296
+ # Compute actual wire length for this router's path
297
+ wire_cost = 0
298
+ if len(stops) > 1:
299
+ for i in range(len(stops) - 1):
300
+ a = stops[i][1]
301
+ b = stops[i+1][1]
302
+ if a is not None and b is not None:
303
+ wire_cost += cmat[int(a)][int(b)]
304
+ # Add depot→first and last→depot legs
305
+ if stops:
306
+ wire_cost += cmat[0][int(stops[0][1])]
307
+ # drop_return_trips=True so no return leg
308
+
309
+ total_wirelength += wire_cost
310
+
311
+ print(f"\n ┌─ {router_name} [{layer_name}]")
312
+ print(f" │ Wire length (cost units) : {wire_cost}")
313
+ print(f" │ Nets routed : {len(stops)}")
314
+
315
+ if not stops:
316
+ print(" │ (no nets assigned)")
317
+ else:
318
+ print(" │ Net sequence:")
319
+ for seq_i, (t, node, arr) in enumerate(stops):
320
+ net_name = task_name_map.get(str(t), str(t))
321
+ r, c = node_to_rc(int(node)) if node is not None else ("?", "?")
322
+ # Timing slack = latest_allowed - actual_arrival
323
+ net_def = next((n for n in NETS if n[0] == net_name), None)
324
+ slack = f"{net_def[6] - float(arr):+.1f}" if net_def and arr != "?" else "?"
325
+ print(f" │ [{seq_i+1}] {net_name:<10} → node {node:>2} "
326
+ f"(grid {r},{c}) arrival={arr:<5} slack={slack}")
327
+ print(" └" + "─" * 50)
328
+
329
+ print(f"\n ► Total wire length : {total_wirelength} cost-units")
330
+ print(f" ► Solver objective : {solution_cost}")
331
+ print(f" ► Grid size : {GRID_ROWS}×{GRID_COLS}")
332
+ print(f" ► Nets routed : {len(NETS)} | Layers: {len(ROUTERS)}")
333
+ print("═" * 60 + "\n")
334
+
335
+ # ─── Main ─────────────────────────────────────────────────────────────────────
336
+
337
+ def main():
338
+ print("Building cost matrices …")
339
+ payload = build_payload()
340
+
341
+ # Optionally dump the payload for inspection
342
+ if "--dump" in sys.argv:
343
+ print(json.dumps(payload, indent=2))
344
+ return
345
+
346
+ print("Submitting to NVIDIA cuOpt …")
347
+ session = requests.Session()
348
+ response = session.post(INVOKE_URL, headers=HEADERS, json=payload, timeout=30)
349
+
350
+ # Poll for async completion
351
+ elapsed = 0
352
+ while response.status_code == 202:
353
+ req_id = response.headers.get("NVCF-REQID")
354
+ fetch_url = FETCH_URL_FMT.format(req_id)
355
+ print(f" Waiting for result (req={req_id[:8]}…) [{elapsed}s]")
356
+ time.sleep(POLL_INTERVAL_S)
357
+ elapsed += POLL_INTERVAL_S
358
+ response = session.get(fetch_url, headers=HEADERS, timeout=30)
359
+ if elapsed > MAX_WAIT_S:
360
+ sys.exit("ERROR: Timed out waiting for cuOpt result.")
361
+
362
+ try:
363
+ response.raise_for_status()
364
+ except requests.HTTPError as exc:
365
+ sys.exit(f"HTTP error: {exc}\nBody: {response.text}")
366
+
367
+ response_body = response.json()
368
+ interpret_results(response_body)
369
+
370
+
371
+ if __name__ == "__main__":
372
+ main()
chip_routingv2.py ADDED
@@ -0,0 +1,780 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ @Author : Mihir Mithani
5
+ @Date : 08-05-2026 , 10:44
6
+ @File : chip_routingv2.py
7
+ @Desc :
8
+ """
9
+ """
10
+ chip_routing_cuopt.py
11
+ ─────────────────────────────────────────────────────────────────────────────
12
+ Interactive chip routing optimizer using NVIDIA cuOpt.
13
+
14
+ Features
15
+ ────────
16
+ • User enters grid dimensions (m × n) via GUI
17
+ • Click cells to select them, then name the component placed there
18
+ • Create connection pairs by clicking source → sink cells
19
+ • Runs cuOpt VRP solver to find optimal routing
20
+ • Visualises the routed result on the grid
21
+
22
+ Usage
23
+ ─────
24
+ pip install requests
25
+ python chip_routing_cuopt.py
26
+
27
+ Set your API key in the NVIDIA_API_KEY variable below (or via env var).
28
+ """
29
+
30
+ import time
31
+ import tkinter as tk
32
+ from tkinter import messagebox, simpledialog
33
+
34
+ import requests
35
+
36
+ import API
37
+
38
+ # ─── API Configuration ────────────────────────────────────────────────────────
39
+
40
+ NVIDIA_API_KEY = API.API()
41
+ INVOKE_URL = "https://optimize.api.nvidia.com/v1/nvidia/cuopt"
42
+ FETCH_URL_FMT = "https://optimize.api.nvidia.com/v1/status/{}"
43
+ POLL_INTERVAL_S = 1.2
44
+ MAX_WAIT_S = 120
45
+
46
+ HEADERS = {
47
+ "Authorization": f"Bearer {NVIDIA_API_KEY}",
48
+ "Accept": "application/json",
49
+ "Content-Type": "application/json",
50
+ }
51
+
52
+ # ─── Colors ───────────────────────────────────────────────────────────────────
53
+
54
+ CLR_BG = "#0f1117"
55
+ CLR_PANEL = "#1a1d27"
56
+ CLR_BORDER = "#2e3248"
57
+ CLR_ACCENT = "#5b6af0"
58
+ CLR_ACCENT2 = "#a78bfa"
59
+ CLR_TEXT = "#e8eaf6"
60
+ CLR_MUTED = "#6b7280"
61
+ CLR_CELL_EMPTY = "#1e2133"
62
+ CLR_CELL_COMP = "#1e2d50"
63
+ CLR_CELL_DEPOT = "#2d1e50"
64
+ CLR_CELL_SELA = "#1e3b2a" # source (green tint)
65
+ CLR_CELL_SELB = "#3b2a1e" # sink (amber tint)
66
+ CLR_CELL_ROUTED = "#1a3320"
67
+ CLR_CELL_HOVER = "#252840"
68
+ CLR_SUCCESS = "#34d399"
69
+ CLR_WARNING = "#fbbf24"
70
+ CLR_DANGER = "#f87171"
71
+ CLR_NET_COLORS = [
72
+ "#5b6af0", "#a78bfa", "#34d399", "#fbbf24", "#f87171",
73
+ "#38bdf8", "#fb7185", "#4ade80", "#facc15", "#818cf8",
74
+ ]
75
+
76
+ CELL_W = 90
77
+ CELL_H = 60
78
+ PAD = 12
79
+
80
+
81
+ # ─── Matrix Builders ──────────────────────────────────────────────────────────
82
+
83
+ def node_to_rc(n, cols):
84
+ return divmod(n, cols)
85
+
86
+
87
+ def manhattan(a, b, cols):
88
+ r1, c1 = node_to_rc(a, cols)
89
+ r2, c2 = node_to_rc(b, cols)
90
+ return abs(r1 - r2) + abs(c1 - c2)
91
+
92
+
93
+ def build_cost_matrix(rows, cols, layer_id):
94
+ n = rows * cols
95
+ mat = []
96
+ for a in range(n):
97
+ row = []
98
+ ra, ca = node_to_rc(a, cols)
99
+ for b in range(n):
100
+ if a == b:
101
+ row.append(0)
102
+ continue
103
+ rb, cb = node_to_rc(b, cols)
104
+ hd = abs(ca - cb)
105
+ vd = abs(ra - rb)
106
+ penalty = vd if layer_id == 1 else hd
107
+ row.append(max(1, hd + vd + penalty))
108
+ mat.append(row)
109
+ return mat
110
+
111
+
112
+ def build_delay_matrix(rows, cols):
113
+ n = rows * cols
114
+ mat = []
115
+ for a in range(n):
116
+ row = []
117
+ for b in range(n):
118
+ if a == b:
119
+ row.append(0)
120
+ else:
121
+ row.append(max(1, manhattan(a, b, cols)))
122
+ mat.append(row)
123
+ return mat
124
+
125
+
126
+ def build_payload(rows, cols, pairs):
127
+ n_nets = len(pairs)
128
+ max_t = rows * cols + 4
129
+ cap = n_nets + 4
130
+
131
+ cost_m1 = build_cost_matrix(rows, cols, 1)
132
+ cost_m2 = build_cost_matrix(rows, cols, 2)
133
+ delay = build_delay_matrix(rows, cols)
134
+
135
+ return {
136
+ "action": "cuOpt_OptimizedRouting",
137
+ "data": {
138
+ "cost_matrix_data": {"data": {"1": cost_m1, "2": cost_m2}},
139
+ "travel_time_matrix_data": {"data": {"1": delay, "2": delay}},
140
+ "fleet_data": {
141
+ "vehicle_locations": [[0, 0], [0, 0]],
142
+ "vehicle_ids": ["M1_router", "M2_router"],
143
+ "capacities": [[cap, cap], [cap, cap]],
144
+ "vehicle_time_windows": [[0, max_t], [0, max_t]],
145
+ "vehicle_types": [1, 2],
146
+ "vehicle_max_costs": [rows * cols * 6, rows * cols * 6],
147
+ "vehicle_max_times": [max_t, max_t],
148
+ "skip_first_trips": [False, False],
149
+ "drop_return_trips": [True, True],
150
+ "min_vehicles": 1,
151
+ },
152
+ "task_data": {
153
+ "task_locations": [p["sink"] for p in pairs],
154
+ "task_ids": [p["name"] for p in pairs],
155
+ "demand": [[1] * n_nets, [1] * n_nets],
156
+ "task_time_windows": [[0, max_t]] * n_nets,
157
+ "service_times": [0] * n_nets,
158
+ },
159
+ "solver_config": {
160
+ "time_limit": 5,
161
+ "objectives": {
162
+ "cost": 2,
163
+ "travel_time": 1,
164
+ "variance_route_size": 1,
165
+ "variance_route_service_time": 0,
166
+ "prize": 0,
167
+ },
168
+ "verbose_mode": False,
169
+ "error_logging": True,
170
+ },
171
+ },
172
+ "client_version": "chip_router_interactive_v2",
173
+ }
174
+
175
+
176
+ def call_cuopt(payload):
177
+ session = requests.Session()
178
+ response = session.post(INVOKE_URL, headers=HEADERS, json=payload, timeout=30)
179
+ elapsed = 0
180
+ while response.status_code == 202:
181
+ req_id = response.headers.get("NVCF-REQID", "")
182
+ fetch_url = FETCH_URL_FMT.format(req_id)
183
+ time.sleep(POLL_INTERVAL_S)
184
+ elapsed += POLL_INTERVAL_S
185
+ if elapsed > MAX_WAIT_S:
186
+ raise TimeoutError("cuOpt request timed out")
187
+ response = session.get(fetch_url, headers=HEADERS, timeout=30)
188
+ response.raise_for_status()
189
+ return response.json()
190
+
191
+
192
+ # ─── Main Application ─────────────────────────────────────────────────────────
193
+
194
+ class ChipRoutingApp(tk.Tk):
195
+ def __init__(self):
196
+ super().__init__()
197
+ self.title("Chip Routing Optimizer · cuOpt")
198
+ self.configure(bg=CLR_BG)
199
+ self.resizable(True, True)
200
+ self.geometry("1100x750")
201
+
202
+ # State
203
+ self.rows = 0
204
+ self.cols = 0
205
+ self.components = {} # node_idx → component name
206
+ self.pairs = [] # list of dicts {name, src, sink, src_name, sink_name}
207
+ self.sel_cell = None # currently selected cell (editing mode)
208
+ self.pair_src = None # first cell picked in pair mode
209
+ self.mode = "edit" # "edit" | "pair" | "result"
210
+ self.cell_btns = {} # node_idx → canvas item ids
211
+ self.net_colors = {} # pair name → color
212
+
213
+ self._build_ui()
214
+
215
+ # ── UI Construction ───────────────────────────────────────────────────────
216
+
217
+ def _build_ui(self):
218
+ # Left panel
219
+ self.left = tk.Frame(self, bg=CLR_PANEL, width=260)
220
+ self.left.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 0))
221
+ self.left.pack_propagate(False)
222
+ self._build_left_panel()
223
+
224
+ # Right canvas area
225
+ self.right = tk.Frame(self, bg=CLR_BG)
226
+ self.right.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
227
+
228
+ # Canvas with scrollbars
229
+ self.canvas_frame = tk.Frame(self.right, bg=CLR_BG)
230
+ self.canvas_frame.pack(fill=tk.BOTH, expand=True, padx=PAD, pady=PAD)
231
+
232
+ self.hscroll = tk.Scrollbar(self.canvas_frame, orient=tk.HORIZONTAL)
233
+ self.vscroll = tk.Scrollbar(self.canvas_frame, orient=tk.VERTICAL)
234
+ self.canvas = tk.Canvas(
235
+ self.canvas_frame,
236
+ bg=CLR_BG,
237
+ highlightthickness=0,
238
+ xscrollcommand=self.hscroll.set,
239
+ yscrollcommand=self.vscroll.set,
240
+ )
241
+ self.hscroll.config(command=self.canvas.xview)
242
+ self.vscroll.config(command=self.canvas.yview)
243
+ self.hscroll.pack(side=tk.BOTTOM, fill=tk.X)
244
+ self.vscroll.pack(side=tk.RIGHT, fill=tk.Y)
245
+ self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
246
+
247
+ # Status bar
248
+ self.status_var = tk.StringVar(value="Enter grid dimensions and click Build.")
249
+ self.status_bar = tk.Label(
250
+ self.right, textvariable=self.status_var,
251
+ bg=CLR_PANEL, fg=CLR_MUTED, font=("Consolas", 10),
252
+ anchor=tk.W, padx=12, pady=6,
253
+ )
254
+ self.status_bar.pack(fill=tk.X, side=tk.BOTTOM)
255
+
256
+ def _build_left_panel(self):
257
+ lp = self.left
258
+ tk.Label(lp, text="CHIP ROUTER", bg=CLR_PANEL, fg=CLR_ACCENT,
259
+ font=("Consolas", 13, "bold")).pack(pady=(18, 2))
260
+ tk.Label(lp, text="cuOpt VRP Engine", bg=CLR_PANEL, fg=CLR_MUTED,
261
+ font=("Consolas", 9)).pack(pady=(0, 16))
262
+
263
+ self._sep(lp)
264
+
265
+ # ── Step 1: Grid dims ─────────────────────────────────────────────
266
+ tk.Label(lp, text="① GRID DIMENSIONS", bg=CLR_PANEL, fg=CLR_TEXT,
267
+ font=("Consolas", 10, "bold")).pack(anchor=tk.W, padx=14, pady=(10, 4))
268
+
269
+ df = tk.Frame(lp, bg=CLR_PANEL)
270
+ df.pack(fill=tk.X, padx=14, pady=4)
271
+ tk.Label(df, text="Rows (m)", bg=CLR_PANEL, fg=CLR_MUTED,
272
+ font=("Consolas", 9)).grid(row=0, column=0, sticky=tk.W)
273
+ self.rows_var = tk.IntVar(value=4)
274
+ tk.Spinbox(df, from_=2, to=12, textvariable=self.rows_var, width=5,
275
+ bg=CLR_CELL_EMPTY, fg=CLR_TEXT, insertbackground=CLR_TEXT,
276
+ buttonbackground=CLR_BORDER, relief=tk.FLAT).grid(row=0, column=1, padx=6)
277
+ tk.Label(df, text="Cols (n)", bg=CLR_PANEL, fg=CLR_MUTED,
278
+ font=("Consolas", 9)).grid(row=1, column=0, sticky=tk.W, pady=(4, 0))
279
+ self.cols_var = tk.IntVar(value=5)
280
+ tk.Spinbox(df, from_=2, to=12, textvariable=self.cols_var, width=5,
281
+ bg=CLR_CELL_EMPTY, fg=CLR_TEXT, insertbackground=CLR_TEXT,
282
+ buttonbackground=CLR_BORDER, relief=tk.FLAT).grid(row=1, column=1, padx=6, pady=(4, 0))
283
+
284
+ self._btn(lp, "▶ BUILD GRID", self._on_build, CLR_ACCENT)
285
+
286
+ self._sep(lp)
287
+
288
+ # ── Step 2: Component placement ───────────────────────────────────
289
+ tk.Label(lp, text="② PLACE COMPONENTS", bg=CLR_PANEL, fg=CLR_TEXT,
290
+ font=("Consolas", 10, "bold")).pack(anchor=tk.W, padx=14, pady=(10, 4))
291
+ tk.Label(lp, text="Click any cell to select it,\nthen name the component below.",
292
+ bg=CLR_PANEL, fg=CLR_MUTED, font=("Consolas", 8),
293
+ justify=tk.LEFT).pack(anchor=tk.W, padx=14)
294
+
295
+ cf = tk.Frame(lp, bg=CLR_PANEL)
296
+ cf.pack(fill=tk.X, padx=14, pady=6)
297
+ tk.Label(cf, text="Name", bg=CLR_PANEL, fg=CLR_MUTED,
298
+ font=("Consolas", 9)).pack(side=tk.LEFT)
299
+ self.comp_var = tk.StringVar()
300
+ self.comp_entry = tk.Entry(cf, textvariable=self.comp_var, width=14,
301
+ bg=CLR_CELL_EMPTY, fg=CLR_TEXT, relief=tk.FLAT,
302
+ insertbackground=CLR_TEXT, font=("Consolas", 10))
303
+ self.comp_entry.pack(side=tk.LEFT, padx=(6, 0))
304
+ self.comp_entry.bind("<Return>", lambda _: self._on_place_comp())
305
+
306
+ bf = tk.Frame(lp, bg=CLR_PANEL)
307
+ bf.pack(fill=tk.X, padx=14)
308
+ self._btn_small(bf, "Place", self._on_place_comp, CLR_SUCCESS, side=tk.LEFT)
309
+ self._btn_small(bf, "Clear", self._on_clear_comp, CLR_DANGER, side=tk.LEFT)
310
+
311
+ self.sel_label = tk.Label(lp, text="No cell selected", bg=CLR_PANEL,
312
+ fg=CLR_MUTED, font=("Consolas", 8))
313
+ self.sel_label.pack(anchor=tk.W, padx=14, pady=(4, 0))
314
+
315
+ self._sep(lp)
316
+
317
+ # ── Step 3: Wire pairs ────────────────────────────────────────────
318
+ tk.Label(lp, text="③ WIRE PAIRS", bg=CLR_PANEL, fg=CLR_TEXT,
319
+ font=("Consolas", 10, "bold")).pack(anchor=tk.W, padx=14, pady=(10, 4))
320
+ tk.Label(lp, text="Click to toggle pair mode.",
321
+ bg=CLR_PANEL, fg=CLR_MUTED, font=("Consolas", 8),
322
+ justify=tk.LEFT).pack(anchor=tk.W, padx=14)
323
+
324
+ self.pair_mode_btn = tk.Button(
325
+ lp, text="⛓ ENTER PAIR MODE", font=("Consolas", 9, "bold"),
326
+ bg=CLR_CELL_EMPTY, fg=CLR_ACCENT2, relief=tk.FLAT,
327
+ activebackground=CLR_BORDER, activeforeground=CLR_ACCENT2,
328
+ command=self._toggle_pair_mode, cursor="hand2",
329
+ )
330
+ self.pair_mode_btn.pack(fill=tk.X, padx=14, pady=(6, 4))
331
+
332
+ self.pair_status_lbl = tk.Label(lp, text="", bg=CLR_PANEL,
333
+ fg=CLR_WARNING, font=("Consolas", 8))
334
+ self.pair_status_lbl.pack(anchor=tk.W, padx=14)
335
+
336
+ # Pair list
337
+ self.pair_list_frame = tk.Frame(lp, bg=CLR_PANEL)
338
+ self.pair_list_frame.pack(fill=tk.X, padx=14, pady=4)
339
+
340
+ self._sep(lp)
341
+
342
+ # ── Step 4: Route ─────────────────────────────────────────────────
343
+ tk.Label(lp, text="④ ROUTE", bg=CLR_PANEL, fg=CLR_TEXT,
344
+ font=("Consolas", 10, "bold")).pack(anchor=tk.W, padx=14, pady=(10, 4))
345
+ self._btn(lp, "⚡ RUN CUOPT", self._on_run, CLR_ACCENT2)
346
+ self._btn(lp, "↺ RESET ALL", self._on_reset, CLR_MUTED)
347
+
348
+ def _sep(self, parent):
349
+ tk.Frame(parent, bg=CLR_BORDER, height=1).pack(fill=tk.X, padx=0, pady=4)
350
+
351
+ def _btn(self, parent, text, cmd, color):
352
+ tk.Button(
353
+ parent, text=text, font=("Consolas", 9, "bold"),
354
+ bg=color, fg=CLR_BG, relief=tk.FLAT,
355
+ activebackground=color, activeforeground=CLR_BG,
356
+ command=cmd, cursor="hand2", padx=6, pady=6,
357
+ ).pack(fill=tk.X, padx=14, pady=4)
358
+
359
+ def _btn_small(self, parent, text, cmd, color, side=tk.LEFT):
360
+ tk.Button(
361
+ parent, text=text, font=("Consolas", 8),
362
+ bg=color, fg=CLR_BG, relief=tk.FLAT,
363
+ activebackground=color, activeforeground=CLR_BG,
364
+ command=cmd, cursor="hand2", padx=6, pady=3,
365
+ ).pack(side=side, padx=(0, 4))
366
+
367
+ # ── Grid Drawing ──────────────────────────────────────────────────────────
368
+
369
+ def _on_build(self):
370
+ self.rows = self.rows_var.get()
371
+ self.cols = self.cols_var.get()
372
+ self.components = {}
373
+ self.pairs = []
374
+ self.sel_cell = None
375
+ self.pair_src = None
376
+ self.mode = "edit"
377
+ self.net_colors = {}
378
+ self.pair_mode_btn.config(text="⛓ ENTER PAIR MODE", bg=CLR_CELL_EMPTY, fg=CLR_ACCENT2)
379
+ self._render_grid()
380
+ self._refresh_pair_list()
381
+ self._set_status(f"Grid {self.rows}×{self.cols} built. Click cells to name components.")
382
+
383
+ def _render_grid(self, routed_nodes=None, route_assignments=None):
384
+ self.canvas.delete("all")
385
+ self.cell_items = {} # node → (rect_id, text_id, sub_id)
386
+ routed_nodes = routed_nodes or {}
387
+ route_assignments = route_assignments or {}
388
+
389
+ total_w = self.cols * CELL_W + PAD * 2
390
+ total_h = self.rows * CELL_H + PAD * 2
391
+ self.canvas.config(scrollregion=(0, 0, total_w, total_h))
392
+
393
+ for r in range(self.rows):
394
+ for c in range(self.cols):
395
+ n = r * self.cols + c
396
+ x0 = PAD + c * CELL_W
397
+ y0 = PAD + r * CELL_H
398
+ x1 = x0 + CELL_W - 2
399
+ y1 = y0 + CELL_H - 2
400
+ cx = (x0 + x1) / 2
401
+ cy = (y0 + y1) / 2
402
+
403
+ color = self._cell_color(n, routed_nodes)
404
+ rid = self.canvas.create_rectangle(
405
+ x0, y0, x1, y1, fill=color, outline=CLR_BORDER,
406
+ width=1, tags=(f"cell_{n}", "cell"),
407
+ )
408
+ # node index label (bottom-right)
409
+ self.canvas.create_text(
410
+ x1 - 3, y1 - 2, text=f"{r},{c}",
411
+ fill=CLR_MUTED, font=("Consolas", 7), anchor=tk.SE,
412
+ tags=(f"cell_{n}",),
413
+ )
414
+ # component name
415
+ label = "DEPOT" if n == 0 else self.components.get(n, "")
416
+ lcolor = CLR_ACCENT2 if n == 0 else CLR_TEXT
417
+ tid = self.canvas.create_text(
418
+ cx, cy - 4, text=label,
419
+ fill=lcolor, font=("Consolas", 9, "bold"),
420
+ width=CELL_W - 8, anchor=tk.CENTER,
421
+ tags=(f"cell_{n}",),
422
+ )
423
+ # net assignment (result mode)
424
+ sub = ""
425
+ if n in route_assignments:
426
+ sub = route_assignments[n]
427
+ sid = self.canvas.create_text(
428
+ cx, y1 - 10, text=sub,
429
+ fill=CLR_SUCCESS, font=("Consolas", 7),
430
+ width=CELL_W - 6, anchor=tk.CENTER,
431
+ tags=(f"cell_{n}",),
432
+ )
433
+ self.cell_items[n] = (rid, tid, sid)
434
+
435
+ # bind clicks
436
+ for tag_id in (rid, tid, sid):
437
+ self.canvas.tag_bind(tag_id, "<Button-1>",
438
+ lambda e, nd=n: self._on_cell_click(nd))
439
+ self.canvas.tag_bind(tag_id, "<Enter>",
440
+ lambda e, nd=n: self._on_cell_hover(nd, True))
441
+ self.canvas.tag_bind(tag_id, "<Leave>",
442
+ lambda e, nd=n: self._on_cell_hover(nd, False))
443
+
444
+ # Draw pair source lines (in pair mode, show pairs as arrows)
445
+ if self.mode in ("pair", "edit"):
446
+ self._draw_pair_arrows()
447
+
448
+ def _draw_pair_arrows(self):
449
+ self.canvas.delete("arrow")
450
+ for i, p in enumerate(self.pairs):
451
+ color = self.net_colors.get(p["name"], CLR_NET_COLORS[i % len(CLR_NET_COLORS)])
452
+ sr, sc = node_to_rc(p["src"], self.cols)
453
+ dr, dc = node_to_rc(p["sink"], self.cols)
454
+ sx = PAD + sc * CELL_W + CELL_W // 2
455
+ sy = PAD + sr * CELL_H + CELL_H // 2
456
+ dx = PAD + dc * CELL_W + CELL_W // 2
457
+ dy = PAD + dr * CELL_H + CELL_H // 2
458
+ self.canvas.create_line(
459
+ sx, sy, dx, dy, fill=color, width=2,
460
+ arrow=tk.LAST, arrowshape=(8, 10, 4),
461
+ dash=(4, 3), tags="arrow",
462
+ )
463
+ mx, my = (sx + dx) / 2, (sy + dy) / 2
464
+ self.canvas.create_text(
465
+ mx, my - 8, text=p["name"],
466
+ fill=color, font=("Consolas", 7, "bold"),
467
+ tags="arrow",
468
+ )
469
+
470
+ def _cell_color(self, n, routed_nodes):
471
+ if n == 0:
472
+ return CLR_CELL_DEPOT
473
+ if n in routed_nodes:
474
+ return CLR_CELL_ROUTED
475
+ if n == self.pair_src:
476
+ return CLR_CELL_SELB
477
+ if n == self.sel_cell:
478
+ return CLR_CELL_SELB
479
+ if n in self.components:
480
+ return CLR_CELL_COMP
481
+ return CLR_CELL_EMPTY
482
+
483
+ def _recolor_cell(self, n, color=None):
484
+ if n not in self.cell_items:
485
+ return
486
+ rid = self.cell_items[n][0]
487
+ c = color or self._cell_color(n, {})
488
+ self.canvas.itemconfig(rid, fill=c)
489
+
490
+ def _on_cell_hover(self, n, entering):
491
+ if n not in self.cell_items:
492
+ return
493
+ if entering:
494
+ cur = self.canvas.itemcget(self.cell_items[n][0], "fill")
495
+ if cur == CLR_CELL_EMPTY:
496
+ self.canvas.itemconfig(self.cell_items[n][0], fill=CLR_CELL_HOVER)
497
+ else:
498
+ self._recolor_cell(n)
499
+
500
+ # ── Cell Click Logic ──────────────────────────────────────────────────────
501
+
502
+ def _on_cell_click(self, n):
503
+ if self.mode == "edit":
504
+ # deselect previous
505
+ if self.sel_cell is not None:
506
+ self._recolor_cell(self.sel_cell)
507
+ self.sel_cell = n
508
+ self._recolor_cell(n, CLR_CELL_SELB)
509
+ r, c = node_to_rc(n, self.cols)
510
+ name = self.components.get(n, "")
511
+ self.comp_var.set(name)
512
+ self.comp_entry.focus_set()
513
+ if n == 0:
514
+ self.sel_label.config(text=f"Cell ({r},{c}) — depot/origin")
515
+ else:
516
+ self.sel_label.config(
517
+ text=f"Cell ({r},{c}){' · ' + name if name else ' — unnamed'}"
518
+ )
519
+ self._set_status(f"Selected ({r},{c}). Type a name and press Enter or Place.")
520
+
521
+ elif self.mode == "pair":
522
+ if self.pair_src is None:
523
+ # pick source
524
+ self.pair_src = n
525
+ self._recolor_cell(n, CLR_CELL_SELB)
526
+ r, c = node_to_rc(n, self.cols)
527
+ name = self.components.get(n, "DEPOT" if n == 0 else f"node{n}")
528
+ self.pair_status_lbl.config(
529
+ text=f"Source: {name} ({r},{c}). Now pick sink →"
530
+ )
531
+ else:
532
+ if n == self.pair_src:
533
+ self._recolor_cell(n)
534
+ self.pair_src = None
535
+ self.pair_status_lbl.config(text="Source cleared. Pick again.")
536
+ return
537
+ # ask for net name
538
+ default_name = f"NET{len(self.pairs)}"
539
+ net_name = simpledialog.askstring(
540
+ "Net name",
541
+ f"Name for this connection\n(src→sink):",
542
+ initialvalue=default_name,
543
+ parent=self,
544
+ )
545
+ if not net_name:
546
+ net_name = default_name
547
+ net_name = net_name.strip().upper().replace(" ", "_")
548
+
549
+ src_name = self.components.get(self.pair_src, "DEPOT" if self.pair_src == 0 else f"node{self.pair_src}")
550
+ sink_name = self.components.get(n, "DEPOT" if n == 0 else f"node{n}")
551
+
552
+ ci = len(self.pairs) % len(CLR_NET_COLORS)
553
+ self.net_colors[net_name] = CLR_NET_COLORS[ci]
554
+
555
+ self.pairs.append({
556
+ "name": net_name,
557
+ "src": self.pair_src,
558
+ "sink": n,
559
+ "src_name": src_name,
560
+ "sink_name": sink_name,
561
+ })
562
+
563
+ self._recolor_cell(self.pair_src)
564
+ self.pair_src = None
565
+ self.pair_status_lbl.config(text=f"Pair '{net_name}' added. Pick next source →")
566
+ self._refresh_pair_list()
567
+ self._draw_pair_arrows()
568
+ self._set_status(f"Pair '{net_name}' added. {len(self.pairs)} pair(s) total.")
569
+
570
+ # ── Component Editing ─────────────────────────────────────────────────────
571
+
572
+ def _on_place_comp(self):
573
+ if self.sel_cell is None or self.sel_cell == 0:
574
+ self._set_status("Select a non-depot cell first.")
575
+ return
576
+ name = self.comp_var.get().strip()
577
+ if not name:
578
+ self._set_status("Enter a component name first.")
579
+ return
580
+ self.components[self.sel_cell] = name
581
+ # update canvas text
582
+ if self.sel_cell in self.cell_items:
583
+ self.canvas.itemconfig(self.cell_items[self.sel_cell][1], text=name)
584
+ self._recolor_cell(self.sel_cell, CLR_CELL_COMP)
585
+ r, c = node_to_rc(self.sel_cell, self.cols)
586
+ self._set_status(f"Placed '{name}' at ({r},{c}).")
587
+ # also refresh any pairs that reference this node
588
+ for p in self.pairs:
589
+ if p["src"] == self.sel_cell:
590
+ p["src_name"] = name
591
+ if p["sink"] == self.sel_cell:
592
+ p["sink_name"] = name
593
+ self._refresh_pair_list()
594
+
595
+ def _on_clear_comp(self):
596
+ if self.sel_cell is None or self.sel_cell == 0:
597
+ return
598
+ self.components.pop(self.sel_cell, None)
599
+ if self.sel_cell in self.cell_items:
600
+ self.canvas.itemconfig(self.cell_items[self.sel_cell][1], text="")
601
+ self._recolor_cell(self.sel_cell, CLR_CELL_SELB)
602
+ self.comp_var.set("")
603
+ self._set_status("Component cleared.")
604
+
605
+ # ── Pair Mode ─────────────────────────────────────────────────────────────
606
+
607
+ def _toggle_pair_mode(self):
608
+ if self.rows == 0:
609
+ self._set_status("Build a grid first.")
610
+ return
611
+ if self.mode == "edit":
612
+ self.mode = "pair"
613
+ self.pair_mode_btn.config(
614
+ text="✕ EXIT PAIR MODE", bg=CLR_ACCENT2, fg=CLR_BG
615
+ )
616
+ self.pair_status_lbl.config(text="Click a source cell →")
617
+ self._set_status("Pair mode: click source, then sink to create a connection.")
618
+ else:
619
+ self.mode = "edit"
620
+ self.pair_src = None
621
+ self.pair_mode_btn.config(
622
+ text="⛓ ENTER PAIR MODE", bg=CLR_CELL_EMPTY, fg=CLR_ACCENT2
623
+ )
624
+ self.pair_status_lbl.config(text="")
625
+ self._render_grid()
626
+ self._set_status("Back to edit mode.")
627
+
628
+ def _refresh_pair_list(self):
629
+ for w in self.pair_list_frame.winfo_children():
630
+ w.destroy()
631
+ for i, p in enumerate(self.pairs):
632
+ color = self.net_colors.get(p["name"], CLR_ACCENT)
633
+ row = tk.Frame(self.pair_list_frame, bg=CLR_CELL_EMPTY)
634
+ row.pack(fill=tk.X, pady=2)
635
+ tk.Label(row, text="●", bg=CLR_CELL_EMPTY, fg=color,
636
+ font=("Consolas", 9)).pack(side=tk.LEFT, padx=(4, 2))
637
+ tk.Label(row, text=f"{p['name']}: {p['src_name']}→{p['sink_name']}",
638
+ bg=CLR_CELL_EMPTY, fg=CLR_TEXT,
639
+ font=("Consolas", 8), anchor=tk.W).pack(side=tk.LEFT, fill=tk.X, expand=True)
640
+ tk.Button(row, text="✕", bg=CLR_CELL_EMPTY, fg=CLR_DANGER,
641
+ font=("Consolas", 8), relief=tk.FLAT, cursor="hand2",
642
+ command=lambda idx=i: self._remove_pair(idx)).pack(side=tk.RIGHT, padx=2)
643
+
644
+ def _remove_pair(self, idx):
645
+ if 0 <= idx < len(self.pairs):
646
+ self.pairs.pop(idx)
647
+ self._refresh_pair_list()
648
+ self._draw_pair_arrows()
649
+
650
+ # ── Run cuOpt ─────────────────────────────────────────────────────────────
651
+
652
+ def _on_run(self):
653
+ if self.rows == 0:
654
+ messagebox.showerror("No grid", "Build a grid first.")
655
+ return
656
+ if not self.pairs:
657
+ messagebox.showerror("No pairs", "Create at least one wire pair first.")
658
+ return
659
+
660
+ self._set_status("⏳ Building matrices and submitting to cuOpt…")
661
+ self.update()
662
+
663
+ try:
664
+ payload = build_payload(self.rows, self.cols, self.pairs)
665
+ body = call_cuopt(payload)
666
+ self._show_results(body)
667
+ except Exception as e:
668
+ self._set_status(f"ERROR: {e}")
669
+ messagebox.showerror("cuOpt error", str(e))
670
+
671
+ def _show_results(self, body):
672
+ routes = body.get("response", {}).get("solver_response", {})
673
+ vehicle_data = routes.get("vehicle_data", {})
674
+ obj = routes.get("solution_cost", routes.get("total_objective", "—"))
675
+
676
+ routed_nodes = {} # node → list of net names
677
+ route_assignment = {} # node → short label string
678
+
679
+ result_lines = [f"Solver objective: {obj}\n"]
680
+
681
+ for vid, data in vehicle_data.items():
682
+ task_seq = data.get("task_id", [])
683
+ route_nodes = data.get("route", [])
684
+ arrivals = data.get("arrival_stamp", [])
685
+
686
+ stops = []
687
+ for i, t in enumerate(task_seq):
688
+ if str(t) == "Depot":
689
+ continue
690
+ node = route_nodes[i] if i < len(route_nodes) else None
691
+ arr = arrivals[i] if i < len(arrivals) else "?"
692
+ stops.append((str(t), int(node) if node is not None else None, arr))
693
+ if node is not None:
694
+ n = int(node)
695
+ routed_nodes.setdefault(n, []).append(str(t))
696
+
697
+ layer = "M1" if "M1" in str(vid) else "M2"
698
+ result_lines.append(f"── {vid} [{layer}] ({len(stops)} nets)")
699
+ for net_name, node, arr in stops:
700
+ pair = next((p for p in self.pairs if p["name"] == net_name), None)
701
+ src = pair["src"] if pair else 0
702
+ r1, c1 = node_to_rc(src, self.cols)
703
+ r2, c2 = node_to_rc(node, self.cols) if node is not None else ("?", "?")
704
+ slack = "?"
705
+ if pair:
706
+ lat = pair.get("late", self.rows * self.cols + 4)
707
+ slack = f"{lat - float(arr):+.1f}" if arr != "?" else "?"
708
+ result_lines.append(
709
+ f" {net_name:<12} ({r1},{c1})→({r2},{c2}) "
710
+ f"arr={arr if arr != '?' else '?':<5} slack={slack}"
711
+ )
712
+ result_lines.append("")
713
+
714
+ # Build route_assignment label per node
715
+ for node, nets in routed_nodes.items():
716
+ route_assignment[node] = ",".join(nets)
717
+
718
+ self.mode = "result"
719
+ self._render_grid(routed_nodes=set(routed_nodes.keys()),
720
+ route_assignments=route_assignment)
721
+
722
+ # Show result window
723
+ self._show_result_window("\n".join(result_lines))
724
+ self._set_status(f"Routing complete. Objective={obj}. Routed {len(self.pairs)} net(s).")
725
+
726
+ def _show_result_window(self, text):
727
+ win = tk.Toplevel(self)
728
+ win.title("Routing Results")
729
+ win.configure(bg=CLR_BG)
730
+ win.geometry("540x420")
731
+
732
+ tk.Label(win, text="ROUTING RESULTS", bg=CLR_BG, fg=CLR_ACCENT,
733
+ font=("Consolas", 12, "bold")).pack(pady=(14, 4))
734
+
735
+ frame = tk.Frame(win, bg=CLR_BG)
736
+ frame.pack(fill=tk.BOTH, expand=True, padx=14, pady=(0, 14))
737
+
738
+ sb = tk.Scrollbar(frame)
739
+ sb.pack(side=tk.RIGHT, fill=tk.Y)
740
+
741
+ txt = tk.Text(frame, bg=CLR_PANEL, fg=CLR_TEXT, font=("Consolas", 9),
742
+ relief=tk.FLAT, yscrollcommand=sb.set, wrap=tk.NONE)
743
+ txt.pack(fill=tk.BOTH, expand=True)
744
+ sb.config(command=txt.yview)
745
+ txt.insert(tk.END, text)
746
+ txt.config(state=tk.DISABLED)
747
+
748
+ tk.Button(win, text="Close", bg=CLR_ACCENT, fg=CLR_BG,
749
+ font=("Consolas", 9, "bold"), relief=tk.FLAT,
750
+ command=win.destroy, cursor="hand2").pack(pady=(0, 12))
751
+
752
+ # ── Reset ─────────────────────────────────────────────────────────────────
753
+
754
+ def _on_reset(self):
755
+ self.components = {}
756
+ self.pairs = []
757
+ self.sel_cell = None
758
+ self.pair_src = None
759
+ self.mode = "edit"
760
+ self.net_colors = {}
761
+ self.pair_mode_btn.config(text="⛓ ENTER PAIR MODE", bg=CLR_CELL_EMPTY, fg=CLR_ACCENT2)
762
+ self.pair_status_lbl.config(text="")
763
+ self.sel_label.config(text="No cell selected")
764
+ self.comp_var.set("")
765
+ if self.rows:
766
+ self._render_grid()
767
+ self._refresh_pair_list()
768
+ self._set_status("Reset. Place components and re-wire pairs.")
769
+
770
+ # ── Helpers ───────────────────────────────────────────────────────────────
771
+
772
+ def _set_status(self, msg):
773
+ self.status_var.set(f" {msg}")
774
+
775
+
776
+ # ─── Entry Point ──────────────────────────────────────────────────────────────
777
+
778
+ if __name__ == "__main__":
779
+ app = ChipRoutingApp()
780
+ app.mainloop()
chip_routingv3.py ADDED
@@ -0,0 +1,911 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ @Author : Mihir Mithani
5
+ @Date : 08-05-2026 , 10:57
6
+ @File : chip_routingv3.py
7
+ @Desc :
8
+ """
9
+ """
10
+ chip_routing_cuopt.py
11
+ ─────────────────────────────────────────────────────────────────────────────
12
+ Interactive chip routing optimizer with REAL PCB-style routing.
13
+
14
+ Routing engine
15
+ ──────────────
16
+ • Octilinear A* pathfinding — only 90° and 45° turns, like real EDA tools
17
+ • Sequential net routing with incremental blocking so wires NEVER share
18
+ grid edges or cross each other
19
+ • Via dots drawn at every bend
20
+ • Solid lines for orthogonal (90°) hops, dashed for diagonal (45°) hops
21
+ • cuOpt VRP used to find the optimal ORDER to route nets
22
+ (minimises total wire length globally)
23
+
24
+ Usage
25
+ ─────
26
+ pip install requests
27
+ python chip_routing_cuopt.py
28
+
29
+ Set NVIDIA_API_KEY env-var or edit the constant below.
30
+ """
31
+
32
+ import heapq
33
+ import time
34
+ import tkinter as tk
35
+ from tkinter import messagebox, simpledialog
36
+
37
+ import requests
38
+
39
+ import API
40
+
41
+ # ─── API ──────────────────────────────────────────────────────────────────────
42
+ NVIDIA_API_KEY = API.API()
43
+ INVOKE_URL = "https://optimize.api.nvidia.com/v1/nvidia/cuopt"
44
+ FETCH_URL_FMT = "https://optimize.api.nvidia.com/v1/status/{}"
45
+ POLL_INTERVAL = 1.2
46
+ MAX_WAIT = 120
47
+
48
+ HEADERS = {
49
+ "Authorization": f"Bearer {NVIDIA_API_KEY}",
50
+ "Accept": "application/json",
51
+ "Content-Type": "application/json",
52
+ }
53
+
54
+ # ─── Theme ────────────────────────────────────────────────────────────────────
55
+ T = {
56
+ "bg": "#0a0c14",
57
+ "panel": "#10131f",
58
+ "panel2": "#14192a",
59
+ "border": "#1e2440",
60
+ "accent": "#4f6ef7",
61
+ "accent2": "#c084fc",
62
+ "text": "#dde1f5",
63
+ "muted": "#4a5275",
64
+ "cell_empty": "#0e1120",
65
+ "cell_comp": "#0e2040",
66
+ "cell_depot": "#1a0e40",
67
+ "cell_sel": "#0e3020",
68
+ "cell_hover": "#161c38",
69
+ "ok": "#22d3a0",
70
+ "warn": "#facc15",
71
+ "danger": "#f87171",
72
+ }
73
+
74
+ NET_COLORS = [
75
+ "#4f6ef7", "#22d3a0", "#facc15", "#f87171", "#c084fc",
76
+ "#fb923c", "#38bdf8", "#f472b6", "#a3e635", "#e879f9",
77
+ ]
78
+
79
+ CELL_W = 72
80
+ CELL_H = 50
81
+ GPAD = 14
82
+
83
+ # ─── Octilinear A* ────────────────────────────────────────────────────────────
84
+ # 8 directions: N, S, E, W, NE, NW, SE, SW
85
+ DIRS = [
86
+ (0, 1, 1.0),
87
+ (0, -1, 1.0),
88
+ (1, 0, 1.0),
89
+ (-1, 0, 1.0),
90
+ (1, 1, 1.414),
91
+ (1, -1, 1.414),
92
+ (-1, 1, 1.414),
93
+ (-1, -1, 1.414),
94
+ ]
95
+
96
+
97
+ def astar(src_rc, dst_rc, rows, cols, blocked: set, comp_nodes: set):
98
+ """
99
+ Octilinear A* path from src_rc to dst_rc.
100
+ blocked : cells occupied by previously routed wires (interior points)
101
+ comp_nodes : cells containing a component — impassable unless src/dst
102
+ Returns list of (r,c) from src to dst inclusive, or None.
103
+ """
104
+ passable = {src_rc, dst_rc}
105
+ walls = (blocked | comp_nodes) - passable
106
+
107
+ sr, sc = src_rc
108
+ dr, dc = dst_rc
109
+
110
+ def h(r, c):
111
+ return max(abs(r - dr), abs(c - dc)) # Chebyshev — admissible
112
+
113
+ # heap: (f, g, r, c, parent)
114
+ heap = [(h(sr, sc), 0.0, sr, sc, None)]
115
+ came = {}
116
+ gscore = {(sr, sc): 0.0}
117
+
118
+ while heap:
119
+ f, g, r, c, parent = heapq.heappop(heap)
120
+ node = (r, c)
121
+ if node in came:
122
+ continue
123
+ came[node] = parent
124
+
125
+ if node == (dr, dc):
126
+ path = []
127
+ cur = node
128
+ while cur is not None:
129
+ path.append(cur)
130
+ cur = came[cur]
131
+ path.reverse()
132
+ return path
133
+
134
+ for ddr, ddc, cost in DIRS:
135
+ nr, nc = r + ddr, c + ddc
136
+ if not (0 <= nr < rows and 0 <= nc < cols):
137
+ continue
138
+ if (nr, nc) in walls:
139
+ continue
140
+ # diagonal squeeze-through check
141
+ if abs(ddr) == 1 and abs(ddc) == 1:
142
+ if (r + ddr, c) in walls and (r, c + ddc) in walls:
143
+ continue
144
+ ng = g + cost
145
+ if ng < gscore.get((nr, nc), 1e18):
146
+ gscore[(nr, nc)] = ng
147
+ heapq.heappush(heap, (ng + h(nr, nc), ng, nr, nc, node))
148
+
149
+ return None
150
+
151
+
152
+ def route_all_nets(pairs, rows, cols, components, order=None):
153
+ """
154
+ Route nets in the given order using sequential A* with incremental blocking.
155
+ Returns dict: net_name -> list of (r,c)
156
+ """
157
+
158
+ def n2rc(n):
159
+ return (n // cols, n % cols)
160
+
161
+ comp_nodes = {n2rc(n) for n in components}
162
+ blocked = set() # interior cells already used by prior nets
163
+ results = {}
164
+
165
+ if order is None:
166
+ order = list(range(len(pairs)))
167
+
168
+ for idx in order:
169
+ p = pairs[idx]
170
+ src = n2rc(p["src"])
171
+ dst = n2rc(p["sink"])
172
+ path = astar(src, dst, rows, cols, blocked, comp_nodes)
173
+
174
+ if path is None:
175
+ # rip-up fallback: ignore wire blocking, respect only components
176
+ path = astar(src, dst, rows, cols, set(), comp_nodes)
177
+
178
+ results[p["name"]] = path or []
179
+
180
+ if path:
181
+ # block interior cells (not endpoints) for subsequent nets
182
+ for cell in path[1:-1]:
183
+ blocked.add(cell)
184
+
185
+ return results
186
+
187
+
188
+ # ─── cuOpt helpers ────────────────────────────────────────────────────────────
189
+
190
+ def _cost_matrix(rows, cols, layer_id):
191
+ n = rows * cols
192
+ mat = []
193
+ for a in range(n):
194
+ ra, ca = divmod(a, cols)
195
+ row = []
196
+ for b in range(n):
197
+ if a == b:
198
+ row.append(0)
199
+ continue
200
+ rb, cb = divmod(b, cols)
201
+ hd = abs(ca - cb)
202
+ vd = abs(ra - rb)
203
+ pen = vd if layer_id == 1 else hd
204
+ row.append(max(1, hd + vd + pen))
205
+ mat.append(row)
206
+ return mat
207
+
208
+
209
+ def _delay_matrix(rows, cols):
210
+ n = rows * cols
211
+ mat = []
212
+ for a in range(n):
213
+ ra, ca = divmod(a, cols)
214
+ row = []
215
+ for b in range(n):
216
+ if a == b:
217
+ row.append(0)
218
+ else:
219
+ rb, cb = divmod(b, cols)
220
+ row.append(max(1, abs(ra - rb) + abs(ca - cb)))
221
+ mat.append(row)
222
+ return mat
223
+
224
+
225
+ def cuopt_net_order(rows, cols, pairs):
226
+ """
227
+ Call cuOpt to get the optimal routing order for the nets.
228
+ Returns (order: list[int], raw_body: dict).
229
+ Falls back to Manhattan-distance greedy if API fails.
230
+ """
231
+ n_nets = len(pairs)
232
+ max_t = rows * cols + 4
233
+ cap = n_nets + 4
234
+
235
+ payload = {
236
+ "action": "cuOpt_OptimizedRouting",
237
+ "data": {
238
+ "cost_matrix_data": {"data": {"1": _cost_matrix(rows, cols, 1),
239
+ "2": _cost_matrix(rows, cols, 2)}},
240
+ "travel_time_matrix_data": {"data": {"1": _delay_matrix(rows, cols),
241
+ "2": _delay_matrix(rows, cols)}},
242
+ "fleet_data": {
243
+ "vehicle_locations": [[0, 0], [0, 0]],
244
+ "vehicle_ids": ["M1_router", "M2_router"],
245
+ "capacities": [[cap, cap], [cap, cap]],
246
+ "vehicle_time_windows": [[0, max_t], [0, max_t]],
247
+ "vehicle_types": [1, 2],
248
+ "vehicle_max_costs": [rows * cols * 8, rows * cols * 8],
249
+ "vehicle_max_times": [max_t, max_t],
250
+ "skip_first_trips": [False, False],
251
+ "drop_return_trips": [True, True],
252
+ "min_vehicles": 1,
253
+ },
254
+ "task_data": {
255
+ "task_locations": [p["sink"] for p in pairs],
256
+ "task_ids": [p["name"] for p in pairs],
257
+ "demand": [[1] * n_nets, [1] * n_nets],
258
+ "task_time_windows": [[0, max_t]] * n_nets,
259
+ "service_times": [0] * n_nets,
260
+ },
261
+ "solver_config": {
262
+ "time_limit": 5,
263
+ "objectives": {
264
+ "cost": 2,
265
+ "travel_time": 1,
266
+ "variance_route_size": 1,
267
+ "variance_route_service_time": 0,
268
+ "prize": 0,
269
+ },
270
+ "verbose_mode": False,
271
+ "error_logging": True,
272
+ },
273
+ },
274
+ "client_version": "chip_router_v3",
275
+ }
276
+
277
+ session = requests.Session()
278
+ resp = session.post(INVOKE_URL, headers=HEADERS, json=payload, timeout=30)
279
+ elapsed = 0
280
+ while resp.status_code == 202:
281
+ req_id = resp.headers.get("NVCF-REQID", "")
282
+ time.sleep(POLL_INTERVAL)
283
+ elapsed += POLL_INTERVAL
284
+ if elapsed > MAX_WAIT:
285
+ raise TimeoutError("cuOpt timed out")
286
+ resp = session.get(FETCH_URL_FMT.format(req_id), headers=HEADERS, timeout=30)
287
+ resp.raise_for_status()
288
+
289
+ body = resp.json()
290
+ vdata = body.get("response", {}).get("solver_response", {}).get("vehicle_data", {})
291
+ names = []
292
+ for vd in vdata.values():
293
+ for t in vd.get("task_id", []):
294
+ if str(t) != "Depot":
295
+ names.append(str(t))
296
+
297
+ name_to_idx = {p["name"]: i for i, p in enumerate(pairs)}
298
+ order = [name_to_idx[n] for n in names if n in name_to_idx]
299
+ for i in range(len(pairs)):
300
+ if i not in order:
301
+ order.append(i)
302
+ return order, body
303
+
304
+
305
+ # ─── GUI ──────────────────────────────────────────────────────────────────────
306
+
307
+ class App(tk.Tk):
308
+ def __init__(self):
309
+ super().__init__()
310
+ self.title("Chip Routing Optimizer — cuOpt + A*")
311
+ self.configure(bg=T["bg"])
312
+ self.geometry("1220x820")
313
+ self.resizable(True, True)
314
+
315
+ self.rows = 0
316
+ self.cols = 0
317
+ self.components = {} # node_idx -> name
318
+ self.pairs = [] # [{name, src, sink, src_name, sink_name}]
319
+ self.routes = {} # name -> [(r,c),...]
320
+ self.net_colors = {} # name -> color
321
+ self.sel_cell = None
322
+ self.pair_src = None
323
+ self.mode = "edit"
324
+ self.cell_items = {} # node -> (rect, text, coord, sub)
325
+
326
+ self._build_ui()
327
+
328
+ # ── UI build ──────────────────────────────────────────────────────────────
329
+
330
+ def _build_ui(self):
331
+ self.left = tk.Frame(self, bg=T["panel"], width=248)
332
+ self.left.pack(side=tk.LEFT, fill=tk.Y)
333
+ self.left.pack_propagate(False)
334
+ self._build_panel()
335
+
336
+ right = tk.Frame(self, bg=T["bg"])
337
+ right.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
338
+
339
+ cf = tk.Frame(right, bg=T["bg"])
340
+ cf.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
341
+ hs = tk.Scrollbar(cf, orient=tk.HORIZONTAL)
342
+ vs = tk.Scrollbar(cf, orient=tk.VERTICAL)
343
+ self.canvas = tk.Canvas(cf, bg=T["bg"], highlightthickness=0,
344
+ xscrollcommand=hs.set, yscrollcommand=vs.set)
345
+ hs.config(command=self.canvas.xview)
346
+ vs.config(command=self.canvas.yview)
347
+ hs.pack(side=tk.BOTTOM, fill=tk.X)
348
+ vs.pack(side=tk.RIGHT, fill=tk.Y)
349
+ self.canvas.pack(fill=tk.BOTH, expand=True)
350
+
351
+ self.status_var = tk.StringVar(value="Enter grid dimensions and click Build.")
352
+ tk.Label(right, textvariable=self.status_var,
353
+ bg=T["panel2"], fg=T["muted"],
354
+ font=("Courier", 9), anchor=tk.W, padx=10, pady=5
355
+ ).pack(fill=tk.X, side=tk.BOTTOM)
356
+
357
+ def _lbl(self, p, text, fg=None, size=9, bold=False):
358
+ tk.Label(p, text=text, bg=T["panel"],
359
+ fg=fg or T["muted"],
360
+ font=("Courier", size, "bold" if bold else "normal"),
361
+ anchor=tk.W).pack(fill=tk.X, padx=12, pady=(2, 0))
362
+
363
+ def _sep(self, p):
364
+ tk.Frame(p, bg=T["border"], height=1).pack(fill=tk.X, pady=5)
365
+
366
+ def _btn(self, p, text, cmd, bg, fg=None, pady=6):
367
+ tk.Button(p, text=text, font=("Courier", 9, "bold"),
368
+ bg=bg, fg=fg or T["bg"], relief=tk.FLAT,
369
+ activebackground=bg, activeforeground=fg or T["bg"],
370
+ command=cmd, cursor="hand2", pady=pady
371
+ ).pack(fill=tk.X, padx=12, pady=3)
372
+
373
+ def _build_panel(self):
374
+ p = self.left
375
+ tk.Label(p, text="CHIP ROUTER", bg=T["panel"], fg=T["accent"],
376
+ font=("Courier", 12, "bold")).pack(pady=(16, 1))
377
+ tk.Label(p, text="cuOpt order · A* octilinear paths",
378
+ bg=T["panel"], fg=T["muted"], font=("Courier", 7)).pack(pady=(0, 10))
379
+ self._sep(p)
380
+
381
+ # ① Grid
382
+ self._lbl(p, "① GRID SIZE", T["text"], 9, True)
383
+ gf = tk.Frame(p, bg=T["panel"]);
384
+ gf.pack(fill=tk.X, padx=12, pady=4)
385
+ for row_i, (lbl, var_name, default) in enumerate(
386
+ [("Rows", "rows_var", 6), ("Cols", "cols_var", 8)]):
387
+ tk.Label(gf, text=lbl, bg=T["panel"], fg=T["muted"],
388
+ font=("Courier", 8)).grid(row=row_i, column=0, sticky=tk.W, pady=2)
389
+ v = tk.IntVar(value=default)
390
+ setattr(self, var_name, v)
391
+ tk.Spinbox(gf, from_=2, to=20, textvariable=v, width=5,
392
+ bg=T["cell_empty"], fg=T["text"], relief=tk.FLAT,
393
+ insertbackground=T["text"], buttonbackground=T["border"]
394
+ ).grid(row=row_i, column=1, padx=8, pady=2)
395
+ self._btn(p, "▶ BUILD GRID", self._on_build, T["accent"])
396
+
397
+ self._sep(p)
398
+
399
+ # ② Components
400
+ self._lbl(p, "② PLACE COMPONENTS", T["text"], 9, True)
401
+ self._lbl(p, "Click cell → type name → Place")
402
+ ef = tk.Frame(p, bg=T["panel"]);
403
+ ef.pack(fill=tk.X, padx=12, pady=4)
404
+ tk.Label(ef, text="Name:", bg=T["panel"], fg=T["muted"],
405
+ font=("Courier", 8)).pack(side=tk.LEFT)
406
+ self.comp_var = tk.StringVar()
407
+ self.comp_entry = tk.Entry(ef, textvariable=self.comp_var, width=13,
408
+ bg=T["cell_empty"], fg=T["text"], relief=tk.FLAT,
409
+ insertbackground=T["text"], font=("Courier", 9))
410
+ self.comp_entry.pack(side=tk.LEFT, padx=(4, 0))
411
+ self.comp_entry.bind("<Return>", lambda _: self._on_place())
412
+ bf = tk.Frame(p, bg=T["panel"]);
413
+ bf.pack(fill=tk.X, padx=12, pady=(0, 4))
414
+ for txt, cmd, col in [("Place", self._on_place, T["ok"]),
415
+ ("Clear", self._on_clear, T["danger"])]:
416
+ tk.Button(bf, text=txt, font=("Courier", 8), bg=col, fg=T["bg"],
417
+ relief=tk.FLAT, command=cmd, cursor="hand2",
418
+ padx=8, pady=2).pack(side=tk.LEFT, padx=(0, 4))
419
+ self.sel_lbl = tk.Label(p, text="No cell selected",
420
+ bg=T["panel"], fg=T["muted"],
421
+ font=("Courier", 7), anchor=tk.W)
422
+ self.sel_lbl.pack(fill=tk.X, padx=12)
423
+
424
+ self._sep(p)
425
+
426
+ # ③ Pairs
427
+ self._lbl(p, "③ WIRE PAIRS", T["text"], 9, True)
428
+ self._lbl(p, "Toggle mode → click src → click sink")
429
+ self.pair_btn = tk.Button(
430
+ p, text="⛓ ENTER PAIR MODE",
431
+ font=("Courier", 8, "bold"),
432
+ bg=T["cell_empty"], fg=T["accent2"], relief=tk.FLAT,
433
+ activebackground=T["border"], activeforeground=T["accent2"],
434
+ command=self._toggle_pair, cursor="hand2", pady=4)
435
+ self.pair_btn.pack(fill=tk.X, padx=12, pady=4)
436
+ self.pair_hint = tk.Label(p, text="", bg=T["panel"], fg=T["warn"],
437
+ font=("Courier", 7), anchor=tk.W)
438
+ self.pair_hint.pack(fill=tk.X, padx=12)
439
+ self.pair_list_frame = tk.Frame(p, bg=T["panel"])
440
+ self.pair_list_frame.pack(fill=tk.X, padx=12, pady=4)
441
+
442
+ self._sep(p)
443
+
444
+ # ④ Route
445
+ self._lbl(p, "④ ROUTE", T["text"], 9, True)
446
+ self._btn(p, "⚡ RUN CUOPT + A*", self._on_run, T["accent2"])
447
+ self._btn(p, "↺ RESET", self._on_reset, T["muted"])
448
+
449
+ self._sep(p)
450
+ self._lbl(p, "LEGEND", T["muted"], 7, True)
451
+ for label, color in [
452
+ ("Orthogonal wire (90°)", T["accent"]),
453
+ ("Diagonal wire (45°)", T["ok"]),
454
+ ("Via / bend point", T["warn"]),
455
+ ("Component cell", T["cell_comp"]),
456
+ ("Depot / origin", T["cell_depot"]),
457
+ ]:
458
+ lf = tk.Frame(p, bg=T["panel"]);
459
+ lf.pack(fill=tk.X, padx=12, pady=1)
460
+ tk.Canvas(lf, width=10, height=10, bg=color, highlightthickness=0
461
+ ).pack(side=tk.LEFT)
462
+ tk.Label(lf, text=f" {label}", bg=T["panel"], fg=T["muted"],
463
+ font=("Courier", 7)).pack(side=tk.LEFT)
464
+
465
+ # ── Grid draw ─────────────────────────────────────────────────────────────
466
+
467
+ def _render_grid(self):
468
+ self.canvas.delete("all")
469
+ self.cell_items = {}
470
+
471
+ tw = self.cols * CELL_W + GPAD * 2
472
+ th = self.rows * CELL_H + GPAD * 2
473
+ self.canvas.config(scrollregion=(0, 0, tw, th))
474
+
475
+ for r in range(self.rows):
476
+ for c in range(self.cols):
477
+ n = r * self.cols + c
478
+ x0 = GPAD + c * CELL_W
479
+ y0 = GPAD + r * CELL_H
480
+ x1 = x0 + CELL_W - 1
481
+ y1 = y0 + CELL_H - 1
482
+ cx = (x0 + x1) / 2
483
+ cy = (y0 + y1) / 2
484
+
485
+ col = self._cell_bg(n)
486
+ rid = self.canvas.create_rectangle(
487
+ x0, y0, x1, y1, fill=col, outline=T["border"],
488
+ width=1, tags=(f"c{n}", "cell"))
489
+
490
+ lbl = "DEPOT" if n == 0 else self.components.get(n, "")
491
+ lclr = T["accent2"] if n == 0 else (T["accent"] if lbl else T["muted"])
492
+ tid = self.canvas.create_text(
493
+ cx, cy - 3, text=lbl,
494
+ fill=lclr, font=("Courier", 8, "bold"),
495
+ width=CELL_W - 6, anchor=tk.CENTER, tags=(f"c{n}",))
496
+
497
+ cid = self.canvas.create_text(
498
+ x1 - 3, y1 - 3, text=f"{r},{c}",
499
+ fill=T["muted"], font=("Courier", 6),
500
+ anchor=tk.SE, tags=(f"c{n}",))
501
+
502
+ self.cell_items[n] = (rid, tid, cid)
503
+
504
+ for item in (rid, tid, cid):
505
+ self.canvas.tag_bind(item, "<Button-1>",
506
+ lambda e, nd=n: self._click(nd))
507
+ self.canvas.tag_bind(item, "<Enter>",
508
+ lambda e, nd=n: self._hover(nd, True))
509
+ self.canvas.tag_bind(item, "<Leave>",
510
+ lambda e, nd=n: self._hover(nd, False))
511
+
512
+ self._redraw_routes()
513
+
514
+ def _cell_bg(self, n):
515
+ if n == 0: return T["cell_depot"]
516
+ if n == self.pair_src: return "#1a3040"
517
+ if n == self.sel_cell: return T["cell_sel"]
518
+ if n in self.components: return T["cell_comp"]
519
+ return T["cell_empty"]
520
+
521
+ def _recolor(self, n):
522
+ if n in self.cell_items:
523
+ self.canvas.itemconfig(self.cell_items[n][0], fill=self._cell_bg(n))
524
+
525
+ def _hover(self, n, on):
526
+ if n not in self.cell_items:
527
+ return
528
+ cur = self.canvas.itemcget(self.cell_items[n][0], "fill")
529
+ if on and cur == T["cell_empty"]:
530
+ self.canvas.itemconfig(self.cell_items[n][0], fill=T["cell_hover"])
531
+ else:
532
+ self._recolor(n)
533
+
534
+ # ── Route drawing — the key visual part ───────────────────────────────────
535
+
536
+ def _redraw_routes(self):
537
+ self.canvas.delete("route")
538
+ self.canvas.delete("via")
539
+
540
+ for i, p in enumerate(self.pairs):
541
+ name = p["name"]
542
+ color = self.net_colors.get(name, NET_COLORS[i % len(NET_COLORS)])
543
+ path = self.routes.get(name)
544
+
545
+ if path and len(path) >= 2:
546
+ # Draw each hop as a line segment
547
+ for seg in range(len(path) - 1):
548
+ r1, c1 = path[seg]
549
+ r2, c2 = path[seg + 1]
550
+ dr = r2 - r1
551
+ dc = c2 - c1
552
+ is45 = abs(dr) == 1 and abs(dc) == 1
553
+
554
+ px1 = GPAD + c1 * CELL_W + CELL_W // 2
555
+ py1 = GPAD + r1 * CELL_H + CELL_H // 2
556
+ px2 = GPAD + c2 * CELL_W + CELL_W // 2
557
+ py2 = GPAD + r2 * CELL_H + CELL_H // 2
558
+
559
+ # Solid for 90°, short-dash for 45°
560
+ dash = (5, 2) if is45 else ()
561
+ self.canvas.create_line(
562
+ px1, py1, px2, py2,
563
+ fill=color, width=3, dash=dash,
564
+ capstyle=tk.ROUND, joinstyle=tk.ROUND,
565
+ tags="route")
566
+
567
+ # Via dots at every direction change (bend)
568
+ for seg in range(1, len(path) - 1):
569
+ r0, c0 = path[seg - 1]
570
+ r1, c1 = path[seg]
571
+ r2, c2 = path[seg + 1]
572
+ if (r1 - r0, c1 - c0) != (r2 - r1, c2 - c1):
573
+ vx = GPAD + c1 * CELL_W + CELL_W // 2
574
+ vy = GPAD + r1 * CELL_H + CELL_H // 2
575
+ self.canvas.create_oval(
576
+ vx - 5, vy - 5, vx + 5, vy + 5,
577
+ fill=T["warn"], outline=T["bg"], width=1,
578
+ tags="via")
579
+
580
+ # Source terminal (large filled circle)
581
+ sr0, sc0 = path[0]
582
+ sx = GPAD + sc0 * CELL_W + CELL_W // 2
583
+ sy = GPAD + sr0 * CELL_H + CELL_H // 2
584
+ self.canvas.create_oval(sx - 6, sy - 6, sx + 6, sy + 6,
585
+ fill=color, outline=T["bg"], width=1,
586
+ tags="via")
587
+
588
+ # Sink terminal
589
+ er0, ec0 = path[-1]
590
+ ex = GPAD + ec0 * CELL_W + CELL_W // 2
591
+ ey = GPAD + er0 * CELL_H + CELL_H // 2
592
+ self.canvas.create_oval(ex - 6, ey - 6, ex + 6, ey + 6,
593
+ fill=color, outline=T["bg"], width=1,
594
+ tags="via")
595
+ # Arrow head at sink to show direction
596
+ self.canvas.create_oval(ex - 3, ey - 3, ex + 3, ey + 3,
597
+ fill=T["bg"], outline="",
598
+ tags="via")
599
+
600
+ # Net label at midpoint
601
+ mid = len(path) // 2
602
+ mr, mc = path[mid]
603
+ mx = GPAD + mc * CELL_W + CELL_W // 2
604
+ my = GPAD + mr * CELL_H + CELL_H // 2
605
+ self.canvas.create_text(mx, my - 10, text=name,
606
+ fill=color, font=("Courier", 7, "bold"),
607
+ tags="route")
608
+
609
+ else:
610
+ # No routed path yet — draw a dashed preview arrow
611
+ sr, sc = divmod(p["src"], self.cols)
612
+ dr_, dc_ = divmod(p["sink"], self.cols)
613
+ sx = GPAD + sc * CELL_W + CELL_W // 2
614
+ sy = GPAD + sr * CELL_H + CELL_H // 2
615
+ dx = GPAD + dc_ * CELL_W + CELL_W // 2
616
+ dy = GPAD + dr_ * CELL_H + CELL_H // 2
617
+ self.canvas.create_line(
618
+ sx, sy, dx, dy,
619
+ fill=color, width=1, dash=(3, 4),
620
+ arrow=tk.LAST, arrowshape=(7, 9, 3),
621
+ tags="route")
622
+ mx_, my_ = (sx + dx) / 2, (sy + dy) / 2
623
+ self.canvas.create_text(mx_, my_ - 7, text=name,
624
+ fill=color, font=("Courier", 7),
625
+ tags="route")
626
+
627
+ self.canvas.tag_raise("route")
628
+ self.canvas.tag_raise("via")
629
+
630
+ # ── Cell click ────────────────────────────────────────────────────────────
631
+
632
+ def _click(self, n):
633
+ if self.mode == "edit":
634
+ if self.sel_cell is not None:
635
+ self._recolor(self.sel_cell)
636
+ self.sel_cell = n
637
+ self._recolor(n)
638
+ r, c = divmod(n, self.cols)
639
+ nm = self.components.get(n, "")
640
+ self.comp_var.set(nm)
641
+ self.comp_entry.focus_set()
642
+ if n == 0:
643
+ self.sel_lbl.config(text=f"({r},{c}) — depot/origin")
644
+ else:
645
+ self.sel_lbl.config(text=f"({r},{c}) · {nm or 'unnamed'}")
646
+ self._set_status(f"Selected ({r},{c}). Type name + Enter to place.")
647
+
648
+ elif self.mode == "pair":
649
+ if self.pair_src is None:
650
+ self.pair_src = n
651
+ self._recolor(n)
652
+ r, c = divmod(n, self.cols)
653
+ nm = self.components.get(n, "DEPOT" if n == 0 else f"node{n}")
654
+ self.pair_hint.config(text=f"Src: {nm} ({r},{c}) → now pick sink")
655
+ else:
656
+ if n == self.pair_src:
657
+ self._recolor(n)
658
+ self.pair_src = None
659
+ self.pair_hint.config(text="Cleared. Pick source again.")
660
+ return
661
+ def_name = f"NET{len(self.pairs)}"
662
+ net_name = simpledialog.askstring(
663
+ "Net name", "Name for this wire connection:",
664
+ initialvalue=def_name, parent=self)
665
+ if not net_name:
666
+ net_name = def_name
667
+ net_name = net_name.strip().upper().replace(" ", "_")
668
+ src_name = self.components.get(self.pair_src,
669
+ "DEPOT" if self.pair_src == 0 else f"N{self.pair_src}")
670
+ sink_name = self.components.get(n,
671
+ "DEPOT" if n == 0 else f"N{n}")
672
+ ci = len(self.pairs) % len(NET_COLORS)
673
+ self.net_colors[net_name] = NET_COLORS[ci]
674
+ self.pairs.append({"name": net_name, "src": self.pair_src,
675
+ "sink": n, "src_name": src_name,
676
+ "sink_name": sink_name})
677
+ prev = self.pair_src
678
+ self.pair_src = None
679
+ self._recolor(prev)
680
+ self.pair_hint.config(text=f"'{net_name}' added. Pick next src →")
681
+ self._refresh_pairs()
682
+ self._redraw_routes()
683
+ self._set_status(f"Pair '{net_name}' added ({len(self.pairs)} total).")
684
+
685
+ # ── Component actions ─────────────────────────────────────────────────────
686
+
687
+ def _on_place(self):
688
+ if self.sel_cell is None or self.sel_cell == 0:
689
+ self._set_status("Select a non-depot cell first.")
690
+ return
691
+ name = self.comp_var.get().strip()
692
+ if not name:
693
+ self._set_status("Enter a component name.")
694
+ return
695
+ self.components[self.sel_cell] = name
696
+ if self.sel_cell in self.cell_items:
697
+ self.canvas.itemconfig(self.cell_items[self.sel_cell][1],
698
+ text=name, fill=T["accent"])
699
+ self._recolor(self.sel_cell)
700
+ r, c = divmod(self.sel_cell, self.cols)
701
+ self._set_status(f"Placed '{name}' at ({r},{c}).")
702
+ for p in self.pairs:
703
+ if p["src"] == self.sel_cell: p["src_name"] = name
704
+ if p["sink"] == self.sel_cell: p["sink_name"] = name
705
+ self._refresh_pairs()
706
+
707
+ def _on_clear(self):
708
+ if self.sel_cell is None or self.sel_cell == 0:
709
+ return
710
+ self.components.pop(self.sel_cell, None)
711
+ if self.sel_cell in self.cell_items:
712
+ self.canvas.itemconfig(self.cell_items[self.sel_cell][1], text="")
713
+ self._recolor(self.sel_cell)
714
+ self.comp_var.set("")
715
+
716
+ # ── Pair mode ─────────────────────────────────────────────────────────────
717
+
718
+ def _toggle_pair(self):
719
+ if not self.rows:
720
+ self._set_status("Build a grid first.")
721
+ return
722
+ if self.mode == "edit":
723
+ self.mode = "pair"
724
+ self.pair_btn.config(text="✕ EXIT PAIR MODE",
725
+ bg=T["accent2"], fg=T["bg"])
726
+ self.pair_hint.config(text="Click a source cell →")
727
+ self._set_status("Pair mode: click source, then sink to add a wire pair.")
728
+ else:
729
+ self.mode = "edit"
730
+ if self.pair_src is not None:
731
+ self._recolor(self.pair_src)
732
+ self.pair_src = None
733
+ self.pair_btn.config(text="⛓ ENTER PAIR MODE",
734
+ bg=T["cell_empty"], fg=T["accent2"])
735
+ self.pair_hint.config(text="")
736
+ self._set_status("Edit mode.")
737
+
738
+ def _refresh_pairs(self):
739
+ for w in self.pair_list_frame.winfo_children():
740
+ w.destroy()
741
+ for i, p in enumerate(self.pairs):
742
+ color = self.net_colors.get(p["name"], NET_COLORS[i % len(NET_COLORS)])
743
+ row = tk.Frame(self.pair_list_frame, bg=T["panel2"])
744
+ row.pack(fill=tk.X, pady=1)
745
+ tk.Canvas(row, width=8, height=8, bg=color, highlightthickness=0
746
+ ).pack(side=tk.LEFT, padx=(4, 3), pady=3)
747
+ tk.Label(row,
748
+ text=f"{p['name']}: {p['src_name']} → {p['sink_name']}",
749
+ bg=T["panel2"], fg=T["text"],
750
+ font=("Courier", 7), anchor=tk.W
751
+ ).pack(side=tk.LEFT, fill=tk.X, expand=True)
752
+ tk.Button(row, text="✕", bg=T["panel2"], fg=T["danger"],
753
+ font=("Courier", 7), relief=tk.FLAT, cursor="hand2",
754
+ command=lambda idx=i: self._remove_pair(idx)
755
+ ).pack(side=tk.RIGHT, padx=2)
756
+
757
+ def _remove_pair(self, idx):
758
+ if 0 <= idx < len(self.pairs):
759
+ name = self.pairs[idx]["name"]
760
+ self.pairs.pop(idx)
761
+ self.routes.pop(name, None)
762
+ self._refresh_pairs()
763
+ self._redraw_routes()
764
+
765
+ # ── Run routing ───────────────────────────────────────────────────────────
766
+
767
+ def _on_run(self):
768
+ if not self.rows:
769
+ messagebox.showerror("No grid", "Build a grid first.")
770
+ return
771
+ if not self.pairs:
772
+ messagebox.showerror("No pairs", "Add at least one wire pair.")
773
+ return
774
+
775
+ self._set_status("Sending net list to NVIDIA cuOpt to optimise routing order…")
776
+ self.update()
777
+
778
+ try:
779
+ order, cuopt_body = cuopt_net_order(self.rows, self.cols, self.pairs)
780
+ except Exception as e:
781
+ self._set_status(f"cuOpt error: {e} — falling back to greedy order.")
782
+ order = list(range(len(self.pairs)))
783
+ cuopt_body = {}
784
+
785
+ self._set_status(
786
+ f"Running A* octilinear router for {len(self.pairs)} nets…")
787
+ self.update()
788
+
789
+ self.routes = route_all_nets(
790
+ self.pairs, self.rows, self.cols, self.components, order=order)
791
+
792
+ self._render_grid()
793
+
794
+ routed = sum(1 for v in self.routes.values() if v)
795
+ self._set_status(
796
+ f"Done. {routed}/{len(self.pairs)} nets routed. "
797
+ f"Solid = 90°, dashed = 45°, yellow dot = via/bend.")
798
+
799
+ self._show_result_popup()
800
+
801
+ def _show_result_popup(self):
802
+ win = tk.Toplevel(self)
803
+ win.title("Routing Results")
804
+ win.configure(bg=T["bg"])
805
+ win.geometry("580x460")
806
+
807
+ tk.Label(win, text="ROUTING RESULTS", bg=T["bg"], fg=T["accent"],
808
+ font=("Courier", 11, "bold")).pack(pady=(14, 6))
809
+
810
+ frm = tk.Frame(win, bg=T["bg"])
811
+ frm.pack(fill=tk.BOTH, expand=True, padx=14)
812
+ sb = tk.Scrollbar(frm);
813
+ sb.pack(side=tk.RIGHT, fill=tk.Y)
814
+ txt = tk.Text(frm, bg=T["panel"], fg=T["text"], font=("Courier", 8),
815
+ relief=tk.FLAT, yscrollcommand=sb.set)
816
+ txt.pack(fill=tk.BOTH, expand=True)
817
+ sb.config(command=txt.yview)
818
+
819
+ total = 0
820
+ for i, p in enumerate(self.pairs):
821
+ path = self.routes.get(p["name"], [])
822
+ wire_len = len(path) - 1 if path else 0
823
+ total += wire_len
824
+
825
+ # count bends
826
+ bends = 0
827
+ for seg in range(1, len(path) - 1):
828
+ r0, c0 = path[seg - 1]
829
+ r1, c1 = path[seg]
830
+ r2, c2 = path[seg + 1]
831
+ if (r1 - r0, c1 - c0) != (r2 - r1, c2 - c1):
832
+ bends += 1
833
+
834
+ # count 45° hops
835
+ diag45 = sum(
836
+ 1 for s in range(len(path) - 1)
837
+ if abs(path[s][0] - path[s + 1][0]) == 1
838
+ and abs(path[s][1] - path[s + 1][1]) == 1
839
+ )
840
+ ortho = wire_len - diag45
841
+
842
+ status = "ROUTED " if path else "UNROUTED"
843
+ src_rc = divmod(p["src"], self.cols)
844
+ sink_rc = divmod(p["sink"], self.cols)
845
+ line = (
846
+ f"[{status}] {p['name']:<12} "
847
+ f"{p['src_name']:<10} ({src_rc[0]},{src_rc[1]}) "
848
+ f"→ {p['sink_name']:<10} ({sink_rc[0]},{sink_rc[1]})\n"
849
+ f" wire: {wire_len} hops "
850
+ f"({ortho} ortho + {diag45} diag) "
851
+ f"bends: {bends}\n\n"
852
+ )
853
+ txt.insert(tk.END, line)
854
+
855
+ txt.insert(tk.END, f"Total wire length : {total} hops\n")
856
+ txt.config(state=tk.DISABLED)
857
+
858
+ tk.Button(win, text="Close", bg=T["accent"], fg=T["bg"],
859
+ font=("Courier", 9, "bold"), relief=tk.FLAT,
860
+ command=win.destroy, cursor="hand2", pady=6
861
+ ).pack(pady=(8, 14))
862
+
863
+ # ── Build / Reset ─────────────────────────────────────────────────────────
864
+
865
+ def _on_build(self):
866
+ self.rows = max(2, min(20, self.rows_var.get()))
867
+ self.cols = max(2, min(20, self.cols_var.get()))
868
+ self.components = {}
869
+ self.pairs = []
870
+ self.routes = {}
871
+ self.net_colors = {}
872
+ self.sel_cell = None
873
+ self.pair_src = None
874
+ self.mode = "edit"
875
+ self.pair_btn.config(text="⛓ ENTER PAIR MODE",
876
+ bg=T["cell_empty"], fg=T["accent2"])
877
+ self.pair_hint.config(text="")
878
+ self.sel_lbl.config(text="No cell selected")
879
+ self.comp_var.set("")
880
+ self._render_grid()
881
+ self._refresh_pairs()
882
+ self._set_status(
883
+ f"Grid {self.rows}×{self.cols} ready. "
884
+ "Click cells to place components.")
885
+
886
+ def _on_reset(self):
887
+ self.components = {}
888
+ self.pairs = []
889
+ self.routes = {}
890
+ self.net_colors = {}
891
+ self.sel_cell = None
892
+ self.pair_src = None
893
+ self.mode = "edit"
894
+ self.pair_btn.config(text="⛓ ENTER PAIR MODE",
895
+ bg=T["cell_empty"], fg=T["accent2"])
896
+ self.pair_hint.config(text="")
897
+ self.sel_lbl.config(text="No cell selected")
898
+ self.comp_var.set("")
899
+ if self.rows:
900
+ self._render_grid()
901
+ self._refresh_pairs()
902
+ self._set_status("Reset. Place components and create wire pairs.")
903
+
904
+ def _set_status(self, msg):
905
+ self.status_var.set(f" {msg}")
906
+
907
+
908
+ # ─── Entry ────────────────────────────────────────────────────────────────────
909
+
910
+ if __name__ == "__main__":
911
+ App().mainloop()
client.py ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import API
3
+ invoke_url = "https://optimize.api.nvidia.com/v1/nvidia/cuopt"
4
+ fetch_url_format = "https://optimize.api.nvidia.com/v1/status/"
5
+
6
+ API = API.API()
7
+ headers = {
8
+ "Authorization": f"Bearer {API}",
9
+ "Accept": "application/json",
10
+ }
11
+
12
+ payload = {
13
+ "action": "cuOpt_OptimizedRouting",
14
+ "data": {
15
+ "cost_waypoint_graph_data": None,
16
+ "travel_time_waypoint_graph_data": None,
17
+ "cost_matrix_data": {
18
+ "data": {
19
+ "1": [
20
+ [
21
+ 0,
22
+ 1,
23
+ 1
24
+ ],
25
+ [
26
+ 1,
27
+ 0,
28
+ 1
29
+ ],
30
+ [
31
+ 1,
32
+ 1,
33
+ 0
34
+ ]
35
+ ],
36
+ "2": [
37
+ [
38
+ 0,
39
+ 1,
40
+ 1
41
+ ],
42
+ [
43
+ 1,
44
+ 0,
45
+ 1
46
+ ],
47
+ [
48
+ 1,
49
+ 2,
50
+ 0
51
+ ]
52
+ ]
53
+ }
54
+ },
55
+ "travel_time_matrix_data": {
56
+ "data": {
57
+ "1": [
58
+ [
59
+ 0,
60
+ 1,
61
+ 1
62
+ ],
63
+ [
64
+ 1,
65
+ 0,
66
+ 1
67
+ ],
68
+ [
69
+ 1,
70
+ 1,
71
+ 0
72
+ ]
73
+ ],
74
+ "2": [
75
+ [
76
+ 0,
77
+ 1,
78
+ 1
79
+ ],
80
+ [
81
+ 1,
82
+ 0,
83
+ 1
84
+ ],
85
+ [
86
+ 1,
87
+ 2,
88
+ 0
89
+ ]
90
+ ]
91
+ }
92
+ },
93
+ "fleet_data": {
94
+ "vehicle_locations": [
95
+ [
96
+ 0,
97
+ 0
98
+ ],
99
+ [
100
+ 0,
101
+ 0
102
+ ]
103
+ ],
104
+ "vehicle_ids": [
105
+ "veh-1",
106
+ "veh-2"
107
+ ],
108
+ "capacities": [
109
+ [
110
+ 2,
111
+ 2
112
+ ],
113
+ [
114
+ 4,
115
+ 1
116
+ ]
117
+ ],
118
+ "vehicle_time_windows": [
119
+ [
120
+ 0,
121
+ 10
122
+ ],
123
+ [
124
+ 0,
125
+ 10
126
+ ]
127
+ ],
128
+ "vehicle_break_time_windows": [
129
+ [
130
+ [
131
+ 1,
132
+ 2
133
+ ],
134
+ [
135
+ 2,
136
+ 3
137
+ ]
138
+ ]
139
+ ],
140
+ "vehicle_break_durations": [
141
+ [
142
+ 1,
143
+ 1
144
+ ]
145
+ ],
146
+ "vehicle_break_locations": [
147
+ 0,
148
+ 1
149
+ ],
150
+ "vehicle_types": [
151
+ 1,
152
+ 2
153
+ ],
154
+ "vehicle_order_match": [
155
+ {
156
+ "order_ids": [
157
+ 0
158
+ ],
159
+ "vehicle_id": 0
160
+ },
161
+ {
162
+ "order_ids": [
163
+ 1
164
+ ],
165
+ "vehicle_id": 1
166
+ }
167
+ ],
168
+ "skip_first_trips": [
169
+ True,
170
+ False
171
+ ],
172
+ "drop_return_trips": [
173
+ True,
174
+ False
175
+ ],
176
+ "min_vehicles": 2,
177
+ "vehicle_max_costs": [
178
+ 7,
179
+ 10
180
+ ],
181
+ "vehicle_max_times": [
182
+ 7,
183
+ 10
184
+ ]
185
+ },
186
+ "task_data": {
187
+ "task_locations": [
188
+ 1,
189
+ 2
190
+ ],
191
+ "task_ids": [
192
+ "Task-A",
193
+ "Task-B"
194
+ ],
195
+ "demand": [
196
+ [
197
+ 1,
198
+ 1
199
+ ],
200
+ [
201
+ 3,
202
+ 1
203
+ ]
204
+ ],
205
+ "task_time_windows": [
206
+ [
207
+ 0,
208
+ 5
209
+ ],
210
+ [
211
+ 3,
212
+ 9
213
+ ]
214
+ ],
215
+ "service_times": [
216
+ 0,
217
+ 0
218
+ ],
219
+ "order_vehicle_match": [
220
+ {
221
+ "order_id": 0,
222
+ "vehicle_ids": [
223
+ 0
224
+ ]
225
+ },
226
+ {
227
+ "order_id": 1,
228
+ "vehicle_ids": [
229
+ 1
230
+ ]
231
+ }
232
+ ]
233
+ },
234
+ "solver_config": {
235
+ "time_limit": 1,
236
+ "objectives": {
237
+ "cost": 1,
238
+ "travel_time": 0,
239
+ "variance_route_size": 0,
240
+ "variance_route_service_time": 0,
241
+ "prize": 0
242
+ },
243
+ "verbose_mode": False,
244
+ "error_logging": True
245
+ }
246
+ },
247
+ "client_version": ""
248
+ }
249
+
250
+ # re-use connections
251
+ session = requests.Session()
252
+
253
+ response = session.post(invoke_url, headers=headers, json=payload)
254
+
255
+ while response.status_code == 202:
256
+ request_id = response.headers.get("NVCF-REQID")
257
+ fetch_url = fetch_url_format + request_id
258
+ response = session.get(fetch_url, headers=headers)
259
+
260
+ response.raise_for_status()
261
+ response_body = response.json()
262
+ print(response_body)
requirements.txt CHANGED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ requests~=2.33.1
2
+ fastapi~=0.136.1
3
+ uvicorn[standard]
requirenments.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ requests>=2.28.0