More annotation added to the program

This commit is contained in:
2026-05-30 12:44:44 +08:00
parent b3f29398f0
commit bf223b52ac
22 changed files with 729 additions and 353 deletions
+27
View File
@@ -17,6 +17,7 @@ from pdk_registry import PdkRegistry
@dataclass
class BuildResult:
"""Container for GDS build output paths, status details, and engine metadata."""
output_path: str
engine: str
cells_built: List[str] = field(default_factory=list)
@@ -35,6 +36,8 @@ def build_project_gds(
if not cells:
raise ValueError("No saved cell YAML files found for this project")
# Prefer the routed builder whenever it is available because it understands
# bundle links, anchor connections, and PDK-aware routing rules.
try:
return _build_with_mxpic_router(
project_dir,
@@ -50,9 +53,13 @@ def build_project_gds(
f"Router import failed: {router_error}"
) from router_error
# Placement-only projects can still be exported with local GDS engines when
# the routed builder is not installed.
registry = PdkRegistry(pdk_public_root, prefer_full_gds=prefer_full_gds)
os.makedirs(os.path.dirname(output_path), exist_ok=True)
# gdstk is the preferred fallback; Nazca remains a secondary fallback for
# environments where gdstk is not installed.
try:
return _build_with_gdstk(cells, output_path, registry)
except ImportError as gdstk_error:
@@ -72,6 +79,9 @@ def _build_with_mxpic_router(
technology_manifest_path: str,
prefer_full_gds: bool,
) -> BuildResult:
"""Delegate routed project GDS generation to the external mxpic_router package."""
# mxpic_router lives beside this repository during local development, so add
# that sibling checkout to sys.path only when it exists.
router_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "mxpic_router"))
if os.path.isdir(router_root) and router_root not in sys.path:
sys.path.insert(0, router_root)
@@ -93,6 +103,7 @@ def _build_with_mxpic_router(
def _load_project_cells(project_dir: str) -> Dict[str, dict]:
"""Load saved cell YAML documents from a project directory."""
cells = {}
for filename in sorted(os.listdir(project_dir)):
if not filename.lower().endswith((".yml", ".yaml")):
@@ -106,12 +117,14 @@ def _load_project_cells(project_dir: str) -> Dict[str, dict]:
def _ordered_cell_names(cells: Dict[str, dict]) -> List[str]:
"""Order cells so dependencies are built before cells that reference them."""
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 _cells_have_links(cells: Dict[str, dict]) -> bool:
"""Detect whether any saved cell contains bundle links that require routed building."""
for data in cells.values():
for bundle in (data.get("bundles") or {}).values():
if bundle.get("links"):
@@ -120,12 +133,15 @@ def _cells_have_links(cells: Dict[str, dict]) -> bool:
def _build_with_gdstk(cells: Dict[str, dict], output_path: str, registry: PdkRegistry) -> BuildResult:
"""Assemble a project GDS with gdstk when Nazca or routed building is unavailable."""
import gdstk
library = gdstk.Library()
built_cells = {}
warnings = []
# Build composite cells before project cells so project-level references can
# reuse cells already inserted into the same GDS library.
for cell_name in _ordered_cell_names(cells):
data = cells[cell_name]
gds_cell = library.new_cell(_safe_cell_name(cell_name, built_cells))
@@ -137,6 +153,8 @@ def _build_with_gdstk(cells: Dict[str, dict], output_path: str, registry: PdkReg
rotation = math.radians(_number(instance.get("rotation")))
child = built_cells.get(component)
if child is None:
# External components are resolved through the active PDK
# registry and imported as references.
asset = registry.resolve(component)
if not asset.gds_path:
warnings.append(f"Missing GDS for {instance_name}: {component}")
@@ -154,10 +172,13 @@ def _build_with_gdstk(cells: Dict[str, dict], output_path: str, registry: PdkReg
def _import_public_gds(gdstk, library, gds_path: str):
"""Import public PDK GDS geometry into the output library."""
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}")
# Avoid adding duplicate cell names when multiple instances reference the
# same imported PDK component.
for source_cell in source.cells:
if _library_cell_by_name(library, source_cell.name) is None:
library.add(source_cell)
@@ -165,11 +186,14 @@ def _import_public_gds(gdstk, library, gds_path: str):
def _build_with_nazca(cells: Dict[str, dict], output_path: str, registry: PdkRegistry) -> BuildResult:
"""Assemble a project GDS with Nazca cells and PDK component placements."""
import nazca as nd
warnings = []
built_cells = {}
ordered_names = _ordered_cell_names(cells)
# Nazca cells are built in dependency order and then the final project cell
# is exported as the top-level GDS.
for cell_name in ordered_names:
data = cells[cell_name]
with nd.Cell(cell_name) as current_cell:
@@ -195,6 +219,7 @@ def _build_with_nazca(cells: Dict[str, dict], output_path: str, registry: PdkReg
def _safe_cell_name(name: str, existing: dict) -> str:
"""Generate a backend-safe unique cell name for GDS/Nazca libraries."""
base = "".join(ch if ch.isalnum() or ch in "._$" else "_" for ch in str(name)) or "cell"
candidate = base
counter = 1
@@ -206,6 +231,7 @@ def _safe_cell_name(name: str, existing: dict) -> str:
def _library_cell_by_name(library, name: str):
"""Find a cell object in a loaded layout library by name."""
for cell in library.cells:
if cell.name == name:
return cell
@@ -213,6 +239,7 @@ def _library_cell_by_name(library, name: str):
def _number(value, default=0.0) -> float:
"""Convert numeric YAML values to floats with a stable default."""
try:
if value is None or value == "":
return default