Spaces:
Sleeping
Sleeping
| """Surrogate Interrogation Probe (SIP) — a measurement instrument. | |
| SIP loads a pre-trained physics-based surrogate, exercises it against a | |
| supplied benchmark and reference set, computes interrogation measurements, and | |
| emits a signed, provenance-bearing evidence bundle. The UofA surrogate pack | |
| ingests that bundle and checks it for completeness and consistency. The | |
| practitioner and the COU acceptance criteria render the credibility decision. | |
| **SIP never emits a verdict.** This is the firewall (``SIP_Evidence_Contract_Spec`` | |
| §8, ``AGENTS.md`` §12), enforced at schema validation, in the command surface, | |
| and by a CI guard — all keyed off the single forbidden-token list in | |
| :mod:`uofa_cli.interrogate.forbidden`. | |
| Only the stdlib is imported at module top level. Heavy measurement dependencies | |
| (numpy, conformal/UQ libraries, model frameworks) are imported lazily inside the | |
| functions that need them, so ``import uofa_cli.interrogate`` never pulls in torch. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| from pathlib import Path | |
| from typing import Any | |
| def run_interrogation( | |
| *, | |
| adapter_ref: str, | |
| benchmark_path: str | Path, | |
| reference_path: str | Path, | |
| scope: dict, | |
| output_path: str | Path, | |
| key_path: str | Path | None = None, | |
| bundle_id: str | None = None, | |
| generated_at: str | None = None, | |
| sip_version: str | None = None, | |
| seed: int | None = None, | |
| context_path: Path | None = None, | |
| ) -> dict: | |
| """Full SIP pipeline: load → measure → assemble → validate → (sign) → write. | |
| Returns ``{"bundle", "output_path", "signed", "hash", "signature"}``. | |
| ``scope`` is the parsed declared-scope dict (subject, trainingEnvelope, | |
| evaluationPoint/Region, declaredPhysicsConstraint, surrogateUQMethod, | |
| parentModelSnapshot, completeness). Validation against the frozen contract | |
| happens before any signature — a forbidden field never gets signed. | |
| """ | |
| import importlib.metadata | |
| import uuid | |
| from datetime import datetime, timezone | |
| from uofa_cli.interrogate import loader, orchestrator, packager | |
| from uofa_cli.interrogate import prov as prov_mod | |
| from uofa_cli.interrogate import reference_source as refsrc | |
| from uofa_cli.interrogate.adapter import load_adapter | |
| bundle_id = bundle_id or f"sip-bundle-{uuid.uuid4().hex[:12]}" | |
| generated_at = generated_at or datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") | |
| if sip_version is None: | |
| try: | |
| sip_version = importlib.metadata.version("uofa") | |
| except Exception: | |
| sip_version = "0.0.0" | |
| adapter = load_adapter(adapter_ref) | |
| benchmark = loader.load_benchmark(Path(benchmark_path)) | |
| # Truth flows through the ReferenceSource interface (§3a): the precomputed | |
| # file path resolves to a serve-only FileReferenceSource; a premium pack can | |
| # supply a custom (serve+generate) source by class reference instead. | |
| reference = refsrc.to_reference(refsrc.load_reference_source(Path(reference_path))) | |
| measurements, measurement_provenance, measured_families = orchestrator.run_measurements( | |
| adapter, benchmark, reference, scope, seed=seed | |
| ) | |
| scope_subject = scope.get("subject", {}) | |
| subject = { | |
| "surrogateId": scope_subject.get("surrogateId", "unspecified"), | |
| "modelVersion": scope_subject.get("modelVersion", "unspecified"), | |
| "surrogateType": scope_subject.get("surrogateType", "data-driven-emulator"), | |
| "modelFingerprint": scope_subject.get("modelFingerprint", "unspecified"), | |
| "adapterRef": adapter_ref, | |
| } | |
| declared_scope: dict[str, Any] = {} | |
| if "trainingEnvelope" in scope: | |
| declared_scope["trainingEnvelope"] = scope["trainingEnvelope"] | |
| if "evaluationPoint" in scope: | |
| declared_scope["evaluationPoint"] = scope["evaluationPoint"] | |
| if "evaluationRegion" in scope: | |
| declared_scope["evaluationRegion"] = scope["evaluationRegion"] | |
| declared_scope["declaredPhysicsConstraint"] = scope.get("declaredPhysicsConstraint", []) | |
| # Carry per-field scope provenance from `interrogate init` into the bundle | |
| # (Addendum A14.1) so the downstream reviewer sees where each field came from. | |
| if "scopeProvenance" in scope: | |
| declared_scope["scopeProvenance"] = scope["scopeProvenance"] | |
| completeness = { | |
| "fieldsPresent": measured_families, | |
| "fieldsDeliberatelyOmitted": scope.get("completeness", {}).get( | |
| "fieldsDeliberatelyOmitted", [] | |
| ), | |
| } | |
| libraries = sorted({ | |
| (p["producedBy"]["library"], p["producedBy"]["version"]) | |
| for p in measurement_provenance | |
| }) | |
| provenance = prov_mod.build_provenance( | |
| run_id=f"sip:run/{bundle_id}", | |
| generated_at=generated_at, | |
| surrogate_ref=f"sip:surrogate/{subject['surrogateId']}", | |
| benchmark_ref=f"sip:benchmark/{Path(benchmark_path).name}", | |
| reference_ref=f"sip:reference/{Path(reference_path).name}", | |
| bundle_id=f"sip:bundle/{bundle_id}", | |
| libraries=list(libraries), | |
| ) | |
| bundle = packager.assemble_bundle( | |
| bundle_id=bundle_id, | |
| sip_version=sip_version, | |
| generated_at=generated_at, | |
| subject=subject, | |
| declared_scope=declared_scope, | |
| measurement_provenance=measurement_provenance, | |
| measurements=measurements, | |
| completeness=completeness, | |
| provenance=provenance, | |
| parent_snapshot=scope.get("parentModelSnapshot"), | |
| ) | |
| output_path = Path(output_path) | |
| if key_path is not None: | |
| hash_hex, sig_hex = packager.emit_and_sign( | |
| bundle, output_path, Path(key_path), context_path=context_path | |
| ) | |
| signed_bundle = json.loads(output_path.read_text(encoding="utf-8")) | |
| return {"bundle": signed_bundle, "output_path": output_path, | |
| "signed": True, "hash": hash_hex, "signature": sig_hex} | |
| packager.emit_unsigned(bundle, output_path) | |
| return {"bundle": bundle, "output_path": output_path, | |
| "signed": False, "hash": None, "signature": None} | |