Persistent dev-fixture for energieffektivisering (energiledelse/M&V), valgt for sin lærings-overflate: gapet mellom modellert besparelse (validatoren regner) og faktisk realisert besparelse i drift (eksperten kjenner) — det ExpeL skal lære. Ett kontorbygg, ett LED-retrofit-tiltak. OKF-bundle (index/project/hypothesis/ methodology/reference/verdict) bærer kontekst-laget; verdict-led-fro.md koder realiseringsgraden (RR ≈ 0,82, forankret i National Grid SBS 2010) som ExpeL-frø. Energi mappet inn i den EKSISTERENDE kost-IR-en uendret (affected = byggets totale energikostnad, claimed = modellert besparelse ~10 % < 30 %-cap), så validatoren kjører som-den-er — src/ urørt. golden.json fryser de seeded percentilene; testen beviser at fixturen er konsumerbar (validerer, ikke Rejection), ikke bare til stede. Domenetall verifisert mot primærkilder (EVO/IPMVP, DOE/NREL UMP, CPUC, fire evalueringsstudier); norsk energipris mot SSB Q1 2026. README + shared/README oppdatert (eksempel finnes, ikke lenger "planned"). Suite 121/4, ruff+mypy rene. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MHR8iKxJRxDiDfNw8HZmWE
123 lines
5.1 KiB
Python
123 lines
5.1 KiB
Python
"""Bygg-energi mikro-eksempel — fixture-konsumerbarhet (shared/examples/bygg-energi-mikro).
|
|
|
|
The energi micro-fixture is a *dev fixture* exercised through the whole build. These tests
|
|
prove it is actually consumable, not merely present:
|
|
|
|
1. the IR projection (``validator-input.json``) drives the EXISTING deterministic validator to
|
|
a ``ValidatedProposal`` (not a ``Rejection``) — energi mapped into the cost-IR unchanged;
|
|
2. that validator output is frozen against ``golden.json`` (seeded ``_MC_SEED=20260624``);
|
|
3. the OKF bundle is well-formed (every file declares ``type``; index cross-links resolve);
|
|
4. the seed verdict encodes a realization gap (RR < 1) consistent with the golden — the anchor
|
|
the later step-1 ExpeL wiring becomes load-bearing against.
|
|
|
|
No ``yaml`` dependency: frontmatter is parsed with a minimal key:value reader.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from portfolio_optimiser.ir import AffectedItem, SavingsProposal
|
|
from portfolio_optimiser.validator import Rejection, ValidatedProposal, validate_proposal
|
|
|
|
BUNDLE_DIR = Path(__file__).resolve().parents[1] / "shared" / "examples" / "bygg-energi-mikro"
|
|
|
|
_EXPECTED_TYPES = {
|
|
"index.md": "index",
|
|
"bygg-kontor-nord.md": "project",
|
|
"tiltak-led-retrofit.md": "hypothesis",
|
|
"metode-ipmvp-a.md": "methodology",
|
|
"kilder-realiseringsgap.md": "reference",
|
|
"verdict-led-fro.md": "verdict",
|
|
}
|
|
|
|
|
|
def _parse_frontmatter(path: Path) -> dict[str, str]:
|
|
"""Minimal YAML-frontmatter reader: the ``---``-delimited leading block as key:value
|
|
strings. Enough for ``type`` and the verdict's scalar fields; list values (``tags: [...]``)
|
|
are kept verbatim and unused."""
|
|
lines = path.read_text(encoding="utf-8").splitlines()
|
|
if not lines or lines[0].strip() != "---":
|
|
return {}
|
|
fm: dict[str, str] = {}
|
|
for line in lines[1:]:
|
|
if line.strip() == "---":
|
|
break
|
|
key, sep, val = line.partition(":")
|
|
if sep:
|
|
fm[key.strip()] = val.strip()
|
|
return fm
|
|
|
|
|
|
def _proposal_from_input() -> SavingsProposal:
|
|
raw = json.loads((BUNDLE_DIR / "validator-input.json").read_text(encoding="utf-8"))
|
|
return SavingsProposal(
|
|
project_id=raw["project_id"],
|
|
measure=raw["measure"],
|
|
affected_items=[AffectedItem(**a) for a in raw["affected_items"]],
|
|
claimed_saving_nok=raw["claimed_saving_nok"],
|
|
assumptions={k: tuple(v) for k, v in raw["assumptions"].items()},
|
|
)
|
|
|
|
|
|
def _golden() -> dict:
|
|
return json.loads((BUNDLE_DIR / "golden.json").read_text(encoding="utf-8"))
|
|
|
|
|
|
def test_validator_input_validates() -> None:
|
|
"""The core: the energi tiltak, mapped into the cost-IR, passes the blocking validator —
|
|
a ``ValidatedProposal``, never a ``Rejection`` (claimed 30k < P90 feasible)."""
|
|
result = validate_proposal(_proposal_from_input())
|
|
assert isinstance(result, ValidatedProposal)
|
|
assert not isinstance(result, Rejection)
|
|
assert result.proposal.claimed_saving_nok < result.p90
|
|
assert result.p10 <= result.p50 <= result.p90
|
|
|
|
|
|
def test_validator_output_matches_golden() -> None:
|
|
"""Regression: the deterministic (seeded) percentiles match the frozen golden."""
|
|
result = validate_proposal(_proposal_from_input())
|
|
assert isinstance(result, ValidatedProposal)
|
|
g = _golden()["validator"]
|
|
assert g["validates"] is True
|
|
assert result.nominal_feasible == pytest.approx(g["nominal_feasible"])
|
|
assert result.p10 == pytest.approx(g["p10"])
|
|
assert result.p50 == pytest.approx(g["p50"])
|
|
assert result.p90 == pytest.approx(g["p90"])
|
|
|
|
|
|
def test_bundle_files_declare_type() -> None:
|
|
"""Every OKF bundle file carries the one required field (``type``) with the expected value."""
|
|
for name, expected_type in _EXPECTED_TYPES.items():
|
|
fm = _parse_frontmatter(BUNDLE_DIR / name)
|
|
assert fm.get("type") == expected_type, f"{name}: type={fm.get('type')!r}"
|
|
|
|
|
|
def test_index_crosslinks_resolve() -> None:
|
|
"""Intra-bundle cross-links in ``index.md`` resolve to existing files (our own example
|
|
must have no broken links, even though OKF consumers must tolerate them)."""
|
|
index = (BUNDLE_DIR / "index.md").read_text(encoding="utf-8")
|
|
targets = re.findall(r"\]\(([^)]+\.md)\)", index)
|
|
internal = [t for t in targets if "/" not in t]
|
|
assert internal, "expected intra-bundle .md links in index.md"
|
|
for target in internal:
|
|
assert (BUNDLE_DIR / target).exists(), f"index links to missing {target}"
|
|
|
|
|
|
def test_verdict_seed_encodes_realization_gap() -> None:
|
|
"""The seed verdict encodes a realization gap (RR < 1), and the golden learning-surface is
|
|
internally consistent (expected_actual = RR x modelled). This is the anchor the later
|
|
step-1 ExpeL wiring is made load-bearing against."""
|
|
fm = _parse_frontmatter(BUNDLE_DIR / "verdict-led-fro.md")
|
|
rr = float(fm["realization_rate"])
|
|
assert 0.0 < rr < 1.0, "a realization gap means RR strictly below 1"
|
|
|
|
ls = _golden()["learning_surface"]
|
|
assert ls["realization_rate"] == pytest.approx(rr)
|
|
assert ls["expected_actual_saving_nok"] == pytest.approx(
|
|
ls["realization_rate"] * ls["modelled_saving_nok"]
|
|
)
|