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

132 lines
5.0 KiB
Python

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