docs(shared): bygg-energi mikro-eksempel — OKF-bundle + golden + load-bearing test
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
This commit is contained in:
parent
dd8529e132
commit
cbc7a22c78
11 changed files with 457 additions and 3 deletions
123
tests/test_bygg_energi_mikro.py
Normal file
123
tests/test_bygg_energi_mikro.py
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
"""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"]
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue