Updated
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
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
|
||||
Reference in New Issue
Block a user