Model/domain/modelling/scoring/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

55 lines
2.2 KiB
Python

"""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 Optional, Sequence
from datatypes.epc.domain.epc_property_data import EpcPropertyData
from domain.modelling.scoring.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_result` is the calculator output the headline figures were taken from,
carried so Bill Derivation can price the scored end-state without a second
`calculate` (ADR-0014 amendment). The optimiser never reads it — it works
off `sap_continuous` only — so it stays domain-agnostic and a stub scorer
may leave it `None`."""
sap_continuous: float
co2_kg_per_yr: float
primary_energy_kwh_per_yr: float
sap_result: Optional[SapResult] = None
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,
sap_result=result,
)