"""Load YAML technology manifests into typed technology model objects.""" from dataclasses import dataclass from pathlib import Path from typing import Any, Dict, Optional, Tuple import yaml from .layer_models import LayerSpec, MaterialSpec, XSectionLayerSpec, XSectionSpec TECHNOLOGIES_ROOT = Path(__file__).resolve().parent @dataclass(frozen=True) class TechnologyManifest: """Parsed technology manifest with typed runtime specs.""" path: Path raw: Dict[str, Any] constants: Dict[str, Any] layers: Dict[str, LayerSpec] xsections: Dict[str, XSectionSpec] materials: Dict[str, MaterialSpec] def load_technology_manifest(manifest: str) -> TechnologyManifest: """Read a YAML manifest and convert it to typed technology specs.""" manifest_path = _manifest_path(manifest) with manifest_path.open("r", encoding="utf-8") as file: raw = yaml.safe_load(file) or {} if not isinstance(raw, dict): raise TypeError("Technology manifest must contain a YAML mapping.") return TechnologyManifest( path=manifest_path, raw=raw, constants=dict(raw.get("constants", {})), layers=_load_layers(raw.get("layers", {})), xsections=_load_xsections(raw.get("xsections", {})), materials=_load_materials(raw.get("materials", {}), manifest_path.parent), ) def _manifest_path(manifest: str) -> Path: """Resolve a manifest path relative to mxpic/technologies.""" path = Path(manifest) if not path.is_absolute(): path = TECHNOLOGIES_ROOT / path path = path.resolve() if not path.exists(): raise FileNotFoundError(f"Technology manifest not found: {path}") return path def _load_layers(entries: Dict[str, Any]) -> Dict[str, LayerSpec]: """Convert manifest layer mappings to LayerSpec objects.""" layers: Dict[str, LayerSpec] = {} for name, entry in entries.items(): if not isinstance(entry, dict): raise TypeError(f"Layer '{name}' must be a mapping.") layer = entry.get("layer") if not isinstance(layer, list) or len(layer) != 2: raise TypeError(f"Layer '{name}' must define layer as [layer, datatype].") layers[name] = LayerSpec( name=name, layer=(int(layer[0]), int(layer[1])), aliases=tuple(entry.get("aliases", ())), material=entry.get("material"), z_start=entry.get("z_start"), thickness=entry.get("thickness"), sidewall_angle=entry.get("sidewall_angle"), process=entry.get("process"), description=entry.get("description", ""), ) return layers def _load_xsections(entries: Dict[str, Any]) -> Dict[str, XSectionSpec]: """Convert manifest xsection mappings to XSectionSpec objects.""" xsections: Dict[str, XSectionSpec] = {} for name, entry in entries.items(): if not isinstance(entry, dict): raise TypeError(f"Xsection '{name}' must be a mapping.") xsection_layers = tuple( _load_xsection_layer(name, layer_entry) for layer_entry in entry.get("layers", ()) ) xsections[name] = XSectionSpec(name=name, layers=xsection_layers) return xsections def _load_xsection_layer(xsection_name: str, entry: Dict[str, Any]) -> XSectionLayerSpec: """Convert one manifest xsection-layer entry.""" if not isinstance(entry, dict): raise TypeError(f"Xsection '{xsection_name}' layer entry must be a mapping.") return XSectionLayerSpec( layer=entry["layer"], growx=entry.get("growx"), growy=entry.get("growy"), leftedge=_optional_tuple(entry.get("leftedge")), rightedge=_optional_tuple(entry.get("rightedge")), overwrite=entry.get("overwrite", True), ) def _optional_tuple(value: Optional[list]) -> Optional[Tuple[Any, ...]]: """Normalize optional YAML lists used by Nazca edge definitions.""" if value is None: return None return tuple(value) def _load_materials(entries: Dict[str, Any], base_path: Path) -> Dict[str, MaterialSpec]: """Convert material metadata and resolve relative material data paths.""" materials: Dict[str, MaterialSpec] = {} for name, entry in entries.items(): if not isinstance(entry, dict): raise TypeError(f"Material '{name}' must be a mapping.") data_file = entry.get("data_file") if data_file is not None: data_file = str((base_path / data_file).resolve()) materials[name] = MaterialSpec( name=name, display_name=entry.get("display_name"), source_file=entry.get("source_file"), source_revision=entry.get("source_revision"), data_file=data_file, notes=entry.get("notes", ""), ) return materials