Files
mxpic_forge/mxpic/technologies/manifest_loader.py
T

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