Technolgy file archetecture revised with dictionary input method
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user