TechAvenger commited on
Commit
d8a58f5
Β·
verified Β·
1 Parent(s): 79b1cf8

Upload 7 files

Browse files
Files changed (3) hide show
  1. .gitignore +5 -2
  2. Dockerfile +25 -9
  3. app.py +903 -8
.gitignore CHANGED
@@ -1,2 +1,5 @@
1
- API
2
- API.py
 
 
 
 
1
+ .venv/
2
+ __pycache__/
3
+ .idea/
4
+ *.pyc
5
+ .env
Dockerfile CHANGED
@@ -1,16 +1,32 @@
1
- # Read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
2
- # you will also find guides on how best to write your Dockerfile
3
 
4
- FROM python:3.9
 
5
 
 
6
  RUN useradd -m -u 1000 user
7
- USER user
8
- ENV PATH="/home/user/.local/bin:$PATH"
9
 
 
10
  WORKDIR /app
11
 
12
- COPY --chown=user ../requirements.txt requirements.txt
13
- RUN pip install --no-cache-dir --upgrade -r requirements.txt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
- COPY --chown=user .. /app
16
- CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
 
1
+ FROM python:3.9-slim
 
2
 
3
+ # Prevent Python buffering
4
+ ENV PYTHONUNBUFFERED=1
5
 
6
+ # Create non-root user
7
  RUN useradd -m -u 1000 user
 
 
8
 
9
+ # Set working directory
10
  WORKDIR /app
11
 
12
+ # Copy requirements first for better Docker caching
13
+ COPY requirements.txt .
14
+
15
+ # Install dependencies
16
+ RUN pip install --no-cache-dir --upgrade pip && \
17
+ pip install --no-cache-dir -r requirements.txt
18
+
19
+ # Copy project files
20
+ COPY . .
21
+
22
+ # Change ownership
23
+ RUN chown -R user:user /app
24
+
25
+ # Switch to non-root user
26
+ USER user
27
+
28
+ # Expose Hugging Face Spaces port
29
+ EXPOSE 7860
30
 
31
+ # Run application
32
+ CMD ["python", "app.py"]
app.py CHANGED
@@ -2,15 +2,910 @@
2
  # -*- coding: utf-8 -*-
3
  """
4
  @Author : Mihir Mithani
5
- @Date : 05-05-2026 , 12:16
6
- @File : app.py.py
7
- @Desc :
8
  """
9
- from fastapi import FastAPI
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
- app = FastAPI()
12
 
 
13
 
14
- @app.get("/")
15
- def greet_json():
16
- return {"Hello": "World!"}
 
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()