Routing problem for multi-pin port and anchors are debugged

This commit is contained in:
2026-05-31 22:16:44 +08:00
parent 9b4e8da796
commit ce7f6e95c4
36 changed files with 2470 additions and 676 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+107 -2
View File
@@ -1,6 +1,6 @@
# -----------------------------------------------------------------------------
# Description: Backend integration wrapper for project GDS generation and build result handling.
# Inside functions: build_project_gds, _build_with_mxpic_router, _load_project_cells, _ordered_cell_names, _cells_have_links, _build_with_gdstk, _import_public_gds, _build_with_nazca, _safe_cell_name, _library_cell_by_name, _number
# Inside functions: build_project_gds, _build_with_mxpic_router, _load_project_cells, _ordered_cell_names, _cells_have_links, _cells_have_elements, _build_with_gdstk, _import_public_gds, _build_with_nazca, _build_nazca_element_cells, _build_nazca_element_cell, _element_port_offset, _safe_cell_name, _library_cell_by_name, _int, _number
# Developer : Qin Yue @ 2026
# Organization : OptiHK Limited
# -----------------------------------------------------------------------------
@@ -58,7 +58,19 @@ def build_project_gds(
registry = PdkRegistry(pdk_public_root, prefer_full_gds=prefer_full_gds)
os.makedirs(os.path.dirname(output_path), exist_ok=True)
# gdstk is the preferred fallback; Nazca remains a secondary fallback for
# Port/Anchor elements are logical pin-bearing devices. Use Nazca for these
# layouts so each element can be a placed cell with local nd.Pin metadata.
if _cells_have_elements(cells):
try:
return _build_with_nazca(cells, output_path, registry)
except ImportError as nazca_error:
raise RuntimeError(
"Build GDS with Port/Anchor elements requires Nazca so element cells can preserve nd.Pin metadata. "
f"Nazca import failed: {nazca_error}"
) from nazca_error
# gdstk is the preferred fallback for placement-only projects without
# Port/Anchor element pin metadata; Nazca remains a secondary fallback for
# environments where gdstk is not installed.
try:
return _build_with_gdstk(cells, output_path, registry)
@@ -132,6 +144,16 @@ def _cells_have_links(cells: Dict[str, dict]) -> bool:
return False
def _cells_have_elements(cells: Dict[str, dict]) -> bool:
"""Detect whether any saved cell contains Port/Anchor element objects."""
for data in cells.values():
elements = data.get("elements") or {}
for element in elements.values():
if str((element or {}).get("type") or "").lower() in {"port", "anchor"}:
return True
return False
def _build_with_gdstk(cells: Dict[str, dict], output_path: str, registry: PdkRegistry) -> BuildResult:
"""Assemble a project GDS with gdstk when Nazca or routed building is unavailable."""
import gdstk
@@ -196,6 +218,7 @@ def _build_with_nazca(cells: Dict[str, dict], output_path: str, registry: PdkReg
# is exported as the top-level GDS.
for cell_name in ordered_names:
data = cells[cell_name]
element_cells = _build_nazca_element_cells(nd, cell_name, data.get("elements") or {}, built_cells)
with nd.Cell(cell_name) as current_cell:
for instance_name, instance in (data.get("instances") or {}).items():
component = str(instance.get("component") or "")
@@ -211,6 +234,12 @@ def _build_with_nazca(cells: Dict[str, dict], output_path: str, registry: PdkReg
continue
loaded = nd.load_gds(asset.gds_path)
loaded.put(x, y, rotation)
for element_name, element_cell in element_cells.items():
element = (data.get("elements") or {}).get(element_name) or {}
x = _number(element.get("x"))
y = _number(element.get("y"))
rotation = _number(element.get("angle"))
element_cell.put(x, y, rotation)
built_cells[cell_name] = current_cell
top_name = ordered_names[-1]
@@ -218,6 +247,72 @@ def _build_with_nazca(cells: Dict[str, dict], output_path: str, registry: PdkReg
return BuildResult(output_path=output_path, engine="nazca", cells_built=ordered_names, warnings=warnings)
def _build_nazca_element_cells(nd, parent_cell_name: str, elements: dict, existing_cells: dict) -> Dict[str, object]:
"""Build reusable Nazca cells for built-in Port and Anchor element objects."""
element_cells = {}
known_cells = dict(existing_cells or {})
for element_name, element in (elements or {}).items():
element_type = str((element or {}).get("type") or "").lower()
if element_type not in {"port", "anchor"}:
continue
raw_cell_name = f"{parent_cell_name}_{element_name}_{element_type}"
cell_name = _safe_cell_name(raw_cell_name, {**known_cells, **element_cells})
element_cell = _build_nazca_element_cell(nd, cell_name, {**(element or {}), "name": str(element_name)})
element_cells[str(element_name)] = element_cell
return element_cells
def _build_nazca_element_cell(nd, cell_name: str, element: dict):
"""Create one local Port or Anchor cell whose pins are placed in local coordinates."""
element = element or {}
element_type = str(element.get("type") or "").lower()
width = _number(element.get("width"), 0.5)
port_number = max(1, _int(element.get("pin_number", element.get("pinNumber", element.get("port_number", element.get("portNumber")))), 1))
pitch = _number(element.get("pitch"), 10.0)
with nd.Cell(name=cell_name) as element_cell:
if element_type == "port":
for index, pin_name in enumerate(_element_pin_names(cell_name, element, port_number, "port")):
y = 0.0 if port_number == 1 else _element_port_offset(index, port_number, pitch)
nd.Pin(pin_name, width=width).put(0.0, y, 180.0)
return element_cell
anchor_pin_names = _element_pin_names(cell_name, element, port_number, "anchor")
for index in range(port_number):
y = -15.0 + _element_port_offset(index, port_number, pitch)
nd.Pin(anchor_pin_names[index * 2], width=width).put(0.0, y, 180.0)
nd.Pin(anchor_pin_names[index * 2 + 1], width=width).put(0.0, y, 0.0)
return element_cell
def _element_port_offset(index: int, count: int, pitch: float) -> float:
"""Return the local y offset for one repeated Port/Anchor pin."""
return ((max(1, count) - 1) / 2 - index) * pitch
def _element_pin_names(cell_name: str, element: dict, count: int, element_type: str) -> List[str]:
"""Return local element pin names, using YAML overrides when present."""
raw_pins = [pin for pin in (element.get("pins") or []) if isinstance(pin, dict)]
default_base = _safe_pin_name(str(element.get("name") or cell_name).replace("_port", "").replace("_anchor", ""))
roles = []
if element_type == "port":
roles = [f"io{index + 1}" for index in range(count)]
else:
for index in range(count):
roles.extend([f"a{index + 1}", f"b{index + 1}"])
by_role = {str(pin.get("role") or ""): str(pin.get("name") or "") for pin in raw_pins}
by_index = [str(pin.get("name") or "") for pin in raw_pins]
names = []
for index, role in enumerate(roles):
name = by_role.get(role) or (by_index[index] if index < len(by_index) else "") or f"{default_base}_{role}"
names.append(_safe_pin_name(name))
return names
def _safe_pin_name(name: str) -> str:
"""Generate a backend-safe pin name for local Nazca element pins."""
return "".join(ch if ch.isalnum() or ch == "_" else "_" for ch in str(name or "pin"))
def _safe_cell_name(name: str, existing: dict) -> str:
"""Generate a backend-safe unique cell name for GDS/Nazca libraries."""
base = "".join(ch if ch.isalnum() or ch in "._$" else "_" for ch in str(name)) or "cell"
@@ -238,6 +333,16 @@ def _library_cell_by_name(library, name: str):
return None
def _int(value, default=0) -> int:
"""Convert integer YAML values with a stable default."""
try:
if value is None or value == "":
return default
return int(float(value))
except (TypeError, ValueError):
return default
def _number(value, default=0.0) -> float:
"""Convert numeric YAML values to floats with a stable default."""
try:
+18 -1
View File
@@ -107,7 +107,24 @@ class PdkRegistry:
if not yaml_path:
return None
with open(yaml_path, "r", encoding="utf-8") as file:
return yaml.safe_load(file) or {}
data = yaml.safe_load(file) or {}
return self._normalize_pdk_pins(data)
def _normalize_pdk_pins(self, data: dict) -> dict:
"""Treat legacy PDK `ports` metadata as `pins` inside approved PDK roots."""
if (
self._allows_legacy_ports_as_pins()
and isinstance(data, dict)
and "pins" not in data
and isinstance(data.get("ports"), dict)
):
data = dict(data)
data["pins"] = data["ports"]
return data
def _allows_legacy_ports_as_pins(self) -> bool:
normalized = self.public_root.replace("\\", "/").lower()
return "/opt_pdk_public/" in f"{normalized}/" or "/opt_pdk_atlas/" in f"{normalized}/"
def _inside_root(self, path: str) -> bool:
"""Check that a candidate asset path remains inside the permitted PDK root."""
+7 -1
View File
@@ -360,7 +360,13 @@ def readCompYaml(compName, comps_root=None):
if ymlFiles:
ymlPath = os.path.join(root, ymlFiles[0])
with open(ymlPath, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)
data = yaml.safe_load(f) or {}
normalized_root = os.path.abspath(search_root).replace("\\", "/").lower()
pdk_ports_allowed = "/opt_pdk_public/" in f"{normalized_root}/" or "/opt_pdk_atlas/" in f"{normalized_root}/"
if pdk_ports_allowed and isinstance(data, dict) and "pins" not in data and isinstance(data.get("ports"), dict):
data = dict(data)
data["pins"] = data["ports"]
return data
return None