Chip_Router / chip_routingv3.py
TechAvenger's picture
Upload 9 files
44b3df5 verified
#!/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()