refactor(baseline): Performance.from_sap_result replaces the loose mapper

PR feedback: the SapResult -> Performance mapping should be a method, not a
free function you must know exists in the rebaseliner. Put the factory on
the target as `Performance.from_sap_result`, beside its sibling
`lodged_performance` and mirroring `Epc.from_sap_score` (the factory this
mapping already calls).

Not a `SapResult.to_performance()`: that would make the SAP calculator
import `Performance` (a property_baseline type), re-introducing the
engine->consumer coupling removed by the SapCalculator ABC. SapResult is a
TYPE_CHECKING-only import in performance.py (the body only reads attributes),
so the calculator module is not pulled in at runtime.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-02 13:59:25 +00:00 committed by Jun-te Kim
parent 8d2ff23e2f
commit 23f01685f9
2 changed files with 20 additions and 14 deletions

View file

@ -3,7 +3,6 @@ from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Optional
from datatypes.epc.domain.epc import Epc
from domain.property_baseline.performance import Performance
from domain.property_baseline.rebaseliner import Rebaseliner, RebaselineReason
@ -22,17 +21,6 @@ _REL_TOL = 0.01
_KG_PER_TONNE = 1000.0
def performance_from_sap_result(result: "SapResult") -> Performance:
"""The four rated quantities, read off a `SapResult`: band derived from the
score, CO2 converted kgtonnes, PEUI rounded to the lodged integer scale."""
return Performance(
sap_score=result.sap_score,
epc_band=Epc.from_sap_score(result.sap_score),
co2_emissions=result.co2_kg_per_yr / _KG_PER_TONNE,
primary_energy_intensity=round(result.primary_energy_kwh_per_m2),
)
def _relative_diff(calculated: float, lodged: float) -> float:
if lodged == 0:
return 0.0 if calculated == 0 else float("inf")
@ -62,7 +50,7 @@ class CalculatorRebaseliner(Rebaseliner):
result: SapResult = self._calculator.calculate(effective_epc)
sap_version: Optional[float] = effective_epc.sap_version
if sap_version is not None and sap_version < _SAP10_2_FLOOR:
return performance_from_sap_result(result), "pre_sap10"
return Performance.from_sap_result(result), "pre_sap10"
self._log_divergence(
property_id=property_id, sap_version=sap_version, result=result, lodged=lodged
)

View file

@ -1,12 +1,16 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Optional, TypeVar
from typing import Optional, TYPE_CHECKING, TypeVar
from datatypes.epc.domain.epc import Epc
from datatypes.epc.domain.epc_property_data import EpcPropertyData
if TYPE_CHECKING:
from domain.sap10_calculator.calculator import SapResult
_T = TypeVar("_T")
_KG_PER_TONNE = 1000.0
@dataclass(frozen=True)
@ -24,6 +28,20 @@ class Performance:
co2_emissions: float
primary_energy_intensity: int
@classmethod
def from_sap_result(cls, result: "SapResult") -> "Performance":
"""The four rated quantities, read off a calculator `SapResult`
(ADR-0013): band derived from the score, CO2 converted kgtonnes, PEUI
rounded to the lodged integer scale. The `from_*` factory mirrors
`Epc.from_sap_score`; living on the target keeps the SAP calculator
free of any `property_baseline` dependency."""
return cls(
sap_score=result.sap_score,
epc_band=Epc.from_sap_score(result.sap_score),
co2_emissions=result.co2_kg_per_yr / _KG_PER_TONNE,
primary_energy_intensity=round(result.primary_energy_kwh_per_m2),
)
def _require(value: Optional[_T], field: str) -> _T:
if value is None: