Spaces:
Runtime error
Runtime error
| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| """ | |
| @Author : Mihir Mithani | |
| @Date : 08-05-2026 , 10:57 | |
| @File : chip_routingv3.py | |
| @Desc : | |
| """ | |
| """ | |
| chip_routing_cuopt.py | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| Interactive chip routing optimizer with REAL PCB-style routing. | |
| Routing engine | |
| ββββββββββββββ | |
| β’ Octilinear A* pathfinding β only 90Β° and 45Β° turns, like real EDA tools | |
| β’ Sequential net routing with incremental blocking so wires NEVER share | |
| grid edges or cross each other | |
| β’ Via dots drawn at every bend | |
| β’ Solid lines for orthogonal (90Β°) hops, dashed for diagonal (45Β°) hops | |
| β’ cuOpt VRP used to find the optimal ORDER to route nets | |
| (minimises total wire length globally) | |
| Usage | |
| βββββ | |
| pip install requests | |
| python chip_routing_cuopt.py | |
| Set NVIDIA_API_KEY env-var or edit the constant below. | |
| """ | |
| import heapq | |
| import time | |
| import tkinter as tk | |
| from tkinter import messagebox, simpledialog | |
| import requests | |
| import API | |
| # βββ API ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| NVIDIA_API_KEY = API.API() | |
| INVOKE_URL = "https://optimize.api.nvidia.com/v1/nvidia/cuopt" | |
| FETCH_URL_FMT = "https://optimize.api.nvidia.com/v1/status/{}" | |
| POLL_INTERVAL = 1.2 | |
| MAX_WAIT = 120 | |
| HEADERS = { | |
| "Authorization": f"Bearer {NVIDIA_API_KEY}", | |
| "Accept": "application/json", | |
| "Content-Type": "application/json", | |
| } | |
| # βββ Theme ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| T = { | |
| "bg": "#0a0c14", | |
| "panel": "#10131f", | |
| "panel2": "#14192a", | |
| "border": "#1e2440", | |
| "accent": "#4f6ef7", | |
| "accent2": "#c084fc", | |
| "text": "#dde1f5", | |
| "muted": "#4a5275", | |
| "cell_empty": "#0e1120", | |
| "cell_comp": "#0e2040", | |
| "cell_depot": "#1a0e40", | |
| "cell_sel": "#0e3020", | |
| "cell_hover": "#161c38", | |
| "ok": "#22d3a0", | |
| "warn": "#facc15", | |
| "danger": "#f87171", | |
| } | |
| NET_COLORS = [ | |
| "#4f6ef7", "#22d3a0", "#facc15", "#f87171", "#c084fc", | |
| "#fb923c", "#38bdf8", "#f472b6", "#a3e635", "#e879f9", | |
| ] | |
| CELL_W = 72 | |
| CELL_H = 50 | |
| GPAD = 14 | |
| # βββ Octilinear A* ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 8 directions: N, S, E, W, NE, NW, SE, SW | |
| DIRS = [ | |
| (0, 1, 1.0), | |
| (0, -1, 1.0), | |
| (1, 0, 1.0), | |
| (-1, 0, 1.0), | |
| (1, 1, 1.414), | |
| (1, -1, 1.414), | |
| (-1, 1, 1.414), | |
| (-1, -1, 1.414), | |
| ] | |
| def astar(src_rc, dst_rc, rows, cols, blocked: set, comp_nodes: set): | |
| """ | |
| Octilinear A* path from src_rc to dst_rc. | |
| blocked : cells occupied by previously routed wires (interior points) | |
| comp_nodes : cells containing a component β impassable unless src/dst | |
| Returns list of (r,c) from src to dst inclusive, or None. | |
| """ | |
| passable = {src_rc, dst_rc} | |
| walls = (blocked | comp_nodes) - passable | |
| sr, sc = src_rc | |
| dr, dc = dst_rc | |
| def h(r, c): | |
| return max(abs(r - dr), abs(c - dc)) # Chebyshev β admissible | |
| # heap: (f, g, r, c, parent) | |
| heap = [(h(sr, sc), 0.0, sr, sc, None)] | |
| came = {} | |
| gscore = {(sr, sc): 0.0} | |
| while heap: | |
| f, g, r, c, parent = heapq.heappop(heap) | |
| node = (r, c) | |
| if node in came: | |
| continue | |
| came[node] = parent | |
| if node == (dr, dc): | |
| path = [] | |
| cur = node | |
| while cur is not None: | |
| path.append(cur) | |
| cur = came[cur] | |
| path.reverse() | |
| return path | |
| for ddr, ddc, cost in DIRS: | |
| nr, nc = r + ddr, c + ddc | |
| if not (0 <= nr < rows and 0 <= nc < cols): | |
| continue | |
| if (nr, nc) in walls: | |
| continue | |
| # diagonal squeeze-through check | |
| if abs(ddr) == 1 and abs(ddc) == 1: | |
| if (r + ddr, c) in walls and (r, c + ddc) in walls: | |
| continue | |
| ng = g + cost | |
| if ng < gscore.get((nr, nc), 1e18): | |
| gscore[(nr, nc)] = ng | |
| heapq.heappush(heap, (ng + h(nr, nc), ng, nr, nc, node)) | |
| return None | |
| def route_all_nets(pairs, rows, cols, components, order=None): | |
| """ | |
| Route nets in the given order using sequential A* with incremental blocking. | |
| Returns dict: net_name -> list of (r,c) | |
| """ | |
| def n2rc(n): | |
| return (n // cols, n % cols) | |
| comp_nodes = {n2rc(n) for n in components} | |
| blocked = set() # interior cells already used by prior nets | |
| results = {} | |
| if order is None: | |
| order = list(range(len(pairs))) | |
| for idx in order: | |
| p = pairs[idx] | |
| src = n2rc(p["src"]) | |
| dst = n2rc(p["sink"]) | |
| path = astar(src, dst, rows, cols, blocked, comp_nodes) | |
| if path is None: | |
| # rip-up fallback: ignore wire blocking, respect only components | |
| path = astar(src, dst, rows, cols, set(), comp_nodes) | |
| results[p["name"]] = path or [] | |
| if path: | |
| # block interior cells (not endpoints) for subsequent nets | |
| for cell in path[1:-1]: | |
| blocked.add(cell) | |
| return results | |
| # βββ cuOpt helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _cost_matrix(rows, cols, layer_id): | |
| n = rows * cols | |
| mat = [] | |
| for a in range(n): | |
| ra, ca = divmod(a, cols) | |
| row = [] | |
| for b in range(n): | |
| if a == b: | |
| row.append(0) | |
| continue | |
| rb, cb = divmod(b, cols) | |
| hd = abs(ca - cb) | |
| vd = abs(ra - rb) | |
| pen = vd if layer_id == 1 else hd | |
| row.append(max(1, hd + vd + pen)) | |
| mat.append(row) | |
| return mat | |
| def _delay_matrix(rows, cols): | |
| n = rows * cols | |
| mat = [] | |
| for a in range(n): | |
| ra, ca = divmod(a, cols) | |
| row = [] | |
| for b in range(n): | |
| if a == b: | |
| row.append(0) | |
| else: | |
| rb, cb = divmod(b, cols) | |
| row.append(max(1, abs(ra - rb) + abs(ca - cb))) | |
| mat.append(row) | |
| return mat | |
| def cuopt_net_order(rows, cols, pairs): | |
| """ | |
| Call cuOpt to get the optimal routing order for the nets. | |
| Returns (order: list[int], raw_body: dict). | |
| Falls back to Manhattan-distance greedy if API fails. | |
| """ | |
| n_nets = len(pairs) | |
| max_t = rows * cols + 4 | |
| cap = n_nets + 4 | |
| payload = { | |
| "action": "cuOpt_OptimizedRouting", | |
| "data": { | |
| "cost_matrix_data": {"data": {"1": _cost_matrix(rows, cols, 1), | |
| "2": _cost_matrix(rows, cols, 2)}}, | |
| "travel_time_matrix_data": {"data": {"1": _delay_matrix(rows, cols), | |
| "2": _delay_matrix(rows, cols)}}, | |
| "fleet_data": { | |
| "vehicle_locations": [[0, 0], [0, 0]], | |
| "vehicle_ids": ["M1_router", "M2_router"], | |
| "capacities": [[cap, cap], [cap, cap]], | |
| "vehicle_time_windows": [[0, max_t], [0, max_t]], | |
| "vehicle_types": [1, 2], | |
| "vehicle_max_costs": [rows * cols * 8, rows * cols * 8], | |
| "vehicle_max_times": [max_t, max_t], | |
| "skip_first_trips": [False, False], | |
| "drop_return_trips": [True, True], | |
| "min_vehicles": 1, | |
| }, | |
| "task_data": { | |
| "task_locations": [p["sink"] for p in pairs], | |
| "task_ids": [p["name"] for p in pairs], | |
| "demand": [[1] * n_nets, [1] * n_nets], | |
| "task_time_windows": [[0, max_t]] * n_nets, | |
| "service_times": [0] * n_nets, | |
| }, | |
| "solver_config": { | |
| "time_limit": 5, | |
| "objectives": { | |
| "cost": 2, | |
| "travel_time": 1, | |
| "variance_route_size": 1, | |
| "variance_route_service_time": 0, | |
| "prize": 0, | |
| }, | |
| "verbose_mode": False, | |
| "error_logging": True, | |
| }, | |
| }, | |
| "client_version": "chip_router_v3", | |
| } | |
| session = requests.Session() | |
| resp = session.post(INVOKE_URL, headers=HEADERS, json=payload, timeout=30) | |
| elapsed = 0 | |
| while resp.status_code == 202: | |
| req_id = resp.headers.get("NVCF-REQID", "") | |
| time.sleep(POLL_INTERVAL) | |
| elapsed += POLL_INTERVAL | |
| if elapsed > MAX_WAIT: | |
| raise TimeoutError("cuOpt timed out") | |
| resp = session.get(FETCH_URL_FMT.format(req_id), headers=HEADERS, timeout=30) | |
| resp.raise_for_status() | |
| body = resp.json() | |
| vdata = body.get("response", {}).get("solver_response", {}).get("vehicle_data", {}) | |
| names = [] | |
| for vd in vdata.values(): | |
| for t in vd.get("task_id", []): | |
| if str(t) != "Depot": | |
| names.append(str(t)) | |
| name_to_idx = {p["name"]: i for i, p in enumerate(pairs)} | |
| order = [name_to_idx[n] for n in names if n in name_to_idx] | |
| for i in range(len(pairs)): | |
| if i not in order: | |
| order.append(i) | |
| return order, body | |
| # βββ GUI ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class App(tk.Tk): | |
| def __init__(self): | |
| super().__init__() | |
| self.title("Chip Routing Optimizer β cuOpt + A*") | |
| self.configure(bg=T["bg"]) | |
| self.geometry("1220x820") | |
| self.resizable(True, True) | |
| self.rows = 0 | |
| self.cols = 0 | |
| self.components = {} # node_idx -> name | |
| self.pairs = [] # [{name, src, sink, src_name, sink_name}] | |
| self.routes = {} # name -> [(r,c),...] | |
| self.net_colors = {} # name -> color | |
| self.sel_cell = None | |
| self.pair_src = None | |
| self.mode = "edit" | |
| self.cell_items = {} # node -> (rect, text, coord, sub) | |
| self._build_ui() | |
| # ββ UI build ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _build_ui(self): | |
| self.left = tk.Frame(self, bg=T["panel"], width=248) | |
| self.left.pack(side=tk.LEFT, fill=tk.Y) | |
| self.left.pack_propagate(False) | |
| self._build_panel() | |
| right = tk.Frame(self, bg=T["bg"]) | |
| right.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) | |
| cf = tk.Frame(right, bg=T["bg"]) | |
| cf.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) | |
| hs = tk.Scrollbar(cf, orient=tk.HORIZONTAL) | |
| vs = tk.Scrollbar(cf, orient=tk.VERTICAL) | |
| self.canvas = tk.Canvas(cf, bg=T["bg"], highlightthickness=0, | |
| xscrollcommand=hs.set, yscrollcommand=vs.set) | |
| hs.config(command=self.canvas.xview) | |
| vs.config(command=self.canvas.yview) | |
| hs.pack(side=tk.BOTTOM, fill=tk.X) | |
| vs.pack(side=tk.RIGHT, fill=tk.Y) | |
| self.canvas.pack(fill=tk.BOTH, expand=True) | |
| self.status_var = tk.StringVar(value="Enter grid dimensions and click Build.") | |
| tk.Label(right, textvariable=self.status_var, | |
| bg=T["panel2"], fg=T["muted"], | |
| font=("Courier", 9), anchor=tk.W, padx=10, pady=5 | |
| ).pack(fill=tk.X, side=tk.BOTTOM) | |
| def _lbl(self, p, text, fg=None, size=9, bold=False): | |
| tk.Label(p, text=text, bg=T["panel"], | |
| fg=fg or T["muted"], | |
| font=("Courier", size, "bold" if bold else "normal"), | |
| anchor=tk.W).pack(fill=tk.X, padx=12, pady=(2, 0)) | |
| def _sep(self, p): | |
| tk.Frame(p, bg=T["border"], height=1).pack(fill=tk.X, pady=5) | |
| def _btn(self, p, text, cmd, bg, fg=None, pady=6): | |
| tk.Button(p, text=text, font=("Courier", 9, "bold"), | |
| bg=bg, fg=fg or T["bg"], relief=tk.FLAT, | |
| activebackground=bg, activeforeground=fg or T["bg"], | |
| command=cmd, cursor="hand2", pady=pady | |
| ).pack(fill=tk.X, padx=12, pady=3) | |
| def _build_panel(self): | |
| p = self.left | |
| tk.Label(p, text="CHIP ROUTER", bg=T["panel"], fg=T["accent"], | |
| font=("Courier", 12, "bold")).pack(pady=(16, 1)) | |
| tk.Label(p, text="cuOpt order Β· A* octilinear paths", | |
| bg=T["panel"], fg=T["muted"], font=("Courier", 7)).pack(pady=(0, 10)) | |
| self._sep(p) | |
| # β Grid | |
| self._lbl(p, "β GRID SIZE", T["text"], 9, True) | |
| gf = tk.Frame(p, bg=T["panel"]); | |
| gf.pack(fill=tk.X, padx=12, pady=4) | |
| for row_i, (lbl, var_name, default) in enumerate( | |
| [("Rows", "rows_var", 6), ("Cols", "cols_var", 8)]): | |
| tk.Label(gf, text=lbl, bg=T["panel"], fg=T["muted"], | |
| font=("Courier", 8)).grid(row=row_i, column=0, sticky=tk.W, pady=2) | |
| v = tk.IntVar(value=default) | |
| setattr(self, var_name, v) | |
| tk.Spinbox(gf, from_=2, to=20, textvariable=v, width=5, | |
| bg=T["cell_empty"], fg=T["text"], relief=tk.FLAT, | |
| insertbackground=T["text"], buttonbackground=T["border"] | |
| ).grid(row=row_i, column=1, padx=8, pady=2) | |
| self._btn(p, "βΆ BUILD GRID", self._on_build, T["accent"]) | |
| self._sep(p) | |
| # β‘ Components | |
| self._lbl(p, "β‘ PLACE COMPONENTS", T["text"], 9, True) | |
| self._lbl(p, "Click cell β type name β Place") | |
| ef = tk.Frame(p, bg=T["panel"]); | |
| ef.pack(fill=tk.X, padx=12, pady=4) | |
| tk.Label(ef, text="Name:", bg=T["panel"], fg=T["muted"], | |
| font=("Courier", 8)).pack(side=tk.LEFT) | |
| self.comp_var = tk.StringVar() | |
| self.comp_entry = tk.Entry(ef, textvariable=self.comp_var, width=13, | |
| bg=T["cell_empty"], fg=T["text"], relief=tk.FLAT, | |
| insertbackground=T["text"], font=("Courier", 9)) | |
| self.comp_entry.pack(side=tk.LEFT, padx=(4, 0)) | |
| self.comp_entry.bind("<Return>", lambda _: self._on_place()) | |
| bf = tk.Frame(p, bg=T["panel"]); | |
| bf.pack(fill=tk.X, padx=12, pady=(0, 4)) | |
| for txt, cmd, col in [("Place", self._on_place, T["ok"]), | |
| ("Clear", self._on_clear, T["danger"])]: | |
| tk.Button(bf, text=txt, font=("Courier", 8), bg=col, fg=T["bg"], | |
| relief=tk.FLAT, command=cmd, cursor="hand2", | |
| padx=8, pady=2).pack(side=tk.LEFT, padx=(0, 4)) | |
| self.sel_lbl = tk.Label(p, text="No cell selected", | |
| bg=T["panel"], fg=T["muted"], | |
| font=("Courier", 7), anchor=tk.W) | |
| self.sel_lbl.pack(fill=tk.X, padx=12) | |
| self._sep(p) | |
| # β’ Pairs | |
| self._lbl(p, "β’ WIRE PAIRS", T["text"], 9, True) | |
| self._lbl(p, "Toggle mode β click src β click sink") | |
| self.pair_btn = tk.Button( | |
| p, text="β ENTER PAIR MODE", | |
| font=("Courier", 8, "bold"), | |
| bg=T["cell_empty"], fg=T["accent2"], relief=tk.FLAT, | |
| activebackground=T["border"], activeforeground=T["accent2"], | |
| command=self._toggle_pair, cursor="hand2", pady=4) | |
| self.pair_btn.pack(fill=tk.X, padx=12, pady=4) | |
| self.pair_hint = tk.Label(p, text="", bg=T["panel"], fg=T["warn"], | |
| font=("Courier", 7), anchor=tk.W) | |
| self.pair_hint.pack(fill=tk.X, padx=12) | |
| self.pair_list_frame = tk.Frame(p, bg=T["panel"]) | |
| self.pair_list_frame.pack(fill=tk.X, padx=12, pady=4) | |
| self._sep(p) | |
| # β£ Route | |
| self._lbl(p, "β£ ROUTE", T["text"], 9, True) | |
| self._btn(p, "β‘ RUN CUOPT + A*", self._on_run, T["accent2"]) | |
| self._btn(p, "βΊ RESET", self._on_reset, T["muted"]) | |
| self._sep(p) | |
| self._lbl(p, "LEGEND", T["muted"], 7, True) | |
| for label, color in [ | |
| ("Orthogonal wire (90Β°)", T["accent"]), | |
| ("Diagonal wire (45Β°)", T["ok"]), | |
| ("Via / bend point", T["warn"]), | |
| ("Component cell", T["cell_comp"]), | |
| ("Depot / origin", T["cell_depot"]), | |
| ]: | |
| lf = tk.Frame(p, bg=T["panel"]); | |
| lf.pack(fill=tk.X, padx=12, pady=1) | |
| tk.Canvas(lf, width=10, height=10, bg=color, highlightthickness=0 | |
| ).pack(side=tk.LEFT) | |
| tk.Label(lf, text=f" {label}", bg=T["panel"], fg=T["muted"], | |
| font=("Courier", 7)).pack(side=tk.LEFT) | |
| # ββ Grid draw βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _render_grid(self): | |
| self.canvas.delete("all") | |
| self.cell_items = {} | |
| tw = self.cols * CELL_W + GPAD * 2 | |
| th = self.rows * CELL_H + GPAD * 2 | |
| self.canvas.config(scrollregion=(0, 0, tw, th)) | |
| for r in range(self.rows): | |
| for c in range(self.cols): | |
| n = r * self.cols + c | |
| x0 = GPAD + c * CELL_W | |
| y0 = GPAD + r * CELL_H | |
| x1 = x0 + CELL_W - 1 | |
| y1 = y0 + CELL_H - 1 | |
| cx = (x0 + x1) / 2 | |
| cy = (y0 + y1) / 2 | |
| col = self._cell_bg(n) | |
| rid = self.canvas.create_rectangle( | |
| x0, y0, x1, y1, fill=col, outline=T["border"], | |
| width=1, tags=(f"c{n}", "cell")) | |
| lbl = "DEPOT" if n == 0 else self.components.get(n, "") | |
| lclr = T["accent2"] if n == 0 else (T["accent"] if lbl else T["muted"]) | |
| tid = self.canvas.create_text( | |
| cx, cy - 3, text=lbl, | |
| fill=lclr, font=("Courier", 8, "bold"), | |
| width=CELL_W - 6, anchor=tk.CENTER, tags=(f"c{n}",)) | |
| cid = self.canvas.create_text( | |
| x1 - 3, y1 - 3, text=f"{r},{c}", | |
| fill=T["muted"], font=("Courier", 6), | |
| anchor=tk.SE, tags=(f"c{n}",)) | |
| self.cell_items[n] = (rid, tid, cid) | |
| for item in (rid, tid, cid): | |
| self.canvas.tag_bind(item, "<Button-1>", | |
| lambda e, nd=n: self._click(nd)) | |
| self.canvas.tag_bind(item, "<Enter>", | |
| lambda e, nd=n: self._hover(nd, True)) | |
| self.canvas.tag_bind(item, "<Leave>", | |
| lambda e, nd=n: self._hover(nd, False)) | |
| self._redraw_routes() | |
| def _cell_bg(self, n): | |
| if n == 0: return T["cell_depot"] | |
| if n == self.pair_src: return "#1a3040" | |
| if n == self.sel_cell: return T["cell_sel"] | |
| if n in self.components: return T["cell_comp"] | |
| return T["cell_empty"] | |
| def _recolor(self, n): | |
| if n in self.cell_items: | |
| self.canvas.itemconfig(self.cell_items[n][0], fill=self._cell_bg(n)) | |
| def _hover(self, n, on): | |
| if n not in self.cell_items: | |
| return | |
| cur = self.canvas.itemcget(self.cell_items[n][0], "fill") | |
| if on and cur == T["cell_empty"]: | |
| self.canvas.itemconfig(self.cell_items[n][0], fill=T["cell_hover"]) | |
| else: | |
| self._recolor(n) | |
| # ββ Route drawing β the key visual part βββββββββββββββββββββββββββββββββββ | |
| def _redraw_routes(self): | |
| self.canvas.delete("route") | |
| self.canvas.delete("via") | |
| for i, p in enumerate(self.pairs): | |
| name = p["name"] | |
| color = self.net_colors.get(name, NET_COLORS[i % len(NET_COLORS)]) | |
| path = self.routes.get(name) | |
| if path and len(path) >= 2: | |
| # Draw each hop as a line segment | |
| for seg in range(len(path) - 1): | |
| r1, c1 = path[seg] | |
| r2, c2 = path[seg + 1] | |
| dr = r2 - r1 | |
| dc = c2 - c1 | |
| is45 = abs(dr) == 1 and abs(dc) == 1 | |
| px1 = GPAD + c1 * CELL_W + CELL_W // 2 | |
| py1 = GPAD + r1 * CELL_H + CELL_H // 2 | |
| px2 = GPAD + c2 * CELL_W + CELL_W // 2 | |
| py2 = GPAD + r2 * CELL_H + CELL_H // 2 | |
| # Solid for 90Β°, short-dash for 45Β° | |
| dash = (5, 2) if is45 else () | |
| self.canvas.create_line( | |
| px1, py1, px2, py2, | |
| fill=color, width=3, dash=dash, | |
| capstyle=tk.ROUND, joinstyle=tk.ROUND, | |
| tags="route") | |
| # Via dots at every direction change (bend) | |
| for seg in range(1, len(path) - 1): | |
| r0, c0 = path[seg - 1] | |
| r1, c1 = path[seg] | |
| r2, c2 = path[seg + 1] | |
| if (r1 - r0, c1 - c0) != (r2 - r1, c2 - c1): | |
| vx = GPAD + c1 * CELL_W + CELL_W // 2 | |
| vy = GPAD + r1 * CELL_H + CELL_H // 2 | |
| self.canvas.create_oval( | |
| vx - 5, vy - 5, vx + 5, vy + 5, | |
| fill=T["warn"], outline=T["bg"], width=1, | |
| tags="via") | |
| # Source terminal (large filled circle) | |
| sr0, sc0 = path[0] | |
| sx = GPAD + sc0 * CELL_W + CELL_W // 2 | |
| sy = GPAD + sr0 * CELL_H + CELL_H // 2 | |
| self.canvas.create_oval(sx - 6, sy - 6, sx + 6, sy + 6, | |
| fill=color, outline=T["bg"], width=1, | |
| tags="via") | |
| # Sink terminal | |
| er0, ec0 = path[-1] | |
| ex = GPAD + ec0 * CELL_W + CELL_W // 2 | |
| ey = GPAD + er0 * CELL_H + CELL_H // 2 | |
| self.canvas.create_oval(ex - 6, ey - 6, ex + 6, ey + 6, | |
| fill=color, outline=T["bg"], width=1, | |
| tags="via") | |
| # Arrow head at sink to show direction | |
| self.canvas.create_oval(ex - 3, ey - 3, ex + 3, ey + 3, | |
| fill=T["bg"], outline="", | |
| tags="via") | |
| # Net label at midpoint | |
| mid = len(path) // 2 | |
| mr, mc = path[mid] | |
| mx = GPAD + mc * CELL_W + CELL_W // 2 | |
| my = GPAD + mr * CELL_H + CELL_H // 2 | |
| self.canvas.create_text(mx, my - 10, text=name, | |
| fill=color, font=("Courier", 7, "bold"), | |
| tags="route") | |
| else: | |
| # No routed path yet β draw a dashed preview arrow | |
| sr, sc = divmod(p["src"], self.cols) | |
| dr_, dc_ = divmod(p["sink"], self.cols) | |
| sx = GPAD + sc * CELL_W + CELL_W // 2 | |
| sy = GPAD + sr * CELL_H + CELL_H // 2 | |
| dx = GPAD + dc_ * CELL_W + CELL_W // 2 | |
| dy = GPAD + dr_ * CELL_H + CELL_H // 2 | |
| self.canvas.create_line( | |
| sx, sy, dx, dy, | |
| fill=color, width=1, dash=(3, 4), | |
| arrow=tk.LAST, arrowshape=(7, 9, 3), | |
| tags="route") | |
| mx_, my_ = (sx + dx) / 2, (sy + dy) / 2 | |
| self.canvas.create_text(mx_, my_ - 7, text=name, | |
| fill=color, font=("Courier", 7), | |
| tags="route") | |
| self.canvas.tag_raise("route") | |
| self.canvas.tag_raise("via") | |
| # ββ Cell click ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _click(self, n): | |
| if self.mode == "edit": | |
| if self.sel_cell is not None: | |
| self._recolor(self.sel_cell) | |
| self.sel_cell = n | |
| self._recolor(n) | |
| r, c = divmod(n, self.cols) | |
| nm = self.components.get(n, "") | |
| self.comp_var.set(nm) | |
| self.comp_entry.focus_set() | |
| if n == 0: | |
| self.sel_lbl.config(text=f"({r},{c}) β depot/origin") | |
| else: | |
| self.sel_lbl.config(text=f"({r},{c}) Β· {nm or 'unnamed'}") | |
| self._set_status(f"Selected ({r},{c}). Type name + Enter to place.") | |
| elif self.mode == "pair": | |
| if self.pair_src is None: | |
| self.pair_src = n | |
| self._recolor(n) | |
| r, c = divmod(n, self.cols) | |
| nm = self.components.get(n, "DEPOT" if n == 0 else f"node{n}") | |
| self.pair_hint.config(text=f"Src: {nm} ({r},{c}) β now pick sink") | |
| else: | |
| if n == self.pair_src: | |
| self._recolor(n) | |
| self.pair_src = None | |
| self.pair_hint.config(text="Cleared. Pick source again.") | |
| return | |
| def_name = f"NET{len(self.pairs)}" | |
| net_name = simpledialog.askstring( | |
| "Net name", "Name for this wire connection:", | |
| initialvalue=def_name, parent=self) | |
| if not net_name: | |
| net_name = def_name | |
| net_name = net_name.strip().upper().replace(" ", "_") | |
| src_name = self.components.get(self.pair_src, | |
| "DEPOT" if self.pair_src == 0 else f"N{self.pair_src}") | |
| sink_name = self.components.get(n, | |
| "DEPOT" if n == 0 else f"N{n}") | |
| ci = len(self.pairs) % len(NET_COLORS) | |
| self.net_colors[net_name] = NET_COLORS[ci] | |
| self.pairs.append({"name": net_name, "src": self.pair_src, | |
| "sink": n, "src_name": src_name, | |
| "sink_name": sink_name}) | |
| prev = self.pair_src | |
| self.pair_src = None | |
| self._recolor(prev) | |
| self.pair_hint.config(text=f"'{net_name}' added. Pick next src β") | |
| self._refresh_pairs() | |
| self._redraw_routes() | |
| self._set_status(f"Pair '{net_name}' added ({len(self.pairs)} total).") | |
| # ββ Component actions βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _on_place(self): | |
| if self.sel_cell is None or self.sel_cell == 0: | |
| self._set_status("Select a non-depot cell first.") | |
| return | |
| name = self.comp_var.get().strip() | |
| if not name: | |
| self._set_status("Enter a component name.") | |
| return | |
| self.components[self.sel_cell] = name | |
| if self.sel_cell in self.cell_items: | |
| self.canvas.itemconfig(self.cell_items[self.sel_cell][1], | |
| text=name, fill=T["accent"]) | |
| self._recolor(self.sel_cell) | |
| r, c = divmod(self.sel_cell, self.cols) | |
| self._set_status(f"Placed '{name}' at ({r},{c}).") | |
| for p in self.pairs: | |
| if p["src"] == self.sel_cell: p["src_name"] = name | |
| if p["sink"] == self.sel_cell: p["sink_name"] = name | |
| self._refresh_pairs() | |
| def _on_clear(self): | |
| if self.sel_cell is None or self.sel_cell == 0: | |
| return | |
| self.components.pop(self.sel_cell, None) | |
| if self.sel_cell in self.cell_items: | |
| self.canvas.itemconfig(self.cell_items[self.sel_cell][1], text="") | |
| self._recolor(self.sel_cell) | |
| self.comp_var.set("") | |
| # ββ Pair mode βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _toggle_pair(self): | |
| if not self.rows: | |
| self._set_status("Build a grid first.") | |
| return | |
| if self.mode == "edit": | |
| self.mode = "pair" | |
| self.pair_btn.config(text="β EXIT PAIR MODE", | |
| bg=T["accent2"], fg=T["bg"]) | |
| self.pair_hint.config(text="Click a source cell β") | |
| self._set_status("Pair mode: click source, then sink to add a wire pair.") | |
| else: | |
| self.mode = "edit" | |
| if self.pair_src is not None: | |
| self._recolor(self.pair_src) | |
| self.pair_src = None | |
| self.pair_btn.config(text="β ENTER PAIR MODE", | |
| bg=T["cell_empty"], fg=T["accent2"]) | |
| self.pair_hint.config(text="") | |
| self._set_status("Edit mode.") | |
| def _refresh_pairs(self): | |
| for w in self.pair_list_frame.winfo_children(): | |
| w.destroy() | |
| for i, p in enumerate(self.pairs): | |
| color = self.net_colors.get(p["name"], NET_COLORS[i % len(NET_COLORS)]) | |
| row = tk.Frame(self.pair_list_frame, bg=T["panel2"]) | |
| row.pack(fill=tk.X, pady=1) | |
| tk.Canvas(row, width=8, height=8, bg=color, highlightthickness=0 | |
| ).pack(side=tk.LEFT, padx=(4, 3), pady=3) | |
| tk.Label(row, | |
| text=f"{p['name']}: {p['src_name']} β {p['sink_name']}", | |
| bg=T["panel2"], fg=T["text"], | |
| font=("Courier", 7), anchor=tk.W | |
| ).pack(side=tk.LEFT, fill=tk.X, expand=True) | |
| tk.Button(row, text="β", bg=T["panel2"], fg=T["danger"], | |
| font=("Courier", 7), relief=tk.FLAT, cursor="hand2", | |
| command=lambda idx=i: self._remove_pair(idx) | |
| ).pack(side=tk.RIGHT, padx=2) | |
| def _remove_pair(self, idx): | |
| if 0 <= idx < len(self.pairs): | |
| name = self.pairs[idx]["name"] | |
| self.pairs.pop(idx) | |
| self.routes.pop(name, None) | |
| self._refresh_pairs() | |
| self._redraw_routes() | |
| # ββ Run routing βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _on_run(self): | |
| if not self.rows: | |
| messagebox.showerror("No grid", "Build a grid first.") | |
| return | |
| if not self.pairs: | |
| messagebox.showerror("No pairs", "Add at least one wire pair.") | |
| return | |
| self._set_status("Sending net list to NVIDIA cuOpt to optimise routing orderβ¦") | |
| self.update() | |
| try: | |
| order, cuopt_body = cuopt_net_order(self.rows, self.cols, self.pairs) | |
| except Exception as e: | |
| self._set_status(f"cuOpt error: {e} β falling back to greedy order.") | |
| order = list(range(len(self.pairs))) | |
| cuopt_body = {} | |
| self._set_status( | |
| f"Running A* octilinear router for {len(self.pairs)} netsβ¦") | |
| self.update() | |
| self.routes = route_all_nets( | |
| self.pairs, self.rows, self.cols, self.components, order=order) | |
| self._render_grid() | |
| routed = sum(1 for v in self.routes.values() if v) | |
| self._set_status( | |
| f"Done. {routed}/{len(self.pairs)} nets routed. " | |
| f"Solid = 90Β°, dashed = 45Β°, yellow dot = via/bend.") | |
| self._show_result_popup() | |
| def _show_result_popup(self): | |
| win = tk.Toplevel(self) | |
| win.title("Routing Results") | |
| win.configure(bg=T["bg"]) | |
| win.geometry("580x460") | |
| tk.Label(win, text="ROUTING RESULTS", bg=T["bg"], fg=T["accent"], | |
| font=("Courier", 11, "bold")).pack(pady=(14, 6)) | |
| frm = tk.Frame(win, bg=T["bg"]) | |
| frm.pack(fill=tk.BOTH, expand=True, padx=14) | |
| sb = tk.Scrollbar(frm); | |
| sb.pack(side=tk.RIGHT, fill=tk.Y) | |
| txt = tk.Text(frm, bg=T["panel"], fg=T["text"], font=("Courier", 8), | |
| relief=tk.FLAT, yscrollcommand=sb.set) | |
| txt.pack(fill=tk.BOTH, expand=True) | |
| sb.config(command=txt.yview) | |
| total = 0 | |
| for i, p in enumerate(self.pairs): | |
| path = self.routes.get(p["name"], []) | |
| wire_len = len(path) - 1 if path else 0 | |
| total += wire_len | |
| # count bends | |
| bends = 0 | |
| for seg in range(1, len(path) - 1): | |
| r0, c0 = path[seg - 1] | |
| r1, c1 = path[seg] | |
| r2, c2 = path[seg + 1] | |
| if (r1 - r0, c1 - c0) != (r2 - r1, c2 - c1): | |
| bends += 1 | |
| # count 45Β° hops | |
| diag45 = sum( | |
| 1 for s in range(len(path) - 1) | |
| if abs(path[s][0] - path[s + 1][0]) == 1 | |
| and abs(path[s][1] - path[s + 1][1]) == 1 | |
| ) | |
| ortho = wire_len - diag45 | |
| status = "ROUTED " if path else "UNROUTED" | |
| src_rc = divmod(p["src"], self.cols) | |
| sink_rc = divmod(p["sink"], self.cols) | |
| line = ( | |
| f"[{status}] {p['name']:<12} " | |
| f"{p['src_name']:<10} ({src_rc[0]},{src_rc[1]}) " | |
| f"β {p['sink_name']:<10} ({sink_rc[0]},{sink_rc[1]})\n" | |
| f" wire: {wire_len} hops " | |
| f"({ortho} ortho + {diag45} diag) " | |
| f"bends: {bends}\n\n" | |
| ) | |
| txt.insert(tk.END, line) | |
| txt.insert(tk.END, f"Total wire length : {total} hops\n") | |
| txt.config(state=tk.DISABLED) | |
| tk.Button(win, text="Close", bg=T["accent"], fg=T["bg"], | |
| font=("Courier", 9, "bold"), relief=tk.FLAT, | |
| command=win.destroy, cursor="hand2", pady=6 | |
| ).pack(pady=(8, 14)) | |
| # ββ Build / Reset βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _on_build(self): | |
| self.rows = max(2, min(20, self.rows_var.get())) | |
| self.cols = max(2, min(20, self.cols_var.get())) | |
| self.components = {} | |
| self.pairs = [] | |
| self.routes = {} | |
| self.net_colors = {} | |
| self.sel_cell = None | |
| self.pair_src = None | |
| self.mode = "edit" | |
| self.pair_btn.config(text="β ENTER PAIR MODE", | |
| bg=T["cell_empty"], fg=T["accent2"]) | |
| self.pair_hint.config(text="") | |
| self.sel_lbl.config(text="No cell selected") | |
| self.comp_var.set("") | |
| self._render_grid() | |
| self._refresh_pairs() | |
| self._set_status( | |
| f"Grid {self.rows}Γ{self.cols} ready. " | |
| "Click cells to place components.") | |
| def _on_reset(self): | |
| self.components = {} | |
| self.pairs = [] | |
| self.routes = {} | |
| self.net_colors = {} | |
| self.sel_cell = None | |
| self.pair_src = None | |
| self.mode = "edit" | |
| self.pair_btn.config(text="β ENTER PAIR MODE", | |
| bg=T["cell_empty"], fg=T["accent2"]) | |
| self.pair_hint.config(text="") | |
| self.sel_lbl.config(text="No cell selected") | |
| self.comp_var.set("") | |
| if self.rows: | |
| self._render_grid() | |
| self._refresh_pairs() | |
| self._set_status("Reset. Place components and create wire pairs.") | |
| def _set_status(self, msg): | |
| self.status_var.set(f" {msg}") | |
| # βββ Entry ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if __name__ == "__main__": | |
| App().mainloop() | |