140 lines
4.8 KiB
Python
140 lines
4.8 KiB
Python
"""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
|