138 lines
5.6 KiB
Python
138 lines
5.6 KiB
Python
# -----------------------------------------------------------------------------
|
|
# 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:
|
|
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:
|
|
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):
|
|
cell_name = _safe_cell_name(layout.get("name") or "layout", library)
|
|
top = library.new_cell(cell_name)
|
|
|
|
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):
|
|
if component in cell_cache:
|
|
return cell_cache[component]
|
|
|
|
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):
|
|
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 _create_with_nazca(layout: dict, output_path: str, pdk_registry, project_dir: Optional[str]) -> str:
|
|
import nazca as nd
|
|
|
|
png_path = os.path.splitext(output_path)[0] + ".gds"
|
|
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]:
|
|
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:
|
|
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):
|
|
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
|