Model/tests/domain/modelling/test_package_scorer.py
Khalim Conn-Kowlessar 2bbc401f0d feat(modelling): Score carries the scored SapResult for billing
Score gains sap_result: Optional[SapResult], populated by PackageScorer with the
calculator output its headline figures came from. This lets the Modelling stage
price the post-package (and baseline) end-state via Bill Derivation reusing a
SapResult already computed by the optimiser's re-score / the orchestrator's
baseline score — no second calculate (ADR-0014 amendment). The optimiser reads
only sap_continuous, so it stays domain-agnostic and the stub scorers (which omit
sap_result) are unaffected — all optimiser tests pass unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 17:20:45 +00:00

71 lines
2.4 KiB
Python

"""Behaviour of the Package Scorer: composing Simulation Overlays onto a
baseline EpcPropertyData and scoring the result on the deterministic SAP10
calculator. The reusable compute primitive (ADR-0016). Elmhurst before/after
cascade pins land with #1154 once the cert parses; here we exercise the real
calculator on a hand-built EPD.
"""
from datatypes.epc.domain.epc_property_data import (
BuildingPartIdentifier,
EpcPropertyData,
)
from domain.modelling.scoring.package_scorer import PackageScorer, Score
from domain.modelling.simulation import BuildingPartOverlay, EpcSimulation
from domain.sap10_calculator.calculator import Sap10Calculator, SapResult
from tests.domain.sap10_calculator.worksheet._elmhurst_worksheet_000490 import (
build_epc,
)
_CAVITY_FILL = EpcSimulation(
building_parts={
BuildingPartIdentifier.MAIN: BuildingPartOverlay(wall_insulation_type=2)
}
)
def test_filling_the_main_cavity_improves_sap() -> None:
# Arrange
baseline: EpcPropertyData = build_epc() # MAIN: uninsulated cavity
scorer = PackageScorer(Sap10Calculator())
# Act
base: Score = scorer.score(baseline, [])
filled: Score = scorer.score(baseline, [_CAVITY_FILL])
# Assert
assert filled.sap_continuous > base.sap_continuous
def test_empty_package_scores_the_unmodified_baseline() -> None:
# Arrange
baseline: EpcPropertyData = build_epc()
calculator = Sap10Calculator()
direct: SapResult = calculator.calculate(baseline)
# Act
score: Score = PackageScorer(calculator).score(baseline, [])
# Assert
assert abs(score.sap_continuous - direct.sap_score_continuous) <= 1e-9
assert abs(score.co2_kg_per_yr - direct.co2_kg_per_yr) <= 1e-9
assert (
abs(score.primary_energy_kwh_per_yr - direct.primary_energy_kwh_per_yr)
<= 1e-9
)
def test_score_carries_the_scored_sap_result_for_billing() -> None:
# Arrange — the post-package SapResult must ride on the Score so Bill
# Derivation can price the simulated end-state without a second calculate
# (ADR-0014 amendment).
baseline: EpcPropertyData = build_epc()
scorer = PackageScorer(Sap10Calculator())
# Act
filled: Score = scorer.score(baseline, [_CAVITY_FILL])
# Assert — the SapResult is the one the Score's headline figures came from.
assert filled.sap_result is not None
assert (
abs(filled.sap_result.sap_score_continuous - filled.sap_continuous) <= 1e-9
)