Spaces:
Sleeping
Sleeping
File size: 8,947 Bytes
a28ec65 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | """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.
"""
@abstractmethod
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)
|