Programm architecture simplified so that the EDA can now run individually without generation of .gds file

This commit is contained in:
2026-06-01 09:19:44 +08:00
parent 78f38d3be7
commit 7cf618fe02
160 changed files with 1640 additions and 11497 deletions
@@ -1,41 +0,0 @@
# -----------------------------------------------------------------------------
# Description: PDK component metadata describing photonic/electronic cell assets, pins, geometry, layers, and library classification.
# Inside functions: N/A - declarative YAML metadata/configuration.
# Developer : Qin Yue @ 2026
# Organization : OptiHK Limited
# -----------------------------------------------------------------------------
name: EC_SiN400_1310_1p0dB_L635_A0_QY_202604
foundry: Silterra
process: EMO1_2ML_Cu_RDL
year: '2026'
type: primitive
dependency: None
maturity: development
tapeout_history:
- run: Silterra_EMO1_2ML_Cu_RDL_2026_Q2
status: Pending testing
center_wavelength: 1310
version: 1.0
designer: Qin Yue
update_notes: New SiN edge couplers with high efficiency
ports:
a1:
x: -642.6
y: 0.0
a: 180.0
width: 0.7
b0:
x: 0.0
y: 0.0
a: 0.0
width: None
a0:
x: 0.0
y: 0.0
a: 180.0
width: 0.0
time: 20260505-170136
box_size:
- 646.0
- 75.0
file_size: 1.36 KB
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+14 -283
View File
@@ -1,23 +1,22 @@
# -----------------------------------------------------------------------------
# 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, _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
# Description: Backend integration wrapper for required mxpic_router project GDS generation.
# Inside functions: build_project_gds, _build_with_mxpic_router, _load_project_cells
# Developer : Qin Yue @ 2026
# Organization : OptiHK Limited
# -----------------------------------------------------------------------------
import math
import os
import sys
from dataclasses import dataclass, field
from typing import Dict, List
import yaml
from pdk_registry import PdkRegistry
from router_dependency import require_router_stack
@dataclass
class BuildResult:
"""Container for GDS build output paths, status details, and engine metadata."""
output_path: str
engine: str
cells_built: List[str] = field(default_factory=list)
@@ -31,57 +30,18 @@ def build_project_gds(
technology_manifest_path: str = None,
prefer_full_gds: bool = False,
) -> BuildResult:
"""Build a hierarchical project GDS from saved cell YAML files."""
"""Build a hierarchical project GDS from saved cell YAML files with mxpic_router."""
cells = _load_project_cells(project_dir)
if not cells:
raise ValueError("No saved cell YAML files found for this project")
# Prefer the routed builder whenever it is available because it understands
# bundle links, anchor connections, and PDK-aware routing rules.
try:
return _build_with_mxpic_router(
project_dir,
output_path,
pdk_public_root,
technology_manifest_path,
prefer_full_gds,
)
except ImportError as router_error:
if _cells_have_links(cells):
raise RuntimeError(
"Routed Build GDS requires mxpic_router, nazca, and mxpic_forge when layout links are present. "
f"Router import failed: {router_error}"
) from router_error
# Placement-only projects can still be exported with local GDS engines when
# the routed builder is not installed.
registry = PdkRegistry(pdk_public_root, prefer_full_gds=prefer_full_gds)
os.makedirs(os.path.dirname(output_path), exist_ok=True)
# 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)
except ImportError as gdstk_error:
try:
return _build_with_nazca(cells, output_path, registry)
except ImportError as nazca_error:
raise RuntimeError(
"Build GDS requires either gdstk or a working nazca installation. "
f"gdstk import failed: {gdstk_error}. nazca import failed: {nazca_error}"
) from nazca_error
return _build_with_mxpic_router(
project_dir,
output_path,
pdk_public_root,
technology_manifest_path,
prefer_full_gds,
)
def _build_with_mxpic_router(
@@ -91,12 +51,8 @@ def _build_with_mxpic_router(
technology_manifest_path: str,
prefer_full_gds: bool,
) -> BuildResult:
"""Delegate routed project GDS generation to the external mxpic_router package."""
# mxpic_router lives beside this repository during local development, so add
# that sibling checkout to sys.path only when it exists.
router_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "mxpic_router"))
if os.path.isdir(router_root) and router_root not in sys.path:
sys.path.insert(0, router_root)
"""Delegate project GDS generation to the required external mxpic_router package."""
require_router_stack()
from mxpic_router import build_project_gds as build_routed_project_gds
result = build_routed_project_gds(
@@ -126,228 +82,3 @@ def _load_project_cells(project_dir: str) -> Dict[str, dict]:
cell_name = str(data.get("name") or os.path.splitext(filename)[0])
cells[cell_name] = data
return cells
def _ordered_cell_names(cells: Dict[str, dict]) -> List[str]:
"""Order cells so dependencies are built before cells that reference them."""
composites = [name for name, data in cells.items() if data.get("type") != "project"]
projects = [name for name, data in cells.items() if data.get("type") == "project"]
return composites + projects
def _cells_have_links(cells: Dict[str, dict]) -> bool:
"""Detect whether any saved cell contains bundle links that require routed building."""
for data in cells.values():
for bundle in (data.get("bundles") or {}).values():
if bundle.get("links"):
return True
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
library = gdstk.Library()
built_cells = {}
warnings = []
# Build composite cells before project cells so project-level references can
# reuse cells already inserted into the same GDS library.
for cell_name in _ordered_cell_names(cells):
data = cells[cell_name]
gds_cell = library.new_cell(_safe_cell_name(cell_name, built_cells))
built_cells[cell_name] = gds_cell
for instance_name, instance in (data.get("instances") or {}).items():
component = str(instance.get("component") or "")
x = _number(instance.get("x"))
y = _number(instance.get("y"))
rotation = math.radians(_number(instance.get("rotation")))
child = built_cells.get(component)
if child is None:
# External components are resolved through the active PDK
# registry and imported as references.
asset = registry.resolve(component)
if not asset.gds_path:
warnings.append(f"Missing GDS for {instance_name}: {component}")
continue
child = _import_public_gds(gdstk, library, asset.gds_path)
gds_cell.add(gdstk.Reference(child, origin=(x, y), rotation=rotation))
library.write_gds(output_path)
return BuildResult(
output_path=output_path,
engine="gdstk",
cells_built=list(built_cells.keys()),
warnings=warnings,
)
def _import_public_gds(gdstk, library, gds_path: str):
"""Import public PDK GDS geometry into the output library."""
source = gdstk.read_gds(gds_path)
top_cells = source.top_level()
if not top_cells:
raise ValueError(f"No top-level cell found in {gds_path}")
# Avoid adding duplicate cell names when multiple instances reference the
# same imported PDK component.
for source_cell in source.cells:
if _library_cell_by_name(library, source_cell.name) is None:
library.add(source_cell)
return top_cells[0]
def _build_with_nazca(cells: Dict[str, dict], output_path: str, registry: PdkRegistry) -> BuildResult:
"""Assemble a project GDS with Nazca cells and PDK component placements."""
import nazca as nd
warnings = []
built_cells = {}
ordered_names = _ordered_cell_names(cells)
# Nazca cells are built in dependency order and then the final project cell
# 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 "")
x = _number(instance.get("x"))
y = _number(instance.get("y"))
rotation = _number(instance.get("rotation"))
if component in built_cells:
built_cells[component].put(x, y, rotation)
continue
asset = registry.resolve(component)
if not asset.gds_path:
warnings.append(f"Missing GDS for {instance_name}: {component}")
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]
nd.export_gds(built_cells[top_name], filename=output_path)
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"
candidate = base
counter = 1
used = {cell.name for cell in existing.values()}
while candidate in used:
counter += 1
candidate = f"{base}_{counter}"
return candidate
def _library_cell_by_name(library, name: str):
"""Find a cell object in a loaded layout library by name."""
for cell in library.cells:
if cell.name == name:
return cell
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:
if value is None or value == "":
return default
return float(value)
except (TypeError, ValueError):
return default
-154
View File
@@ -1,154 +0,0 @@
# -----------------------------------------------------------------------------
# Description: Layout preview helpers for generating SVG previews from saved layout YAML.
# Inside functions: create_layout_svg_from_gds, _create_with_gdstk, _build_gdstk_cell, _resolve_child_cell, _import_gds_cell, _create_with_nazca, _load_local_layout, _safe_cell_name, _library_cell_by_name, _number
# Developer : Qin Yue @ 2026
# Organization : OptiHK Limited
# -----------------------------------------------------------------------------
import os
from typing import Dict, Optional
import yaml
def create_layout_svg_from_gds(yaml_content: str, output_path: str, pdk_registry, project_dir: str = None) -> str:
"""Create an SVG preview by placing real public _BB.gds cells from layout YAML."""
layout = yaml.safe_load(yaml_content) or {}
# Try gdstk first because it can write SVG directly; keep Nazca as a GDS
# placement fallback for environments where gdstk is unavailable.
try:
return _create_with_gdstk(layout, output_path, pdk_registry, project_dir)
except ImportError as gdstk_error:
try:
return _create_with_nazca(layout, output_path, pdk_registry, project_dir)
except ImportError as nazca_error:
raise RuntimeError(
"Layout SVG requires GDS geometry support. Install gdstk, or fix nazca dependencies. "
f"gdstk import failed: {gdstk_error}. nazca import failed: {nazca_error}"
) from nazca_error
def _create_with_gdstk(layout: dict, output_path: str, pdk_registry, project_dir: Optional[str]) -> str:
"""Generate preview SVG geometry using gdstk import and placement APIs."""
import gdstk
library = gdstk.Library()
cell_cache = {}
top = _build_gdstk_cell(gdstk, library, layout, pdk_registry, project_dir, cell_cache)
os.makedirs(os.path.dirname(output_path), exist_ok=True)
top.write_svg(output_path)
return output_path
def _build_gdstk_cell(gdstk, library, layout: dict, pdk_registry, project_dir: Optional[str], cell_cache: Dict):
"""Build or reuse a gdstk cell for a saved layout document."""
cell_name = _safe_cell_name(layout.get("name") or "layout", library)
top = library.new_cell(cell_name)
# Each saved instance becomes a GDS reference to either another project cell
# or a resolved PDK asset.
for instance_name, instance in (layout.get("instances") or {}).items():
component = str(instance.get("component") or "")
x = _number(instance.get("x"))
y = _number(instance.get("y"))
rotation = _number(instance.get("rotation")) * 3.141592653589793 / 180
child = _resolve_child_cell(gdstk, library, component, pdk_registry, project_dir, cell_cache)
if child is None:
raise FileNotFoundError(f"Unable to resolve _BB.gds for instance {instance_name}: {component}")
top.add(gdstk.Reference(child, origin=(x, y), rotation=rotation))
return top
def _resolve_child_cell(gdstk, library, component: str, pdk_registry, project_dir: Optional[str], cell_cache: Dict):
"""Resolve a placed child component from local cells or PDK assets."""
if component in cell_cache:
return cell_cache[component]
# Project-local composite cells are resolved before external PDK components
# so nested user-created cells can appear in preview output.
local_layout = _load_local_layout(component, project_dir)
if local_layout is not None:
child = _build_gdstk_cell(gdstk, library, local_layout, pdk_registry, project_dir, cell_cache)
cell_cache[component] = child
return child
asset = pdk_registry.resolve(component)
if not asset.gds_path:
return None
child = _import_gds_cell(gdstk, library, asset.gds_path)
cell_cache[component] = child
return child
def _import_gds_cell(gdstk, library, gds_path: str):
"""Import a GDS file and return the first usable cell for placement."""
source = gdstk.read_gds(gds_path)
top_cells = source.top_level()
if not top_cells:
raise ValueError(f"No top-level cell found in {gds_path}")
# Reuse already-imported cells by name to keep the preview library compact.
for source_cell in source.cells:
if _library_cell_by_name(library, source_cell.name) is None:
library.add(source_cell)
return top_cells[0]
def _create_with_nazca(layout: dict, output_path: str, pdk_registry, project_dir: Optional[str]) -> str:
"""Generate preview SVG geometry using Nazca when gdstk is unavailable."""
import nazca as nd
png_path = os.path.splitext(output_path)[0] + ".gds"
# Nazca can place the same GDS references as the preview path, but this
# backend still requires gdstk for final SVG conversion.
with nd.Cell(str(layout.get("name") or "layout")) as top:
for instance_name, instance in (layout.get("instances") or {}).items():
component = str(instance.get("component") or "")
asset = pdk_registry.resolve(component)
if not asset.gds_path:
raise FileNotFoundError(f"Unable to resolve _BB.gds for instance {instance_name}: {component}")
loaded = nd.load_gds(asset.gds_path)
loaded.put(_number(instance.get("x")), _number(instance.get("y")), _number(instance.get("rotation")))
nd.export_gds(top, filename=png_path)
raise RuntimeError(
"Nazca can build the placed GDS, but SVG preview export requires gdstk in this backend."
)
def _load_local_layout(component: str, project_dir: Optional[str]) -> Optional[dict]:
"""Load a project-local composite layout referenced by another cell."""
if not project_dir or "/" in component or "\\" in component or component == "generate_with_forge":
return None
for ext in (".yml", ".yaml"):
path = os.path.join(project_dir, f"{component}{ext}")
if os.path.exists(path):
with open(path, "r", encoding="utf-8") as file:
return yaml.safe_load(file) or {}
return None
def _safe_cell_name(name, library) -> 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 "layout"
candidate = base
counter = 1
while _library_cell_by_name(library, candidate) is not None:
counter += 1
candidate = f"{base}_{counter}"
return candidate
def _library_cell_by_name(library, name: str):
"""Find a cell object in a loaded layout library by name."""
for cell in library.cells:
if cell.name == name:
return cell
return None
def _number(value, default=0.0) -> float:
"""Convert numeric YAML values to floats with a stable default."""
try:
if value is None or value == "":
return default
return float(value)
except (TypeError, ValueError):
return default
-132
View File
@@ -1,132 +0,0 @@
# -----------------------------------------------------------------------------
# Description: PDK registry scanning helpers for finding component YAML/GDS assets and building library trees.
# Inside functions: __init__, resolve, _find_yaml, _find_gds, _load_yaml, _inside_root
# Developer : Qin Yue @ 2026
# Organization : OptiHK Limited
# -----------------------------------------------------------------------------
import os
from dataclasses import dataclass
from typing import Optional
import yaml
@dataclass
class PdkAsset:
"""Container describing the YAML and GDS assets resolved for a PDK component."""
component: str
yaml_path: Optional[str] = None
gds_path: Optional[str] = None
metadata: Optional[dict] = None
class PdkRegistry:
"""Resolve public PDK component names to metadata and public GDS assets."""
def __init__(self, public_root: str, prefer_full_gds: bool = False):
"""Store the active PDK root and cache resolved component assets."""
self.public_root = os.path.abspath(public_root)
self.prefer_full_gds = prefer_full_gds
self._asset_cache = {}
def resolve(self, component: str) -> PdkAsset:
"""Resolve YAML and GDS assets for a requested component key."""
key = (component or "").strip().replace("\\", "/").strip("/")
if not key:
return PdkAsset(component=component)
if key in self._asset_cache:
return self._asset_cache[key]
yaml_path = self._find_yaml(key)
gds_path = self._find_gds(key, yaml_path)
metadata = self._load_yaml(yaml_path) if yaml_path else None
asset = PdkAsset(component=component, yaml_path=yaml_path, gds_path=gds_path, metadata=metadata)
self._asset_cache[key] = asset
return asset
def _find_yaml(self, key: str) -> Optional[str]:
"""Locate the component YAML description file in the active PDK tree."""
# Try direct component paths first so saved YAML component references
# resolve without scanning the whole PDK tree.
direct = os.path.join(self.public_root, *key.split("/"))
candidates = []
if direct.lower().endswith((".yml", ".yaml")):
candidates.append(direct)
else:
name = key.split("/")[-1]
candidates.append(os.path.join(direct, f"{name}.yml"))
candidates.append(os.path.join(direct, f"{name}.yaml"))
for candidate in candidates:
if self._inside_root(candidate) and os.path.exists(candidate):
return os.path.abspath(candidate)
name = key.split("/")[-1]
# Fall back to a tree scan for older saved references that only stored
# the component folder name.
for root, dirs, files in os.walk(self.public_root):
if os.path.basename(root) == name:
for filename in files:
if filename.lower().endswith((".yml", ".yaml")):
return os.path.join(root, filename)
dirs.clear()
return None
def _find_gds(self, key: str, yaml_path: Optional[str]) -> Optional[str]:
"""Locate the best matching GDS asset for a component YAML or key."""
search_dir = os.path.dirname(yaml_path) if yaml_path else os.path.join(self.public_root, *key.split("/"))
name = key.split("/")[-1]
# Normal users prefer black-box GDS for fast previews; manager sessions
# can prefer full layout geometry for complete export.
if self.prefer_full_gds:
candidates = [
os.path.join(search_dir, f"{name}.gds"),
os.path.join(search_dir, f"{name}_BB.gds"),
]
else:
candidates = [
os.path.join(search_dir, f"{name}_BB.gds"),
os.path.join(search_dir, f"{name}.gds"),
]
for candidate in candidates:
if self._inside_root(candidate) and os.path.exists(candidate):
return os.path.abspath(candidate)
# If the expected filename is missing, choose the first available GDS in
# the component folder while respecting the full-vs-BB preference.
if os.path.isdir(search_dir):
gds_files = sorted(filename for filename in os.listdir(search_dir) if filename.lower().endswith(".gds"))
full_files = [filename for filename in gds_files if not filename.lower().endswith("_bb.gds")]
bb_files = [filename for filename in gds_files if filename.lower().endswith("_bb.gds")]
ordered = (full_files + bb_files) if self.prefer_full_gds else (bb_files + full_files)
if ordered:
return os.path.join(search_dir, ordered[0])
return None
def _load_yaml(self, yaml_path: Optional[str]) -> Optional[dict]:
"""Read a YAML component metadata file into a dictionary."""
if not yaml_path:
return None
with open(yaml_path, "r", encoding="utf-8") as file:
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."""
target = os.path.abspath(path)
return target == self.public_root or target.startswith(self.public_root + os.sep)
+3 -6
View File
@@ -5,11 +5,12 @@
# Organization : OptiHK Limited
# -----------------------------------------------------------------------------
import os
import sys
import tempfile
import yaml
from router_dependency import require_router_stack
def create_routed_layout_svg(
yaml_content: str,
@@ -20,15 +21,11 @@ def create_routed_layout_svg(
prefer_full_gds: bool = False,
) -> str:
"""Create an SVG preview from routed GDS geometry generated by mxpic_router."""
require_router_stack(require_gdstk=True)
import gdstk
layout = yaml.safe_load(yaml_content) or {}
cell_name = str(layout.get("name") or "layout")
# mxpic_router is kept as a sibling repository, so the backend adds it to
# sys.path only when the local checkout is available.
router_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "mxpic_router"))
if os.path.isdir(router_root) and router_root not in sys.path:
sys.path.insert(0, router_root)
from mxpic_router import build_project_gds
os.makedirs(os.path.dirname(output_path), exist_ok=True)
+77
View File
@@ -0,0 +1,77 @@
# -----------------------------------------------------------------------------
# Description: Build-time mxpic_router runtime dependency validation helpers.
# Inside functions: ensure_router_path, require_router_stack
# Developer : Qin Yue @ 2026
# Organization : OptiHK Limited
# -----------------------------------------------------------------------------
import importlib
import os
import sys
from dataclasses import dataclass, field
from typing import List
@dataclass
class RouterStackStatus:
"""Summary of the router stack checks completed for a build action."""
ok: bool
router_root: str
checked: List[str] = field(default_factory=list)
class RouterStackUnavailable(RuntimeError):
"""Raised when a build action needs the external router stack but it is absent."""
pass
def ensure_router_path() -> str:
"""Add the sibling mxpic_router checkout to import resolution when present."""
router_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "mxpic_router"))
if os.path.isdir(router_root) and router_root not in sys.path:
sys.path.insert(0, router_root)
return router_root
def require_router_stack(require_gdstk: bool = False) -> RouterStackStatus:
"""Validate the runtime stack required for build-time router actions."""
router_root = ensure_router_path()
checked = []
missing = []
try:
importlib.import_module("mxpic_router")
checked.append("mxpic_router")
except Exception as exc:
missing.append(f"mxpic_router: {exc}")
try:
importlib.import_module("nazca")
checked.append("nazca")
except Exception as exc:
missing.append(f"nazca: {exc}")
if require_gdstk:
try:
importlib.import_module("gdstk")
checked.append("gdstk")
except Exception as exc:
missing.append(f"gdstk: {exc}")
try:
router_builder = importlib.import_module("mxpic_router.builder")
route_factory = getattr(router_builder, "_import_mxpic_forge_route")
route_factory()
checked.append("mxpic_forge Route")
except Exception as exc:
missing.append(f"mxpic_forge Route: {exc}")
if missing:
details = "; ".join(missing)
raise RouterStackUnavailable(
"Required mxpic_router runtime stack is unavailable. "
"Build actions require the matched mxpic_router and mxpic_forge checkouts, "
f"Nazca, and gdstk for SVG preview generation. Details: {details}"
)
return RouterStackStatus(ok=True, router_root=router_root, checked=checked)
+52 -43
View File
@@ -1,6 +1,6 @@
# -----------------------------------------------------------------------------
# Description: Flask backend API server for authentication, project management, PDK library access, layout preview, and GDS build endpoints.
# Inside functions: no_cache_response, login_required_json, wrapper, request_ip, record_action, safe_name, user_layout_root, project_root, cell_file_path, cell_svg_path, cell_routes_path, write_route_points_sidecar, project_gds_path, technology_manifest_path_for_project, current_pdk_root, current_pdk_registry, scoped_pdk_root_for_project, pdk_root_for_request_project, project_meta_path, read_project_meta
# Inside functions: no_cache_response, login_required_json, wrapper, request_ip, record_action, safe_name, user_layout_root, project_root, cell_file_path, cell_svg_path, cell_routes_path, write_route_points_sidecar, project_gds_path, technology_manifest_path_for_project, current_pdk_root, scoped_pdk_root_for_project, pdk_root_for_request_project, project_meta_path, read_project_meta
# Developer : Qin Yue @ 2026
# Organization : OptiHK Limited
# -----------------------------------------------------------------------------
@@ -17,15 +17,14 @@ from werkzeug.security import check_password_hash
import database
from flask import Response
from gds_builder import build_project_gds
from layout_preview import create_layout_svg_from_gds
from pdk_registry import PdkRegistry
from pdk_access import (
cleanup_expired_exports,
create_export_path,
pdk_root_for_session,
prefer_full_gds_for_session,
)
from routed_layout_preview import create_routed_layout_svg, layout_has_links
from router_dependency import RouterStackUnavailable
from routed_layout_preview import create_routed_layout_svg
from technology_manifest import TechnologyManifestError, read_technology_manifest
# --- Path Configurations ---
@@ -35,12 +34,6 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
FRONTEND_DIR = os.path.join(BASE_DIR, '..', 'frontend')
REPO_ROOT = os.path.abspath(os.path.join(BASE_DIR, '..', '..'))
PDK_PUBLIC_ROOT = os.path.abspath(os.environ.get(
'MXPIC_PDK_PUBLIC_ROOT',
os.path.join(REPO_ROOT, 'opt_pdk_public', 'foundries')
))
EDA_PDK_ROOT = os.path.abspath(os.path.join(BASE_DIR, '..', 'mxpic', 'PDKs'))
YML_PATH = os.path.join(BASE_DIR, '..', 'mxpic', 'PDKs', 'Silterra', 'directories.yaml')
# Component/category icons are served from backend/icons for the library panel.
ICONS_DIR = os.path.join(BASE_DIR, 'icons')
@@ -181,27 +174,32 @@ def project_gds_path(project_name):
return os.path.join(project_root(project_name), f"{safe_name(project_name, 'project_1')}.gds")
def technology_manifest_path_for_project(project_name):
"""Return the stored technology manifest path for a project."""
technology_id = read_project_meta(project_name).get("technology") or ""
if "/" not in technology_id:
return None
foundry, technology = technology_id.split("/", 1)
path = os.path.abspath(os.path.join(EDA_PDK_ROOT, safe_name(foundry, ''), safe_name(technology, ''), "technology.yml"))
# Keep stored project metadata from escaping the local EDA PDK root.
if path.startswith(EDA_PDK_ROOT + os.sep) and os.path.exists(path):
return path
return None
def current_pdk_root():
"""Resolve the active PDK root for the current request session."""
return pdk_root_for_session(session, REPO_ROOT)
def current_pdk_registry():
"""Create a PDK registry configured for the current session scope."""
return PdkRegistry(current_pdk_root(), prefer_full_gds=prefer_full_gds_for_session(session))
def technology_manifest_path_for_pdk_root(pdk_root, foundry, technology):
"""Return a safe technology.yml path under a role-scoped PDK root."""
base_root = os.path.abspath(pdk_root)
path = os.path.abspath(os.path.join(
base_root,
safe_name(foundry, ''),
safe_name(technology, ''),
"technology.yml"
))
if path.startswith(base_root + os.sep) and os.path.exists(path):
return path
return None
def technology_manifest_path_for_project(project_name):
"""Return the stored technology manifest path for a project and session."""
technology_id = read_project_meta(project_name).get("technology") or ""
if "/" not in technology_id:
return None
foundry, technology = technology_id.split("/", 1)
return technology_manifest_path_for_pdk_root(current_pdk_root(), foundry, technology)
def scoped_pdk_root_for_project(project_name):
@@ -265,6 +263,14 @@ def ensure_project_path(project_name):
# These helpers turn the active PDK folder structure into the nested component
# library tree shown in the canvas sidebar.
def component_yml_files(files):
"""Return component metadata YAML files, excluding technology manifests."""
return [
f for f in files
if f.lower().endswith(('.yml', '.yaml')) and f.lower() != "technology.yml"
]
def findComps(baseDir, path_root=None):
"""Scan component folders, return map of paths -> component info."""
compMap = {}
@@ -273,7 +279,7 @@ def findComps(baseDir, path_root=None):
# A folder containing a YAML file is treated as a component leaf; scanning
# stops below that leaf so nested assets do not pollute the library tree.
for root, dirs, files in os.walk(baseDir):
ymlFiles = [f for f in files if f.endswith('.yml')]
ymlFiles = component_yml_files(files)
if ymlFiles:
parentDir = os.path.dirname(root)
relPath = os.path.relpath(parentDir, refDir)
@@ -356,7 +362,7 @@ def readCompYaml(compName, comps_root=None):
for root, dirs, files in os.walk(search_root):
if os.path.basename(root) == compName:
dirs.clear()
ymlFiles = [f for f in files if f.endswith('.yml')]
ymlFiles = component_yml_files(files)
if ymlFiles:
ymlPath = os.path.join(root, ymlFiles[0])
with open(ymlPath, 'r', encoding='utf-8') as f:
@@ -445,11 +451,11 @@ def health_check():
@app.route('/api/technologies', methods=['GET'])
@login_required_json
def list_technologies():
"""List technology choices from mxpic/PDKs/<foundry>/<technology>."""
"""List technology choices from the active role-scoped PDK root."""
technologies = []
pdks_root = EDA_PDK_ROOT
# Technology choices are built from directory names because each technology
# folder owns its generated technology.yml manifest.
pdks_root = current_pdk_root()
# Technology choices are built from foundry/technology directories under the
# role PDK root, and only folders with technology.yml are exposed.
if os.path.isdir(pdks_root):
for foundry in sorted(os.listdir(pdks_root)):
foundry_path = os.path.join(pdks_root, foundry)
@@ -459,6 +465,8 @@ def list_technologies():
technology_path = os.path.join(foundry_path, technology)
if not os.path.isdir(technology_path):
continue
if not os.path.exists(os.path.join(technology_path, "technology.yml")):
continue
technologies.append({
"foundry": foundry,
"technology": technology,
@@ -473,11 +481,7 @@ def list_technologies():
def get_technology_manifest(foundry, technology):
"""Return the routing and layer manifest for a selected technology."""
try:
manifest = read_technology_manifest(
EDA_PDK_ROOT,
safe_name(foundry, ''),
safe_name(technology, '')
)
manifest = read_technology_manifest(current_pdk_root(), safe_name(foundry, ''), safe_name(technology, ''))
return jsonify({"manifest": manifest})
except TechnologyManifestError as e:
return jsonify({"error": str(e)}), 404
@@ -735,11 +739,11 @@ def save_layout():
write_route_points_sidecar(content, cell_routes_path(project, cell))
svg_path = None
preview_status = "not_requested"
preview_error = None
if create_preview:
svg_path = cell_svg_path(project, cell)
# Routed layouts need the router backend; placement-only layouts can
# use the simpler GDS/SVG preview path.
if layout_has_links(content):
try:
create_routed_layout_svg(
content,
svg_path,
@@ -748,8 +752,11 @@ def save_layout():
technology_manifest_path=technology_manifest_path_for_project(project),
prefer_full_gds=prefer_full_gds_for_session(session),
)
else:
create_layout_svg_from_gds(content, svg_path, pdk_registry=current_pdk_registry(), project_dir=project_root(project))
preview_status = "generated"
except RouterStackUnavailable as e:
preview_status = "skipped"
preview_error = str(e)
svg_path = None
record_action('layout.save', project=project, cell=cell, detail={"bytes": len(content), "svg": svg_path})
return jsonify({
@@ -758,7 +765,9 @@ def save_layout():
"cell": cell,
"path": save_path,
"svg_path": svg_path,
"svg_url": url_for('get_layout_svg', project_name=project, cell_name=cell) if svg_path else None
"svg_url": url_for('get_layout_svg', project_name=project, cell_name=cell) if svg_path else None,
"preview_status": preview_status,
"preview_error": preview_error
}), 200
except Exception as e: