Spaces:
Runtime error
Runtime error
Upload 9 files
Browse files- .gitignore +2 -0
- API +1 -0
- API.py +2 -0
- chip_routing.py +372 -0
- chip_routingv2.py +780 -0
- chip_routingv3.py +911 -0
- client.py +262 -0
- requirements.txt +3 -0
- 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
|