From 23f01685f9cfedf950ee5b0eed1e8b65393ad7e6 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 2 Jun 2026 13:59:25 +0000 Subject: [PATCH] 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 --- .../calculator_rebaseliner.py | 14 +------------ domain/property_baseline/performance.py | 20 ++++++++++++++++++- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/domain/property_baseline/calculator_rebaseliner.py b/domain/property_baseline/calculator_rebaseliner.py index c6519c83..184f56b0 100644 --- a/domain/property_baseline/calculator_rebaseliner.py +++ b/domain/property_baseline/calculator_rebaseliner.py @@ -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 kg→tonnes, 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 ) diff --git a/domain/property_baseline/performance.py b/domain/property_baseline/performance.py index 1db38846..b2ab45ce 100644 --- a/domain/property_baseline/performance.py +++ b/domain/property_baseline/performance.py @@ -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 kg→tonnes, 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: