Files
mxpic_EDA/backend/gds_builder.py
T
2026-05-28 17:53:41 +08:00

159 lines
5.6 KiB
Python

import math
import os
from dataclasses import dataclass, field
from typing import Dict, List
import yaml
from pdk_registry import PdkRegistry
@dataclass
class BuildResult:
output_path: str
engine: str
cells_built: List[str] = field(default_factory=list)
warnings: List[str] = field(default_factory=list)
def build_project_gds(project_dir: str, output_path: str, pdk_public_root: str) -> BuildResult:
"""Build a hierarchical project GDS from saved cell YAML files."""
cells = _load_project_cells(project_dir)
if not cells:
raise ValueError("No saved cell YAML files found for this project")
registry = PdkRegistry(pdk_public_root)
os.makedirs(os.path.dirname(output_path), exist_ok=True)
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
def _load_project_cells(project_dir: str) -> Dict[str, dict]:
cells = {}
for filename in sorted(os.listdir(project_dir)):
if not filename.lower().endswith((".yml", ".yaml")):
continue
path = os.path.join(project_dir, filename)
with open(path, "r", encoding="utf-8") as file:
data = yaml.safe_load(file) or {}
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]:
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 _build_with_gdstk(cells: Dict[str, dict], output_path: str, registry: PdkRegistry) -> BuildResult:
import gdstk
library = gdstk.Library()
built_cells = {}
warnings = []
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:
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):
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}")
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:
import nazca as nd
warnings = []
built_cells = {}
ordered_names = _ordered_cell_names(cells)
for cell_name in ordered_names:
data = cells[cell_name]
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)
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 _safe_cell_name(name: str, existing: dict) -> str:
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):
for cell in library.cells:
if cell.name == name:
return cell
return None
def _number(value, default=0.0) -> float:
try:
if value is None or value == "":
return default
return float(value)
except (TypeError, ValueError):
return default