Spaces:
Runtime error
Runtime error
Upload 7 files
Browse files- .gitignore +5 -2
- Dockerfile +25 -9
- app.py +903 -8
.gitignore
CHANGED
|
@@ -1,2 +1,5 @@
|
|
| 1 |
-
|
| 2 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.venv/
|
| 2 |
+
__pycache__/
|
| 3 |
+
.idea/
|
| 4 |
+
*.pyc
|
| 5 |
+
.env
|
Dockerfile
CHANGED
|
@@ -1,16 +1,32 @@
|
|
| 1 |
-
|
| 2 |
-
# you will also find guides on how best to write your Dockerfile
|
| 3 |
|
| 4 |
-
|
|
|
|
| 5 |
|
|
|
|
| 6 |
RUN useradd -m -u 1000 user
|
| 7 |
-
USER user
|
| 8 |
-
ENV PATH="/home/user/.local/bin:$PATH"
|
| 9 |
|
|
|
|
| 10 |
WORKDIR /app
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
-
|
| 16 |
-
CMD ["
|
|
|
|
| 1 |
+
FROM python:3.9-slim
|
|
|
|
| 2 |
|
| 3 |
+
# Prevent Python buffering
|
| 4 |
+
ENV PYTHONUNBUFFERED=1
|
| 5 |
|
| 6 |
+
# Create non-root user
|
| 7 |
RUN useradd -m -u 1000 user
|
|
|
|
|
|
|
| 8 |
|
| 9 |
+
# Set working directory
|
| 10 |
WORKDIR /app
|
| 11 |
|
| 12 |
+
# Copy requirements first for better Docker caching
|
| 13 |
+
COPY requirements.txt .
|
| 14 |
+
|
| 15 |
+
# Install dependencies
|
| 16 |
+
RUN pip install --no-cache-dir --upgrade pip && \
|
| 17 |
+
pip install --no-cache-dir -r requirements.txt
|
| 18 |
+
|
| 19 |
+
# Copy project files
|
| 20 |
+
COPY . .
|
| 21 |
+
|
| 22 |
+
# Change ownership
|
| 23 |
+
RUN chown -R user:user /app
|
| 24 |
+
|
| 25 |
+
# Switch to non-root user
|
| 26 |
+
USER user
|
| 27 |
+
|
| 28 |
+
# Expose Hugging Face Spaces port
|
| 29 |
+
EXPOSE 7860
|
| 30 |
|
| 31 |
+
# Run application
|
| 32 |
+
CMD ["python", "app.py"]
|
app.py
CHANGED
|
@@ -2,15 +2,910 @@
|
|
| 2 |
# -*- coding: utf-8 -*-
|
| 3 |
"""
|
| 4 |
@Author : Mihir Mithani
|
| 5 |
-
@Date :
|
| 6 |
-
@File :
|
| 7 |
-
@Desc :
|
| 8 |
"""
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
-
app = FastAPI()
|
| 12 |
|
|
|
|
| 13 |
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
return {"Hello": "World!"}
|
|
|
|
| 2 |
# -*- coding: utf-8 -*-
|
| 3 |
"""
|
| 4 |
@Author : Mihir Mithani
|
| 5 |
+
@Date : 08-05-2026 , 10:57
|
| 6 |
+
@File : chip_routingv3.py
|
| 7 |
+
@Desc :
|
| 8 |
"""
|
| 9 |
+
"""
|
| 10 |
+
chip_routing_cuopt.py
|
| 11 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 12 |
+
Interactive chip routing optimizer with REAL PCB-style routing.
|
| 13 |
+
|
| 14 |
+
Routing engine
|
| 15 |
+
ββββββββββββββ
|
| 16 |
+
β’ Octilinear A* pathfinding β only 90Β° and 45Β° turns, like real EDA tools
|
| 17 |
+
β’ Sequential net routing with incremental blocking so wires NEVER share
|
| 18 |
+
grid edges or cross each other
|
| 19 |
+
β’ Via dots drawn at every bend
|
| 20 |
+
β’ Solid lines for orthogonal (90Β°) hops, dashed for diagonal (45Β°) hops
|
| 21 |
+
β’ cuOpt VRP used to find the optimal ORDER to route nets
|
| 22 |
+
(minimises total wire length globally)
|
| 23 |
+
|
| 24 |
+
Usage
|
| 25 |
+
βββββ
|
| 26 |
+
pip install requests
|
| 27 |
+
python chip_routing_cuopt.py
|
| 28 |
+
|
| 29 |
+
Set NVIDIA_API_KEY env-var or edit the constant below.
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
import heapq
|
| 33 |
+
import time
|
| 34 |
+
import tkinter as tk
|
| 35 |
+
from tkinter import messagebox, simpledialog
|
| 36 |
+
|
| 37 |
+
import requests
|
| 38 |
+
|
| 39 |
+
import API
|
| 40 |
+
|
| 41 |
+
# βββ API ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 42 |
+
NVIDIA_API_KEY = API.API()
|
| 43 |
+
INVOKE_URL = "https://optimize.api.nvidia.com/v1/nvidia/cuopt"
|
| 44 |
+
FETCH_URL_FMT = "https://optimize.api.nvidia.com/v1/status/{}"
|
| 45 |
+
POLL_INTERVAL = 1.2
|
| 46 |
+
MAX_WAIT = 120
|
| 47 |
+
|
| 48 |
+
HEADERS = {
|
| 49 |
+
"Authorization": f"Bearer {NVIDIA_API_KEY}",
|
| 50 |
+
"Accept": "application/json",
|
| 51 |
+
"Content-Type": "application/json",
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
# βββ Theme ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 55 |
+
T = {
|
| 56 |
+
"bg": "#0a0c14",
|
| 57 |
+
"panel": "#10131f",
|
| 58 |
+
"panel2": "#14192a",
|
| 59 |
+
"border": "#1e2440",
|
| 60 |
+
"accent": "#4f6ef7",
|
| 61 |
+
"accent2": "#c084fc",
|
| 62 |
+
"text": "#dde1f5",
|
| 63 |
+
"muted": "#4a5275",
|
| 64 |
+
"cell_empty": "#0e1120",
|
| 65 |
+
"cell_comp": "#0e2040",
|
| 66 |
+
"cell_depot": "#1a0e40",
|
| 67 |
+
"cell_sel": "#0e3020",
|
| 68 |
+
"cell_hover": "#161c38",
|
| 69 |
+
"ok": "#22d3a0",
|
| 70 |
+
"warn": "#facc15",
|
| 71 |
+
"danger": "#f87171",
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
NET_COLORS = [
|
| 75 |
+
"#4f6ef7", "#22d3a0", "#facc15", "#f87171", "#c084fc",
|
| 76 |
+
"#fb923c", "#38bdf8", "#f472b6", "#a3e635", "#e879f9",
|
| 77 |
+
]
|
| 78 |
+
|
| 79 |
+
CELL_W = 72
|
| 80 |
+
CELL_H = 50
|
| 81 |
+
GPAD = 14
|
| 82 |
+
|
| 83 |
+
# βββ Octilinear A* ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 84 |
+
# 8 directions: N, S, E, W, NE, NW, SE, SW
|
| 85 |
+
DIRS = [
|
| 86 |
+
(0, 1, 1.0),
|
| 87 |
+
(0, -1, 1.0),
|
| 88 |
+
(1, 0, 1.0),
|
| 89 |
+
(-1, 0, 1.0),
|
| 90 |
+
(1, 1, 1.414),
|
| 91 |
+
(1, -1, 1.414),
|
| 92 |
+
(-1, 1, 1.414),
|
| 93 |
+
(-1, -1, 1.414),
|
| 94 |
+
]
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def astar(src_rc, dst_rc, rows, cols, blocked: set, comp_nodes: set):
|
| 98 |
+
"""
|
| 99 |
+
Octilinear A* path from src_rc to dst_rc.
|
| 100 |
+
blocked : cells occupied by previously routed wires (interior points)
|
| 101 |
+
comp_nodes : cells containing a component β impassable unless src/dst
|
| 102 |
+
Returns list of (r,c) from src to dst inclusive, or None.
|
| 103 |
+
"""
|
| 104 |
+
passable = {src_rc, dst_rc}
|
| 105 |
+
walls = (blocked | comp_nodes) - passable
|
| 106 |
+
|
| 107 |
+
sr, sc = src_rc
|
| 108 |
+
dr, dc = dst_rc
|
| 109 |
+
|
| 110 |
+
def h(r, c):
|
| 111 |
+
return max(abs(r - dr), abs(c - dc)) # Chebyshev β admissible
|
| 112 |
+
|
| 113 |
+
# heap: (f, g, r, c, parent)
|
| 114 |
+
heap = [(h(sr, sc), 0.0, sr, sc, None)]
|
| 115 |
+
came = {}
|
| 116 |
+
gscore = {(sr, sc): 0.0}
|
| 117 |
+
|
| 118 |
+
while heap:
|
| 119 |
+
f, g, r, c, parent = heapq.heappop(heap)
|
| 120 |
+
node = (r, c)
|
| 121 |
+
if node in came:
|
| 122 |
+
continue
|
| 123 |
+
came[node] = parent
|
| 124 |
+
|
| 125 |
+
if node == (dr, dc):
|
| 126 |
+
path = []
|
| 127 |
+
cur = node
|
| 128 |
+
while cur is not None:
|
| 129 |
+
path.append(cur)
|
| 130 |
+
cur = came[cur]
|
| 131 |
+
path.reverse()
|
| 132 |
+
return path
|
| 133 |
+
|
| 134 |
+
for ddr, ddc, cost in DIRS:
|
| 135 |
+
nr, nc = r + ddr, c + ddc
|
| 136 |
+
if not (0 <= nr < rows and 0 <= nc < cols):
|
| 137 |
+
continue
|
| 138 |
+
if (nr, nc) in walls:
|
| 139 |
+
continue
|
| 140 |
+
# diagonal squeeze-through check
|
| 141 |
+
if abs(ddr) == 1 and abs(ddc) == 1:
|
| 142 |
+
if (r + ddr, c) in walls and (r, c + ddc) in walls:
|
| 143 |
+
continue
|
| 144 |
+
ng = g + cost
|
| 145 |
+
if ng < gscore.get((nr, nc), 1e18):
|
| 146 |
+
gscore[(nr, nc)] = ng
|
| 147 |
+
heapq.heappush(heap, (ng + h(nr, nc), ng, nr, nc, node))
|
| 148 |
+
|
| 149 |
+
return None
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def route_all_nets(pairs, rows, cols, components, order=None):
|
| 153 |
+
"""
|
| 154 |
+
Route nets in the given order using sequential A* with incremental blocking.
|
| 155 |
+
Returns dict: net_name -> list of (r,c)
|
| 156 |
+
"""
|
| 157 |
+
|
| 158 |
+
def n2rc(n):
|
| 159 |
+
return (n // cols, n % cols)
|
| 160 |
+
|
| 161 |
+
comp_nodes = {n2rc(n) for n in components}
|
| 162 |
+
blocked = set() # interior cells already used by prior nets
|
| 163 |
+
results = {}
|
| 164 |
+
|
| 165 |
+
if order is None:
|
| 166 |
+
order = list(range(len(pairs)))
|
| 167 |
+
|
| 168 |
+
for idx in order:
|
| 169 |
+
p = pairs[idx]
|
| 170 |
+
src = n2rc(p["src"])
|
| 171 |
+
dst = n2rc(p["sink"])
|
| 172 |
+
path = astar(src, dst, rows, cols, blocked, comp_nodes)
|
| 173 |
+
|
| 174 |
+
if path is None:
|
| 175 |
+
# rip-up fallback: ignore wire blocking, respect only components
|
| 176 |
+
path = astar(src, dst, rows, cols, set(), comp_nodes)
|
| 177 |
+
|
| 178 |
+
results[p["name"]] = path or []
|
| 179 |
+
|
| 180 |
+
if path:
|
| 181 |
+
# block interior cells (not endpoints) for subsequent nets
|
| 182 |
+
for cell in path[1:-1]:
|
| 183 |
+
blocked.add(cell)
|
| 184 |
+
|
| 185 |
+
return results
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
# βββ cuOpt helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 189 |
+
|
| 190 |
+
def _cost_matrix(rows, cols, layer_id):
|
| 191 |
+
n = rows * cols
|
| 192 |
+
mat = []
|
| 193 |
+
for a in range(n):
|
| 194 |
+
ra, ca = divmod(a, cols)
|
| 195 |
+
row = []
|
| 196 |
+
for b in range(n):
|
| 197 |
+
if a == b:
|
| 198 |
+
row.append(0)
|
| 199 |
+
continue
|
| 200 |
+
rb, cb = divmod(b, cols)
|
| 201 |
+
hd = abs(ca - cb)
|
| 202 |
+
vd = abs(ra - rb)
|
| 203 |
+
pen = vd if layer_id == 1 else hd
|
| 204 |
+
row.append(max(1, hd + vd + pen))
|
| 205 |
+
mat.append(row)
|
| 206 |
+
return mat
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def _delay_matrix(rows, cols):
|
| 210 |
+
n = rows * cols
|
| 211 |
+
mat = []
|
| 212 |
+
for a in range(n):
|
| 213 |
+
ra, ca = divmod(a, cols)
|
| 214 |
+
row = []
|
| 215 |
+
for b in range(n):
|
| 216 |
+
if a == b:
|
| 217 |
+
row.append(0)
|
| 218 |
+
else:
|
| 219 |
+
rb, cb = divmod(b, cols)
|
| 220 |
+
row.append(max(1, abs(ra - rb) + abs(ca - cb)))
|
| 221 |
+
mat.append(row)
|
| 222 |
+
return mat
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
def cuopt_net_order(rows, cols, pairs):
|
| 226 |
+
"""
|
| 227 |
+
Call cuOpt to get the optimal routing order for the nets.
|
| 228 |
+
Returns (order: list[int], raw_body: dict).
|
| 229 |
+
Falls back to Manhattan-distance greedy if API fails.
|
| 230 |
+
"""
|
| 231 |
+
n_nets = len(pairs)
|
| 232 |
+
max_t = rows * cols + 4
|
| 233 |
+
cap = n_nets + 4
|
| 234 |
+
|
| 235 |
+
payload = {
|
| 236 |
+
"action": "cuOpt_OptimizedRouting",
|
| 237 |
+
"data": {
|
| 238 |
+
"cost_matrix_data": {"data": {"1": _cost_matrix(rows, cols, 1),
|
| 239 |
+
"2": _cost_matrix(rows, cols, 2)}},
|
| 240 |
+
"travel_time_matrix_data": {"data": {"1": _delay_matrix(rows, cols),
|
| 241 |
+
"2": _delay_matrix(rows, cols)}},
|
| 242 |
+
"fleet_data": {
|
| 243 |
+
"vehicle_locations": [[0, 0], [0, 0]],
|
| 244 |
+
"vehicle_ids": ["M1_router", "M2_router"],
|
| 245 |
+
"capacities": [[cap, cap], [cap, cap]],
|
| 246 |
+
"vehicle_time_windows": [[0, max_t], [0, max_t]],
|
| 247 |
+
"vehicle_types": [1, 2],
|
| 248 |
+
"vehicle_max_costs": [rows * cols * 8, rows * cols * 8],
|
| 249 |
+
"vehicle_max_times": [max_t, max_t],
|
| 250 |
+
"skip_first_trips": [False, False],
|
| 251 |
+
"drop_return_trips": [True, True],
|
| 252 |
+
"min_vehicles": 1,
|
| 253 |
+
},
|
| 254 |
+
"task_data": {
|
| 255 |
+
"task_locations": [p["sink"] for p in pairs],
|
| 256 |
+
"task_ids": [p["name"] for p in pairs],
|
| 257 |
+
"demand": [[1] * n_nets, [1] * n_nets],
|
| 258 |
+
"task_time_windows": [[0, max_t]] * n_nets,
|
| 259 |
+
"service_times": [0] * n_nets,
|
| 260 |
+
},
|
| 261 |
+
"solver_config": {
|
| 262 |
+
"time_limit": 5,
|
| 263 |
+
"objectives": {
|
| 264 |
+
"cost": 2,
|
| 265 |
+
"travel_time": 1,
|
| 266 |
+
"variance_route_size": 1,
|
| 267 |
+
"variance_route_service_time": 0,
|
| 268 |
+
"prize": 0,
|
| 269 |
+
},
|
| 270 |
+
"verbose_mode": False,
|
| 271 |
+
"error_logging": True,
|
| 272 |
+
},
|
| 273 |
+
},
|
| 274 |
+
"client_version": "chip_router_v3",
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
session = requests.Session()
|
| 278 |
+
resp = session.post(INVOKE_URL, headers=HEADERS, json=payload, timeout=30)
|
| 279 |
+
elapsed = 0
|
| 280 |
+
while resp.status_code == 202:
|
| 281 |
+
req_id = resp.headers.get("NVCF-REQID", "")
|
| 282 |
+
time.sleep(POLL_INTERVAL)
|
| 283 |
+
elapsed += POLL_INTERVAL
|
| 284 |
+
if elapsed > MAX_WAIT:
|
| 285 |
+
raise TimeoutError("cuOpt timed out")
|
| 286 |
+
resp = session.get(FETCH_URL_FMT.format(req_id), headers=HEADERS, timeout=30)
|
| 287 |
+
resp.raise_for_status()
|
| 288 |
+
|
| 289 |
+
body = resp.json()
|
| 290 |
+
vdata = body.get("response", {}).get("solver_response", {}).get("vehicle_data", {})
|
| 291 |
+
names = []
|
| 292 |
+
for vd in vdata.values():
|
| 293 |
+
for t in vd.get("task_id", []):
|
| 294 |
+
if str(t) != "Depot":
|
| 295 |
+
names.append(str(t))
|
| 296 |
+
|
| 297 |
+
name_to_idx = {p["name"]: i for i, p in enumerate(pairs)}
|
| 298 |
+
order = [name_to_idx[n] for n in names if n in name_to_idx]
|
| 299 |
+
for i in range(len(pairs)):
|
| 300 |
+
if i not in order:
|
| 301 |
+
order.append(i)
|
| 302 |
+
return order, body
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
# βββ GUI ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 306 |
+
|
| 307 |
+
class App(tk.Tk):
|
| 308 |
+
def __init__(self):
|
| 309 |
+
super().__init__()
|
| 310 |
+
self.title("Chip Routing Optimizer β cuOpt + A*")
|
| 311 |
+
self.configure(bg=T["bg"])
|
| 312 |
+
self.geometry("1220x820")
|
| 313 |
+
self.resizable(True, True)
|
| 314 |
+
|
| 315 |
+
self.rows = 0
|
| 316 |
+
self.cols = 0
|
| 317 |
+
self.components = {} # node_idx -> name
|
| 318 |
+
self.pairs = [] # [{name, src, sink, src_name, sink_name}]
|
| 319 |
+
self.routes = {} # name -> [(r,c),...]
|
| 320 |
+
self.net_colors = {} # name -> color
|
| 321 |
+
self.sel_cell = None
|
| 322 |
+
self.pair_src = None
|
| 323 |
+
self.mode = "edit"
|
| 324 |
+
self.cell_items = {} # node -> (rect, text, coord, sub)
|
| 325 |
+
|
| 326 |
+
self._build_ui()
|
| 327 |
+
|
| 328 |
+
# ββ UI build ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 329 |
+
|
| 330 |
+
def _build_ui(self):
|
| 331 |
+
self.left = tk.Frame(self, bg=T["panel"], width=248)
|
| 332 |
+
self.left.pack(side=tk.LEFT, fill=tk.Y)
|
| 333 |
+
self.left.pack_propagate(False)
|
| 334 |
+
self._build_panel()
|
| 335 |
+
|
| 336 |
+
right = tk.Frame(self, bg=T["bg"])
|
| 337 |
+
right.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
| 338 |
+
|
| 339 |
+
cf = tk.Frame(right, bg=T["bg"])
|
| 340 |
+
cf.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
| 341 |
+
hs = tk.Scrollbar(cf, orient=tk.HORIZONTAL)
|
| 342 |
+
vs = tk.Scrollbar(cf, orient=tk.VERTICAL)
|
| 343 |
+
self.canvas = tk.Canvas(cf, bg=T["bg"], highlightthickness=0,
|
| 344 |
+
xscrollcommand=hs.set, yscrollcommand=vs.set)
|
| 345 |
+
hs.config(command=self.canvas.xview)
|
| 346 |
+
vs.config(command=self.canvas.yview)
|
| 347 |
+
hs.pack(side=tk.BOTTOM, fill=tk.X)
|
| 348 |
+
vs.pack(side=tk.RIGHT, fill=tk.Y)
|
| 349 |
+
self.canvas.pack(fill=tk.BOTH, expand=True)
|
| 350 |
+
|
| 351 |
+
self.status_var = tk.StringVar(value="Enter grid dimensions and click Build.")
|
| 352 |
+
tk.Label(right, textvariable=self.status_var,
|
| 353 |
+
bg=T["panel2"], fg=T["muted"],
|
| 354 |
+
font=("Courier", 9), anchor=tk.W, padx=10, pady=5
|
| 355 |
+
).pack(fill=tk.X, side=tk.BOTTOM)
|
| 356 |
+
|
| 357 |
+
def _lbl(self, p, text, fg=None, size=9, bold=False):
|
| 358 |
+
tk.Label(p, text=text, bg=T["panel"],
|
| 359 |
+
fg=fg or T["muted"],
|
| 360 |
+
font=("Courier", size, "bold" if bold else "normal"),
|
| 361 |
+
anchor=tk.W).pack(fill=tk.X, padx=12, pady=(2, 0))
|
| 362 |
+
|
| 363 |
+
def _sep(self, p):
|
| 364 |
+
tk.Frame(p, bg=T["border"], height=1).pack(fill=tk.X, pady=5)
|
| 365 |
+
|
| 366 |
+
def _btn(self, p, text, cmd, bg, fg=None, pady=6):
|
| 367 |
+
tk.Button(p, text=text, font=("Courier", 9, "bold"),
|
| 368 |
+
bg=bg, fg=fg or T["bg"], relief=tk.FLAT,
|
| 369 |
+
activebackground=bg, activeforeground=fg or T["bg"],
|
| 370 |
+
command=cmd, cursor="hand2", pady=pady
|
| 371 |
+
).pack(fill=tk.X, padx=12, pady=3)
|
| 372 |
+
|
| 373 |
+
def _build_panel(self):
|
| 374 |
+
p = self.left
|
| 375 |
+
tk.Label(p, text="CHIP ROUTER", bg=T["panel"], fg=T["accent"],
|
| 376 |
+
font=("Courier", 12, "bold")).pack(pady=(16, 1))
|
| 377 |
+
tk.Label(p, text="cuOpt order Β· A* octilinear paths",
|
| 378 |
+
bg=T["panel"], fg=T["muted"], font=("Courier", 7)).pack(pady=(0, 10))
|
| 379 |
+
self._sep(p)
|
| 380 |
+
|
| 381 |
+
# β Grid
|
| 382 |
+
self._lbl(p, "β GRID SIZE", T["text"], 9, True)
|
| 383 |
+
gf = tk.Frame(p, bg=T["panel"]);
|
| 384 |
+
gf.pack(fill=tk.X, padx=12, pady=4)
|
| 385 |
+
for row_i, (lbl, var_name, default) in enumerate(
|
| 386 |
+
[("Rows", "rows_var", 6), ("Cols", "cols_var", 8)]):
|
| 387 |
+
tk.Label(gf, text=lbl, bg=T["panel"], fg=T["muted"],
|
| 388 |
+
font=("Courier", 8)).grid(row=row_i, column=0, sticky=tk.W, pady=2)
|
| 389 |
+
v = tk.IntVar(value=default)
|
| 390 |
+
setattr(self, var_name, v)
|
| 391 |
+
tk.Spinbox(gf, from_=2, to=20, textvariable=v, width=5,
|
| 392 |
+
bg=T["cell_empty"], fg=T["text"], relief=tk.FLAT,
|
| 393 |
+
insertbackground=T["text"], buttonbackground=T["border"]
|
| 394 |
+
).grid(row=row_i, column=1, padx=8, pady=2)
|
| 395 |
+
self._btn(p, "βΆ BUILD GRID", self._on_build, T["accent"])
|
| 396 |
+
|
| 397 |
+
self._sep(p)
|
| 398 |
+
|
| 399 |
+
# β‘ Components
|
| 400 |
+
self._lbl(p, "β‘ PLACE COMPONENTS", T["text"], 9, True)
|
| 401 |
+
self._lbl(p, "Click cell β type name β Place")
|
| 402 |
+
ef = tk.Frame(p, bg=T["panel"]);
|
| 403 |
+
ef.pack(fill=tk.X, padx=12, pady=4)
|
| 404 |
+
tk.Label(ef, text="Name:", bg=T["panel"], fg=T["muted"],
|
| 405 |
+
font=("Courier", 8)).pack(side=tk.LEFT)
|
| 406 |
+
self.comp_var = tk.StringVar()
|
| 407 |
+
self.comp_entry = tk.Entry(ef, textvariable=self.comp_var, width=13,
|
| 408 |
+
bg=T["cell_empty"], fg=T["text"], relief=tk.FLAT,
|
| 409 |
+
insertbackground=T["text"], font=("Courier", 9))
|
| 410 |
+
self.comp_entry.pack(side=tk.LEFT, padx=(4, 0))
|
| 411 |
+
self.comp_entry.bind("<Return>", lambda _: self._on_place())
|
| 412 |
+
bf = tk.Frame(p, bg=T["panel"]);
|
| 413 |
+
bf.pack(fill=tk.X, padx=12, pady=(0, 4))
|
| 414 |
+
for txt, cmd, col in [("Place", self._on_place, T["ok"]),
|
| 415 |
+
("Clear", self._on_clear, T["danger"])]:
|
| 416 |
+
tk.Button(bf, text=txt, font=("Courier", 8), bg=col, fg=T["bg"],
|
| 417 |
+
relief=tk.FLAT, command=cmd, cursor="hand2",
|
| 418 |
+
padx=8, pady=2).pack(side=tk.LEFT, padx=(0, 4))
|
| 419 |
+
self.sel_lbl = tk.Label(p, text="No cell selected",
|
| 420 |
+
bg=T["panel"], fg=T["muted"],
|
| 421 |
+
font=("Courier", 7), anchor=tk.W)
|
| 422 |
+
self.sel_lbl.pack(fill=tk.X, padx=12)
|
| 423 |
+
|
| 424 |
+
self._sep(p)
|
| 425 |
+
|
| 426 |
+
# β’ Pairs
|
| 427 |
+
self._lbl(p, "β’ WIRE PAIRS", T["text"], 9, True)
|
| 428 |
+
self._lbl(p, "Toggle mode β click src β click sink")
|
| 429 |
+
self.pair_btn = tk.Button(
|
| 430 |
+
p, text="β ENTER PAIR MODE",
|
| 431 |
+
font=("Courier", 8, "bold"),
|
| 432 |
+
bg=T["cell_empty"], fg=T["accent2"], relief=tk.FLAT,
|
| 433 |
+
activebackground=T["border"], activeforeground=T["accent2"],
|
| 434 |
+
command=self._toggle_pair, cursor="hand2", pady=4)
|
| 435 |
+
self.pair_btn.pack(fill=tk.X, padx=12, pady=4)
|
| 436 |
+
self.pair_hint = tk.Label(p, text="", bg=T["panel"], fg=T["warn"],
|
| 437 |
+
font=("Courier", 7), anchor=tk.W)
|
| 438 |
+
self.pair_hint.pack(fill=tk.X, padx=12)
|
| 439 |
+
self.pair_list_frame = tk.Frame(p, bg=T["panel"])
|
| 440 |
+
self.pair_list_frame.pack(fill=tk.X, padx=12, pady=4)
|
| 441 |
+
|
| 442 |
+
self._sep(p)
|
| 443 |
+
|
| 444 |
+
# β£ Route
|
| 445 |
+
self._lbl(p, "β£ ROUTE", T["text"], 9, True)
|
| 446 |
+
self._btn(p, "β‘ RUN CUOPT + A*", self._on_run, T["accent2"])
|
| 447 |
+
self._btn(p, "βΊ RESET", self._on_reset, T["muted"])
|
| 448 |
+
|
| 449 |
+
self._sep(p)
|
| 450 |
+
self._lbl(p, "LEGEND", T["muted"], 7, True)
|
| 451 |
+
for label, color in [
|
| 452 |
+
("Orthogonal wire (90Β°)", T["accent"]),
|
| 453 |
+
("Diagonal wire (45Β°)", T["ok"]),
|
| 454 |
+
("Via / bend point", T["warn"]),
|
| 455 |
+
("Component cell", T["cell_comp"]),
|
| 456 |
+
("Depot / origin", T["cell_depot"]),
|
| 457 |
+
]:
|
| 458 |
+
lf = tk.Frame(p, bg=T["panel"]);
|
| 459 |
+
lf.pack(fill=tk.X, padx=12, pady=1)
|
| 460 |
+
tk.Canvas(lf, width=10, height=10, bg=color, highlightthickness=0
|
| 461 |
+
).pack(side=tk.LEFT)
|
| 462 |
+
tk.Label(lf, text=f" {label}", bg=T["panel"], fg=T["muted"],
|
| 463 |
+
font=("Courier", 7)).pack(side=tk.LEFT)
|
| 464 |
+
|
| 465 |
+
# ββ Grid draw βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 466 |
+
|
| 467 |
+
def _render_grid(self):
|
| 468 |
+
self.canvas.delete("all")
|
| 469 |
+
self.cell_items = {}
|
| 470 |
+
|
| 471 |
+
tw = self.cols * CELL_W + GPAD * 2
|
| 472 |
+
th = self.rows * CELL_H + GPAD * 2
|
| 473 |
+
self.canvas.config(scrollregion=(0, 0, tw, th))
|
| 474 |
+
|
| 475 |
+
for r in range(self.rows):
|
| 476 |
+
for c in range(self.cols):
|
| 477 |
+
n = r * self.cols + c
|
| 478 |
+
x0 = GPAD + c * CELL_W
|
| 479 |
+
y0 = GPAD + r * CELL_H
|
| 480 |
+
x1 = x0 + CELL_W - 1
|
| 481 |
+
y1 = y0 + CELL_H - 1
|
| 482 |
+
cx = (x0 + x1) / 2
|
| 483 |
+
cy = (y0 + y1) / 2
|
| 484 |
+
|
| 485 |
+
col = self._cell_bg(n)
|
| 486 |
+
rid = self.canvas.create_rectangle(
|
| 487 |
+
x0, y0, x1, y1, fill=col, outline=T["border"],
|
| 488 |
+
width=1, tags=(f"c{n}", "cell"))
|
| 489 |
+
|
| 490 |
+
lbl = "DEPOT" if n == 0 else self.components.get(n, "")
|
| 491 |
+
lclr = T["accent2"] if n == 0 else (T["accent"] if lbl else T["muted"])
|
| 492 |
+
tid = self.canvas.create_text(
|
| 493 |
+
cx, cy - 3, text=lbl,
|
| 494 |
+
fill=lclr, font=("Courier", 8, "bold"),
|
| 495 |
+
width=CELL_W - 6, anchor=tk.CENTER, tags=(f"c{n}",))
|
| 496 |
+
|
| 497 |
+
cid = self.canvas.create_text(
|
| 498 |
+
x1 - 3, y1 - 3, text=f"{r},{c}",
|
| 499 |
+
fill=T["muted"], font=("Courier", 6),
|
| 500 |
+
anchor=tk.SE, tags=(f"c{n}",))
|
| 501 |
+
|
| 502 |
+
self.cell_items[n] = (rid, tid, cid)
|
| 503 |
+
|
| 504 |
+
for item in (rid, tid, cid):
|
| 505 |
+
self.canvas.tag_bind(item, "<Button-1>",
|
| 506 |
+
lambda e, nd=n: self._click(nd))
|
| 507 |
+
self.canvas.tag_bind(item, "<Enter>",
|
| 508 |
+
lambda e, nd=n: self._hover(nd, True))
|
| 509 |
+
self.canvas.tag_bind(item, "<Leave>",
|
| 510 |
+
lambda e, nd=n: self._hover(nd, False))
|
| 511 |
+
|
| 512 |
+
self._redraw_routes()
|
| 513 |
+
|
| 514 |
+
def _cell_bg(self, n):
|
| 515 |
+
if n == 0: return T["cell_depot"]
|
| 516 |
+
if n == self.pair_src: return "#1a3040"
|
| 517 |
+
if n == self.sel_cell: return T["cell_sel"]
|
| 518 |
+
if n in self.components: return T["cell_comp"]
|
| 519 |
+
return T["cell_empty"]
|
| 520 |
+
|
| 521 |
+
def _recolor(self, n):
|
| 522 |
+
if n in self.cell_items:
|
| 523 |
+
self.canvas.itemconfig(self.cell_items[n][0], fill=self._cell_bg(n))
|
| 524 |
+
|
| 525 |
+
def _hover(self, n, on):
|
| 526 |
+
if n not in self.cell_items:
|
| 527 |
+
return
|
| 528 |
+
cur = self.canvas.itemcget(self.cell_items[n][0], "fill")
|
| 529 |
+
if on and cur == T["cell_empty"]:
|
| 530 |
+
self.canvas.itemconfig(self.cell_items[n][0], fill=T["cell_hover"])
|
| 531 |
+
else:
|
| 532 |
+
self._recolor(n)
|
| 533 |
+
|
| 534 |
+
# ββ Route drawing β the key visual part βββββββββββββββββββββββββββββββββββ
|
| 535 |
+
|
| 536 |
+
def _redraw_routes(self):
|
| 537 |
+
self.canvas.delete("route")
|
| 538 |
+
self.canvas.delete("via")
|
| 539 |
+
|
| 540 |
+
for i, p in enumerate(self.pairs):
|
| 541 |
+
name = p["name"]
|
| 542 |
+
color = self.net_colors.get(name, NET_COLORS[i % len(NET_COLORS)])
|
| 543 |
+
path = self.routes.get(name)
|
| 544 |
+
|
| 545 |
+
if path and len(path) >= 2:
|
| 546 |
+
# Draw each hop as a line segment
|
| 547 |
+
for seg in range(len(path) - 1):
|
| 548 |
+
r1, c1 = path[seg]
|
| 549 |
+
r2, c2 = path[seg + 1]
|
| 550 |
+
dr = r2 - r1
|
| 551 |
+
dc = c2 - c1
|
| 552 |
+
is45 = abs(dr) == 1 and abs(dc) == 1
|
| 553 |
+
|
| 554 |
+
px1 = GPAD + c1 * CELL_W + CELL_W // 2
|
| 555 |
+
py1 = GPAD + r1 * CELL_H + CELL_H // 2
|
| 556 |
+
px2 = GPAD + c2 * CELL_W + CELL_W // 2
|
| 557 |
+
py2 = GPAD + r2 * CELL_H + CELL_H // 2
|
| 558 |
+
|
| 559 |
+
# Solid for 90Β°, short-dash for 45Β°
|
| 560 |
+
dash = (5, 2) if is45 else ()
|
| 561 |
+
self.canvas.create_line(
|
| 562 |
+
px1, py1, px2, py2,
|
| 563 |
+
fill=color, width=3, dash=dash,
|
| 564 |
+
capstyle=tk.ROUND, joinstyle=tk.ROUND,
|
| 565 |
+
tags="route")
|
| 566 |
+
|
| 567 |
+
# Via dots at every direction change (bend)
|
| 568 |
+
for seg in range(1, len(path) - 1):
|
| 569 |
+
r0, c0 = path[seg - 1]
|
| 570 |
+
r1, c1 = path[seg]
|
| 571 |
+
r2, c2 = path[seg + 1]
|
| 572 |
+
if (r1 - r0, c1 - c0) != (r2 - r1, c2 - c1):
|
| 573 |
+
vx = GPAD + c1 * CELL_W + CELL_W // 2
|
| 574 |
+
vy = GPAD + r1 * CELL_H + CELL_H // 2
|
| 575 |
+
self.canvas.create_oval(
|
| 576 |
+
vx - 5, vy - 5, vx + 5, vy + 5,
|
| 577 |
+
fill=T["warn"], outline=T["bg"], width=1,
|
| 578 |
+
tags="via")
|
| 579 |
+
|
| 580 |
+
# Source terminal (large filled circle)
|
| 581 |
+
sr0, sc0 = path[0]
|
| 582 |
+
sx = GPAD + sc0 * CELL_W + CELL_W // 2
|
| 583 |
+
sy = GPAD + sr0 * CELL_H + CELL_H // 2
|
| 584 |
+
self.canvas.create_oval(sx - 6, sy - 6, sx + 6, sy + 6,
|
| 585 |
+
fill=color, outline=T["bg"], width=1,
|
| 586 |
+
tags="via")
|
| 587 |
+
|
| 588 |
+
# Sink terminal
|
| 589 |
+
er0, ec0 = path[-1]
|
| 590 |
+
ex = GPAD + ec0 * CELL_W + CELL_W // 2
|
| 591 |
+
ey = GPAD + er0 * CELL_H + CELL_H // 2
|
| 592 |
+
self.canvas.create_oval(ex - 6, ey - 6, ex + 6, ey + 6,
|
| 593 |
+
fill=color, outline=T["bg"], width=1,
|
| 594 |
+
tags="via")
|
| 595 |
+
# Arrow head at sink to show direction
|
| 596 |
+
self.canvas.create_oval(ex - 3, ey - 3, ex + 3, ey + 3,
|
| 597 |
+
fill=T["bg"], outline="",
|
| 598 |
+
tags="via")
|
| 599 |
+
|
| 600 |
+
# Net label at midpoint
|
| 601 |
+
mid = len(path) // 2
|
| 602 |
+
mr, mc = path[mid]
|
| 603 |
+
mx = GPAD + mc * CELL_W + CELL_W // 2
|
| 604 |
+
my = GPAD + mr * CELL_H + CELL_H // 2
|
| 605 |
+
self.canvas.create_text(mx, my - 10, text=name,
|
| 606 |
+
fill=color, font=("Courier", 7, "bold"),
|
| 607 |
+
tags="route")
|
| 608 |
+
|
| 609 |
+
else:
|
| 610 |
+
# No routed path yet β draw a dashed preview arrow
|
| 611 |
+
sr, sc = divmod(p["src"], self.cols)
|
| 612 |
+
dr_, dc_ = divmod(p["sink"], self.cols)
|
| 613 |
+
sx = GPAD + sc * CELL_W + CELL_W // 2
|
| 614 |
+
sy = GPAD + sr * CELL_H + CELL_H // 2
|
| 615 |
+
dx = GPAD + dc_ * CELL_W + CELL_W // 2
|
| 616 |
+
dy = GPAD + dr_ * CELL_H + CELL_H // 2
|
| 617 |
+
self.canvas.create_line(
|
| 618 |
+
sx, sy, dx, dy,
|
| 619 |
+
fill=color, width=1, dash=(3, 4),
|
| 620 |
+
arrow=tk.LAST, arrowshape=(7, 9, 3),
|
| 621 |
+
tags="route")
|
| 622 |
+
mx_, my_ = (sx + dx) / 2, (sy + dy) / 2
|
| 623 |
+
self.canvas.create_text(mx_, my_ - 7, text=name,
|
| 624 |
+
fill=color, font=("Courier", 7),
|
| 625 |
+
tags="route")
|
| 626 |
+
|
| 627 |
+
self.canvas.tag_raise("route")
|
| 628 |
+
self.canvas.tag_raise("via")
|
| 629 |
+
|
| 630 |
+
# ββ Cell click ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 631 |
+
|
| 632 |
+
def _click(self, n):
|
| 633 |
+
if self.mode == "edit":
|
| 634 |
+
if self.sel_cell is not None:
|
| 635 |
+
self._recolor(self.sel_cell)
|
| 636 |
+
self.sel_cell = n
|
| 637 |
+
self._recolor(n)
|
| 638 |
+
r, c = divmod(n, self.cols)
|
| 639 |
+
nm = self.components.get(n, "")
|
| 640 |
+
self.comp_var.set(nm)
|
| 641 |
+
self.comp_entry.focus_set()
|
| 642 |
+
if n == 0:
|
| 643 |
+
self.sel_lbl.config(text=f"({r},{c}) β depot/origin")
|
| 644 |
+
else:
|
| 645 |
+
self.sel_lbl.config(text=f"({r},{c}) Β· {nm or 'unnamed'}")
|
| 646 |
+
self._set_status(f"Selected ({r},{c}). Type name + Enter to place.")
|
| 647 |
+
|
| 648 |
+
elif self.mode == "pair":
|
| 649 |
+
if self.pair_src is None:
|
| 650 |
+
self.pair_src = n
|
| 651 |
+
self._recolor(n)
|
| 652 |
+
r, c = divmod(n, self.cols)
|
| 653 |
+
nm = self.components.get(n, "DEPOT" if n == 0 else f"node{n}")
|
| 654 |
+
self.pair_hint.config(text=f"Src: {nm} ({r},{c}) β now pick sink")
|
| 655 |
+
else:
|
| 656 |
+
if n == self.pair_src:
|
| 657 |
+
self._recolor(n)
|
| 658 |
+
self.pair_src = None
|
| 659 |
+
self.pair_hint.config(text="Cleared. Pick source again.")
|
| 660 |
+
return
|
| 661 |
+
def_name = f"NET{len(self.pairs)}"
|
| 662 |
+
net_name = simpledialog.askstring(
|
| 663 |
+
"Net name", "Name for this wire connection:",
|
| 664 |
+
initialvalue=def_name, parent=self)
|
| 665 |
+
if not net_name:
|
| 666 |
+
net_name = def_name
|
| 667 |
+
net_name = net_name.strip().upper().replace(" ", "_")
|
| 668 |
+
src_name = self.components.get(self.pair_src,
|
| 669 |
+
"DEPOT" if self.pair_src == 0 else f"N{self.pair_src}")
|
| 670 |
+
sink_name = self.components.get(n,
|
| 671 |
+
"DEPOT" if n == 0 else f"N{n}")
|
| 672 |
+
ci = len(self.pairs) % len(NET_COLORS)
|
| 673 |
+
self.net_colors[net_name] = NET_COLORS[ci]
|
| 674 |
+
self.pairs.append({"name": net_name, "src": self.pair_src,
|
| 675 |
+
"sink": n, "src_name": src_name,
|
| 676 |
+
"sink_name": sink_name})
|
| 677 |
+
prev = self.pair_src
|
| 678 |
+
self.pair_src = None
|
| 679 |
+
self._recolor(prev)
|
| 680 |
+
self.pair_hint.config(text=f"'{net_name}' added. Pick next src β")
|
| 681 |
+
self._refresh_pairs()
|
| 682 |
+
self._redraw_routes()
|
| 683 |
+
self._set_status(f"Pair '{net_name}' added ({len(self.pairs)} total).")
|
| 684 |
+
|
| 685 |
+
# ββ Component actions βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 686 |
+
|
| 687 |
+
def _on_place(self):
|
| 688 |
+
if self.sel_cell is None or self.sel_cell == 0:
|
| 689 |
+
self._set_status("Select a non-depot cell first.")
|
| 690 |
+
return
|
| 691 |
+
name = self.comp_var.get().strip()
|
| 692 |
+
if not name:
|
| 693 |
+
self._set_status("Enter a component name.")
|
| 694 |
+
return
|
| 695 |
+
self.components[self.sel_cell] = name
|
| 696 |
+
if self.sel_cell in self.cell_items:
|
| 697 |
+
self.canvas.itemconfig(self.cell_items[self.sel_cell][1],
|
| 698 |
+
text=name, fill=T["accent"])
|
| 699 |
+
self._recolor(self.sel_cell)
|
| 700 |
+
r, c = divmod(self.sel_cell, self.cols)
|
| 701 |
+
self._set_status(f"Placed '{name}' at ({r},{c}).")
|
| 702 |
+
for p in self.pairs:
|
| 703 |
+
if p["src"] == self.sel_cell: p["src_name"] = name
|
| 704 |
+
if p["sink"] == self.sel_cell: p["sink_name"] = name
|
| 705 |
+
self._refresh_pairs()
|
| 706 |
+
|
| 707 |
+
def _on_clear(self):
|
| 708 |
+
if self.sel_cell is None or self.sel_cell == 0:
|
| 709 |
+
return
|
| 710 |
+
self.components.pop(self.sel_cell, None)
|
| 711 |
+
if self.sel_cell in self.cell_items:
|
| 712 |
+
self.canvas.itemconfig(self.cell_items[self.sel_cell][1], text="")
|
| 713 |
+
self._recolor(self.sel_cell)
|
| 714 |
+
self.comp_var.set("")
|
| 715 |
+
|
| 716 |
+
# ββ Pair mode βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 717 |
+
|
| 718 |
+
def _toggle_pair(self):
|
| 719 |
+
if not self.rows:
|
| 720 |
+
self._set_status("Build a grid first.")
|
| 721 |
+
return
|
| 722 |
+
if self.mode == "edit":
|
| 723 |
+
self.mode = "pair"
|
| 724 |
+
self.pair_btn.config(text="β EXIT PAIR MODE",
|
| 725 |
+
bg=T["accent2"], fg=T["bg"])
|
| 726 |
+
self.pair_hint.config(text="Click a source cell β")
|
| 727 |
+
self._set_status("Pair mode: click source, then sink to add a wire pair.")
|
| 728 |
+
else:
|
| 729 |
+
self.mode = "edit"
|
| 730 |
+
if self.pair_src is not None:
|
| 731 |
+
self._recolor(self.pair_src)
|
| 732 |
+
self.pair_src = None
|
| 733 |
+
self.pair_btn.config(text="β ENTER PAIR MODE",
|
| 734 |
+
bg=T["cell_empty"], fg=T["accent2"])
|
| 735 |
+
self.pair_hint.config(text="")
|
| 736 |
+
self._set_status("Edit mode.")
|
| 737 |
+
|
| 738 |
+
def _refresh_pairs(self):
|
| 739 |
+
for w in self.pair_list_frame.winfo_children():
|
| 740 |
+
w.destroy()
|
| 741 |
+
for i, p in enumerate(self.pairs):
|
| 742 |
+
color = self.net_colors.get(p["name"], NET_COLORS[i % len(NET_COLORS)])
|
| 743 |
+
row = tk.Frame(self.pair_list_frame, bg=T["panel2"])
|
| 744 |
+
row.pack(fill=tk.X, pady=1)
|
| 745 |
+
tk.Canvas(row, width=8, height=8, bg=color, highlightthickness=0
|
| 746 |
+
).pack(side=tk.LEFT, padx=(4, 3), pady=3)
|
| 747 |
+
tk.Label(row,
|
| 748 |
+
text=f"{p['name']}: {p['src_name']} β {p['sink_name']}",
|
| 749 |
+
bg=T["panel2"], fg=T["text"],
|
| 750 |
+
font=("Courier", 7), anchor=tk.W
|
| 751 |
+
).pack(side=tk.LEFT, fill=tk.X, expand=True)
|
| 752 |
+
tk.Button(row, text="β", bg=T["panel2"], fg=T["danger"],
|
| 753 |
+
font=("Courier", 7), relief=tk.FLAT, cursor="hand2",
|
| 754 |
+
command=lambda idx=i: self._remove_pair(idx)
|
| 755 |
+
).pack(side=tk.RIGHT, padx=2)
|
| 756 |
+
|
| 757 |
+
def _remove_pair(self, idx):
|
| 758 |
+
if 0 <= idx < len(self.pairs):
|
| 759 |
+
name = self.pairs[idx]["name"]
|
| 760 |
+
self.pairs.pop(idx)
|
| 761 |
+
self.routes.pop(name, None)
|
| 762 |
+
self._refresh_pairs()
|
| 763 |
+
self._redraw_routes()
|
| 764 |
+
|
| 765 |
+
# ββ Run routing βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 766 |
+
|
| 767 |
+
def _on_run(self):
|
| 768 |
+
if not self.rows:
|
| 769 |
+
messagebox.showerror("No grid", "Build a grid first.")
|
| 770 |
+
return
|
| 771 |
+
if not self.pairs:
|
| 772 |
+
messagebox.showerror("No pairs", "Add at least one wire pair.")
|
| 773 |
+
return
|
| 774 |
+
|
| 775 |
+
self._set_status("Sending net list to NVIDIA cuOpt to optimise routing orderβ¦")
|
| 776 |
+
self.update()
|
| 777 |
+
|
| 778 |
+
try:
|
| 779 |
+
order, cuopt_body = cuopt_net_order(self.rows, self.cols, self.pairs)
|
| 780 |
+
except Exception as e:
|
| 781 |
+
self._set_status(f"cuOpt error: {e} β falling back to greedy order.")
|
| 782 |
+
order = list(range(len(self.pairs)))
|
| 783 |
+
cuopt_body = {}
|
| 784 |
+
|
| 785 |
+
self._set_status(
|
| 786 |
+
f"Running A* octilinear router for {len(self.pairs)} netsβ¦")
|
| 787 |
+
self.update()
|
| 788 |
+
|
| 789 |
+
self.routes = route_all_nets(
|
| 790 |
+
self.pairs, self.rows, self.cols, self.components, order=order)
|
| 791 |
+
|
| 792 |
+
self._render_grid()
|
| 793 |
+
|
| 794 |
+
routed = sum(1 for v in self.routes.values() if v)
|
| 795 |
+
self._set_status(
|
| 796 |
+
f"Done. {routed}/{len(self.pairs)} nets routed. "
|
| 797 |
+
f"Solid = 90Β°, dashed = 45Β°, yellow dot = via/bend.")
|
| 798 |
+
|
| 799 |
+
self._show_result_popup()
|
| 800 |
+
|
| 801 |
+
def _show_result_popup(self):
|
| 802 |
+
win = tk.Toplevel(self)
|
| 803 |
+
win.title("Routing Results")
|
| 804 |
+
win.configure(bg=T["bg"])
|
| 805 |
+
win.geometry("580x460")
|
| 806 |
+
|
| 807 |
+
tk.Label(win, text="ROUTING RESULTS", bg=T["bg"], fg=T["accent"],
|
| 808 |
+
font=("Courier", 11, "bold")).pack(pady=(14, 6))
|
| 809 |
+
|
| 810 |
+
frm = tk.Frame(win, bg=T["bg"])
|
| 811 |
+
frm.pack(fill=tk.BOTH, expand=True, padx=14)
|
| 812 |
+
sb = tk.Scrollbar(frm);
|
| 813 |
+
sb.pack(side=tk.RIGHT, fill=tk.Y)
|
| 814 |
+
txt = tk.Text(frm, bg=T["panel"], fg=T["text"], font=("Courier", 8),
|
| 815 |
+
relief=tk.FLAT, yscrollcommand=sb.set)
|
| 816 |
+
txt.pack(fill=tk.BOTH, expand=True)
|
| 817 |
+
sb.config(command=txt.yview)
|
| 818 |
+
|
| 819 |
+
total = 0
|
| 820 |
+
for i, p in enumerate(self.pairs):
|
| 821 |
+
path = self.routes.get(p["name"], [])
|
| 822 |
+
wire_len = len(path) - 1 if path else 0
|
| 823 |
+
total += wire_len
|
| 824 |
+
|
| 825 |
+
# count bends
|
| 826 |
+
bends = 0
|
| 827 |
+
for seg in range(1, len(path) - 1):
|
| 828 |
+
r0, c0 = path[seg - 1]
|
| 829 |
+
r1, c1 = path[seg]
|
| 830 |
+
r2, c2 = path[seg + 1]
|
| 831 |
+
if (r1 - r0, c1 - c0) != (r2 - r1, c2 - c1):
|
| 832 |
+
bends += 1
|
| 833 |
+
|
| 834 |
+
# count 45Β° hops
|
| 835 |
+
diag45 = sum(
|
| 836 |
+
1 for s in range(len(path) - 1)
|
| 837 |
+
if abs(path[s][0] - path[s + 1][0]) == 1
|
| 838 |
+
and abs(path[s][1] - path[s + 1][1]) == 1
|
| 839 |
+
)
|
| 840 |
+
ortho = wire_len - diag45
|
| 841 |
+
|
| 842 |
+
status = "ROUTED " if path else "UNROUTED"
|
| 843 |
+
src_rc = divmod(p["src"], self.cols)
|
| 844 |
+
sink_rc = divmod(p["sink"], self.cols)
|
| 845 |
+
line = (
|
| 846 |
+
f"[{status}] {p['name']:<12} "
|
| 847 |
+
f"{p['src_name']:<10} ({src_rc[0]},{src_rc[1]}) "
|
| 848 |
+
f"β {p['sink_name']:<10} ({sink_rc[0]},{sink_rc[1]})\n"
|
| 849 |
+
f" wire: {wire_len} hops "
|
| 850 |
+
f"({ortho} ortho + {diag45} diag) "
|
| 851 |
+
f"bends: {bends}\n\n"
|
| 852 |
+
)
|
| 853 |
+
txt.insert(tk.END, line)
|
| 854 |
+
|
| 855 |
+
txt.insert(tk.END, f"Total wire length : {total} hops\n")
|
| 856 |
+
txt.config(state=tk.DISABLED)
|
| 857 |
+
|
| 858 |
+
tk.Button(win, text="Close", bg=T["accent"], fg=T["bg"],
|
| 859 |
+
font=("Courier", 9, "bold"), relief=tk.FLAT,
|
| 860 |
+
command=win.destroy, cursor="hand2", pady=6
|
| 861 |
+
).pack(pady=(8, 14))
|
| 862 |
+
|
| 863 |
+
# ββ Build / Reset βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 864 |
+
|
| 865 |
+
def _on_build(self):
|
| 866 |
+
self.rows = max(2, min(20, self.rows_var.get()))
|
| 867 |
+
self.cols = max(2, min(20, self.cols_var.get()))
|
| 868 |
+
self.components = {}
|
| 869 |
+
self.pairs = []
|
| 870 |
+
self.routes = {}
|
| 871 |
+
self.net_colors = {}
|
| 872 |
+
self.sel_cell = None
|
| 873 |
+
self.pair_src = None
|
| 874 |
+
self.mode = "edit"
|
| 875 |
+
self.pair_btn.config(text="β ENTER PAIR MODE",
|
| 876 |
+
bg=T["cell_empty"], fg=T["accent2"])
|
| 877 |
+
self.pair_hint.config(text="")
|
| 878 |
+
self.sel_lbl.config(text="No cell selected")
|
| 879 |
+
self.comp_var.set("")
|
| 880 |
+
self._render_grid()
|
| 881 |
+
self._refresh_pairs()
|
| 882 |
+
self._set_status(
|
| 883 |
+
f"Grid {self.rows}Γ{self.cols} ready. "
|
| 884 |
+
"Click cells to place components.")
|
| 885 |
+
|
| 886 |
+
def _on_reset(self):
|
| 887 |
+
self.components = {}
|
| 888 |
+
self.pairs = []
|
| 889 |
+
self.routes = {}
|
| 890 |
+
self.net_colors = {}
|
| 891 |
+
self.sel_cell = None
|
| 892 |
+
self.pair_src = None
|
| 893 |
+
self.mode = "edit"
|
| 894 |
+
self.pair_btn.config(text="β ENTER PAIR MODE",
|
| 895 |
+
bg=T["cell_empty"], fg=T["accent2"])
|
| 896 |
+
self.pair_hint.config(text="")
|
| 897 |
+
self.sel_lbl.config(text="No cell selected")
|
| 898 |
+
self.comp_var.set("")
|
| 899 |
+
if self.rows:
|
| 900 |
+
self._render_grid()
|
| 901 |
+
self._refresh_pairs()
|
| 902 |
+
self._set_status("Reset. Place components and create wire pairs.")
|
| 903 |
+
|
| 904 |
+
def _set_status(self, msg):
|
| 905 |
+
self.status_var.set(f" {msg}")
|
| 906 |
|
|
|
|
| 907 |
|
| 908 |
+
# βββ Entry ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 909 |
|
| 910 |
+
if __name__ == "__main__":
|
| 911 |
+
App().mainloop()
|
|
|