Technolgy file archetecture revised with dictionary input method

This commit is contained in:
=
2026-06-07 17:07:20 +08:00
parent 8a17f1dde0
commit 54d20eb154
163 changed files with 5948 additions and 1297 deletions
+139
View File
@@ -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