mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
feat(modelling): Package Scorer — compose overlays + score on the calculator
PackageScorer(calculator: SapCalculator).score(baseline, simulations) folds the Simulation Overlays onto the baseline via the Overlay Applicator and scores the throwaway EpcPropertyData on the injected deterministic SAP calculator, returning Score(sap_continuous, co2_kg_per_yr, primary_energy_kwh_per_yr). Depends on the SapCalculator abstraction, not a concrete engine. This is the reusable scoring primitive (ADR-0016) — the same call serves the optimiser's whole-package re-score and a future live re-score of a user-assembled plan. Two behaviour tests against the real Sap10Calculator on a hand-built EPD: filling the main cavity improves SAP (right-directional through the real physics); an empty package scores the unmodified baseline (pins the SapResult->Score mapping). The Elmhurst before/after cascade PIN (#1154's acceptance) lands once cert 001431 parses (external _extract_windows fix). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
bb2c0068ff
commit
7a478cff6e
2 changed files with 101 additions and 0 deletions
47
domain/modelling/package_scorer.py
Normal file
47
domain/modelling/package_scorer.py
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
"""The Package Scorer — the reusable scoring primitive (ADR-0016).
|
||||||
|
|
||||||
|
Composes an ordered set of Simulation Overlays onto a baseline EpcPropertyData
|
||||||
|
(via the Overlay Applicator) and scores the throwaway result on a deterministic
|
||||||
|
SAP calculator, returning the headline metrics. The same primitive powers the
|
||||||
|
optimiser's whole-package re-score and any future live re-score of a
|
||||||
|
user-assembled plan.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
|
from datatypes.epc.domain.epc_property_data import EpcPropertyData
|
||||||
|
from domain.modelling.overlay_applicator import apply_simulations
|
||||||
|
from domain.modelling.simulation import EpcSimulation
|
||||||
|
from domain.sap10_calculator.calculator import SapCalculator, SapResult
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Score:
|
||||||
|
"""The headline metrics of a scored package. `sap_continuous` is the
|
||||||
|
un-rounded SAP rating (used for deltas); carbon and primary energy are the
|
||||||
|
annual totals."""
|
||||||
|
|
||||||
|
sap_continuous: float
|
||||||
|
co2_kg_per_yr: float
|
||||||
|
primary_energy_kwh_per_yr: float
|
||||||
|
|
||||||
|
|
||||||
|
class PackageScorer:
|
||||||
|
"""Scores a package of Simulation Overlays against a baseline EpcPropertyData
|
||||||
|
on an injected SAP calculator (depends on the `SapCalculator` abstraction,
|
||||||
|
not a concrete engine)."""
|
||||||
|
|
||||||
|
def __init__(self, calculator: SapCalculator) -> None:
|
||||||
|
self._calculator = calculator
|
||||||
|
|
||||||
|
def score(
|
||||||
|
self, baseline: EpcPropertyData, simulations: Sequence[EpcSimulation]
|
||||||
|
) -> Score:
|
||||||
|
simulated: EpcPropertyData = apply_simulations(baseline, simulations)
|
||||||
|
result: SapResult = self._calculator.calculate(simulated)
|
||||||
|
return Score(
|
||||||
|
sap_continuous=result.sap_score_continuous,
|
||||||
|
co2_kg_per_yr=result.co2_kg_per_yr,
|
||||||
|
primary_energy_kwh_per_yr=result.primary_energy_kwh_per_yr,
|
||||||
|
)
|
||||||
54
tests/domain/modelling/test_package_scorer.py
Normal file
54
tests/domain/modelling/test_package_scorer.py
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
"""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.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
|
||||||
|
)
|
||||||
Loading…
Add table
Reference in a new issue