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