Spaces:
Sleeping
Sleeping
| """Reference source β the pluggable boundary for *truth* SIP measures against. | |
| Pack-shaped architecture spec Β§3a. The reference/solver is the *thing that | |
| varies* β precomputed AirfRANS data, experimental data, a cached solver result, | |
| or (downstream) a live OpenFOAM run. It must live behind a contract, not be | |
| hardcoded, exactly as the model under test lives behind ``ModelAdapter``. This is | |
| that contract, defined as **one whole interface now** (serve + generate) so | |
| nothing forks between Product A (serve) and Product B (generate): Product B | |
| *exercises more of the same interface* rather than reconciling a second one. | |
| Two capabilities, one interface: | |
| - **serve** β :meth:`ReferenceSource.reference` returns truth for inputs that | |
| *have* answers (precomputed datasets, experimental data, cached results). | |
| Directly analogous to ``ModelAdapter.predict`` β same shape, opposite source | |
| (truth vs. prediction). This is what Product A's evidence path exercises. | |
| - **generate** β :meth:`ReferenceSource.generate` produces truth for *novel* | |
| inputs with no answer yet (run the solver). The heavier capability Product | |
| B's mitigation loop exercises. The contract is fixed now; a working | |
| generating source (VTK/OpenFOAM parsing, fieldβQoI reduction, convergence) | |
| is the downstream solver-ingest build β a separate spec β not this one. | |
| **Capability-detection, not a dead method** (Β§3a): a serve-only source declares | |
| ``supports_generate() β False`` and does not carry a live ``generate``; the | |
| consumer asks before invoking. A whole interface with a declared *optional* | |
| capability, never a present-but-dead method. | |
| **Firewall placement β the same for both halves** (Β§3a, decided once for the | |
| whole interface so the Product-B *generate* half can't later be bolted on with a | |
| different stance): a reference source feeds *truth into the measurement | |
| comparison*, whether looked-up (*serve*) or computed (*generate*). Both halves | |
| sit on the **reference side** of the firewall β they provide what SIP measures | |
| against; they never produce verdicts, decisions, or actions. (Distinct from the | |
| action/decision region governed by the Β§4 two-scope signing.) | |
| SIP has NO native solver/dataset support: a user (or a premium pack) wraps their | |
| truth source in a ``ReferenceSource`` subclass, exactly as ``adapter.py`` wraps a | |
| model. SIP never imports VTK/OpenFOAM/airfrans; the source does. | |
| """ | |
| from __future__ import annotations | |
| import importlib | |
| import importlib.util | |
| from abc import ABC, abstractmethod | |
| from pathlib import Path | |
| from typing import Any | |
| from uofa_cli.interrogate.loader import Reference, load_reference | |
| _DATA_SUFFIXES = {".npz", ".json"} | |
| class ReferenceSource(ABC): | |
| """One contract for truth: serve precomputed answers, optionally generate new ones. | |
| Subclasses implement :meth:`reference` (serve). They MAY additionally support | |
| generation by overriding :meth:`supports_generate` to return True and | |
| implementing :meth:`generate`; a serve-only source leaves both at their | |
| defaults (no dead method). Mirrors the ``ModelAdapter`` ABC style β a thin, | |
| single-responsibility interface a pack implements, resolved by reference. | |
| """ | |
| def reference(self, inputs: Any = None) -> dict[str, Any]: | |
| """Serve truth: ``{qoi_name: reference_values}`` for the evaluation points. | |
| The serve analog of ``ModelAdapter.predict`` β same ``{qoi: array}`` shape, | |
| opposite source. ``inputs`` is accepted to mirror ``predict`` and to let a | |
| generating source key off the points; a precomputed source already holds | |
| the answers and may ignore it. | |
| """ | |
| raise NotImplementedError | |
| def constraint_fields(self) -> dict[str, Any]: | |
| """Residual fields for declared physics constraints (``{constraintId: field}``). | |
| Reference-side supporting data SIP compares against; empty when the source | |
| carries none. Not a measurement and not a verdict. | |
| """ | |
| return {} | |
| def uq_intervals(self) -> dict[str, tuple[Any, Any]]: | |
| """Prediction-interval bounds per QoI (``{qoi: (lower, upper)}``) for UQ coverage.""" | |
| return {} | |
| def supports_generate(self) -> bool: | |
| """Whether this source can *generate* truth for novel inputs (capability-detection). | |
| Defaults to False β a serve-only source. The consumer asks this before | |
| invoking :meth:`generate`, so a serve-only source carries no dead method. | |
| """ | |
| return False | |
| def generate(self, inputs: Any) -> dict[str, Any]: | |
| """Generate truth for *novel* inputs (run the solver). Optional capability. | |
| Default raises β a serve-only source declares it does not support | |
| generation via :meth:`supports_generate`. A generating source overrides | |
| both. Output sits on the reference side of the firewall, identical to | |
| :meth:`reference` (truth, never a verdict). Building a working generating | |
| source (the solver internals) is the downstream solver-ingest spec. | |
| """ | |
| raise NotImplementedError( | |
| f"{type(self).__name__} is serve-only and does not support generation. " | |
| f"Check supports_generate() before calling generate()." | |
| ) | |
| class FileReferenceSource(ReferenceSource): | |
| """Serve-only reference source backed by a precomputed ``.npz``/``.json`` file. | |
| The Product-A evidence path's source: it serves precomputed reference values, | |
| constraint residual fields, and UQ interval bounds (the conventions in | |
| ``loader``). It declares ``supports_generate() β False`` and carries no | |
| ``generate`` β capability-detection, not a no-op (Β§3a). Proves the interface | |
| end to end without any generating internals. | |
| """ | |
| def __init__(self, path: str | Path): | |
| self.path = Path(path) | |
| self._data: Reference = load_reference(self.path) | |
| def reference(self, inputs: Any = None) -> dict[str, Any]: | |
| return self._data.reference | |
| def constraint_fields(self) -> dict[str, Any]: | |
| return self._data.constraint_fields | |
| def uq_intervals(self) -> dict[str, tuple[Any, Any]]: | |
| return self._data.uq_intervals | |
| def to_reference(source: ReferenceSource, inputs: Any = None) -> Reference: | |
| """Materialize the in-memory :class:`Reference` SIP's measurements consume. | |
| The boundary between the pluggable source and SIP's internals: serve the | |
| three kinds of truth through the interface, then hand SIP the data container | |
| it already reads. Keeps the measurement methods unchanged while routing truth | |
| through ``ReferenceSource``. | |
| """ | |
| return Reference( | |
| reference=source.reference(inputs), | |
| constraint_fields=source.constraint_fields(), | |
| uq_intervals=source.uq_intervals(), | |
| ) | |
| def load_reference_source(ref: str | Path) -> ReferenceSource: | |
| """Resolve a ``ReferenceSource`` from a data-file path or a class reference. | |
| Accepted forms (mirrors ``adapter.load_adapter``, with a data-file shortcut): | |
| - ``"/path/to/reference.npz"`` / ``".json"`` β a :class:`FileReferenceSource` | |
| (the precomputed serve-only source). | |
| - ``"package.module:ClassName"`` / ``"/path/file.py:ClassName"`` β a custom | |
| (e.g. premium, possibly generating) source, imported and instantiated. | |
| Raises ``ValueError`` if a class reference does not resolve to a | |
| ``ReferenceSource`` subclass. | |
| """ | |
| ref_str = str(ref) | |
| if Path(ref_str).suffix.lower() in _DATA_SUFFIXES: | |
| return FileReferenceSource(ref_str) | |
| cls = _resolve_class(ref_str) | |
| if not (isinstance(cls, type) and issubclass(cls, ReferenceSource)): | |
| raise ValueError( | |
| f"{ref!r} does not resolve to a ReferenceSource subclass (got {cls!r}). " | |
| f"Subclass uofa_cli.interrogate.reference_source.ReferenceSource." | |
| ) | |
| return cls() | |
| def _resolve_class(ref: str): | |
| if ":" in ref and (ref.endswith(".py") or "/" in ref.split(":", 1)[0]): | |
| file_part, _, class_name = ref.partition(":") | |
| path = Path(file_part).expanduser().resolve() | |
| if not path.is_file(): | |
| raise FileNotFoundError(f"ReferenceSource file not found: {path}") | |
| spec = importlib.util.spec_from_file_location(path.stem, path) | |
| if spec is None or spec.loader is None: | |
| raise ImportError(f"Could not load ReferenceSource module from {path}") | |
| module = importlib.util.module_from_spec(spec) | |
| spec.loader.exec_module(module) | |
| return getattr(module, class_name) | |
| module_path, _, class_name = ref.rpartition(":") if ":" in ref else ref.rpartition(".") | |
| if not module_path: | |
| raise ValueError( | |
| f"ReferenceSource ref {ref!r} must be a .npz/.json path, " | |
| f"'pkg.module:ClassName', or '/path/file.py:ClassName'." | |
| ) | |
| module = importlib.import_module(module_path) | |
| return getattr(module, class_name) | |