mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
feat(baseline): Rebaseliner returns RebaselineResult carrying the SapResult
The Rebaseliner is the assemble-and-score step (ADR-0013 amendment); its
SapResult is the scored picture that Bill Derivation also prices (ADR-0014),
so rebaseline() now returns a RebaselineResult{effective, reason, sap_result}
instead of (Performance, reason). CalculatorRebaseliner sets sap_result on
both branches (the bill prices it whether lodged or calculated figures win);
StubRebaseliner returns sap_result=None (runs no calculator). Orchestrator
unpacks the result; the bill wiring lands in the next slice.
Also refreshes the stale ML-era docstrings in rebaseliner.py to the
assemble-and-score model (the calculator, not ML, is the rebaseliner
mechanism per ADR-0013).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
5e75fb474c
commit
f7dc9dbccb
5 changed files with 81 additions and 42 deletions
|
|
@ -4,7 +4,7 @@ import logging
|
|||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from domain.property_baseline.performance import Performance
|
||||
from domain.property_baseline.rebaseliner import Rebaseliner, RebaselineReason
|
||||
from domain.property_baseline.rebaseliner import Rebaseliner, RebaselineResult
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from datatypes.epc.domain.epc_property_data import EpcPropertyData
|
||||
|
|
@ -51,17 +51,23 @@ class CalculatorRebaseliner(Rebaseliner):
|
|||
|
||||
def rebaseline(
|
||||
self, property_id: int, effective_epc: "EpcPropertyData", lodged: Performance
|
||||
) -> tuple[Performance, RebaselineReason]:
|
||||
) -> RebaselineResult:
|
||||
# A raise (UnmappedSapCode, etc.) propagates: the calculator is
|
||||
# load-bearing, so the batch aborts and the cert is fixed at once.
|
||||
# load-bearing, so the batch aborts and the cert is fixed at once. The
|
||||
# SapResult rides on the result either way — Bill Derivation prices it
|
||||
# regardless of whether lodged or calculated figures win (ADR-0013/0014).
|
||||
result: SapResult = self._calculator.calculate(effective_epc)
|
||||
sap_version: Optional[float] = effective_epc.sap_version
|
||||
if sap_version is not None and sap_version < _MIN_TRUSTED_SAP_VERSION:
|
||||
return Performance.from_sap_result(result), "pre_sap10"
|
||||
return RebaselineResult(
|
||||
effective=Performance.from_sap_result(result),
|
||||
reason="pre_sap10",
|
||||
sap_result=result,
|
||||
)
|
||||
self._log_divergence(
|
||||
property_id=property_id, sap_version=sap_version, result=result, lodged=lodged
|
||||
)
|
||||
return lodged, "none"
|
||||
return RebaselineResult(effective=lodged, reason="none", sap_result=result)
|
||||
|
||||
def _log_divergence(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -1,15 +1,20 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Literal
|
||||
from dataclasses import dataclass
|
||||
from typing import Literal, Optional, TYPE_CHECKING
|
||||
|
||||
from datatypes.epc.domain.epc_property_data import EpcPropertyData
|
||||
from domain.property_baseline.performance import Performance
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from domain.sap10_calculator.calculator import SapResult
|
||||
|
||||
RebaselineReason = Literal["none", "pre_sap10", "physical_state_changed", "both"]
|
||||
|
||||
# The SAP spec version below which a cert's recorded scores reflect a superseded
|
||||
# methodology and must be ML-rebaselined (CONTEXT.md: Rebaselining).
|
||||
# methodology and must be rebaselined to the calculator's output (CONTEXT.md:
|
||||
# Rebaselining).
|
||||
_SAP10_FLOOR = 10.0
|
||||
|
||||
|
||||
|
|
@ -23,40 +28,60 @@ class RebaselineNotImplemented(Exception):
|
|||
"""
|
||||
|
||||
|
||||
class Rebaseliner(ABC):
|
||||
"""Produces a Property's Effective Performance from its Effective EPC.
|
||||
@dataclass(frozen=True)
|
||||
class RebaselineResult:
|
||||
"""The outcome of Rebaselining a Property: its Effective Performance, why it
|
||||
differs from Lodged, and the calculator `SapResult` it was scored from.
|
||||
|
||||
Rebaselining (CONTEXT.md) re-predicts the rated quantities via ML when the
|
||||
EPC was lodged pre-SAP10 or its physical state diverged from the lodged EPC;
|
||||
``sap_result`` is the scored picture (ADR-0013 amendment) — a first-class
|
||||
part of the result because Bill Derivation prices the *same* scoring
|
||||
(ADR-0014). It is ``None`` only for a Rebaseliner that ran no calculator (the
|
||||
test ``StubRebaseliner``); the load-bearing ``CalculatorRebaseliner`` always
|
||||
sets it.
|
||||
"""
|
||||
|
||||
effective: Performance
|
||||
reason: RebaselineReason
|
||||
sap_result: Optional["SapResult"]
|
||||
|
||||
|
||||
class Rebaseliner(ABC):
|
||||
"""Produces a Property's Effective Performance by Rebaselining its Effective EPC.
|
||||
|
||||
Rebaselining (CONTEXT.md) assembles the Effective EPC picture and scores it
|
||||
through SAP10 Calculation, replacing the recorded scores when the EPC was
|
||||
lodged pre-SAP10 or its physical state diverged from the lodged EPC;
|
||||
otherwise Effective Performance equals Lodged. Injected into the
|
||||
PropertyBaselineOrchestrator (ADR-0011) so the ML adapter can swap in without
|
||||
touching the orchestrator, and so the single-property re-score-on-override
|
||||
flow reuses the same port.
|
||||
PropertyBaselineOrchestrator (ADR-0011) so the implementation can swap
|
||||
without touching the orchestrator, and so the single-property
|
||||
re-score-on-override flow reuses the same port.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def rebaseline(
|
||||
self, property_id: int, effective_epc: EpcPropertyData, lodged: Performance
|
||||
) -> tuple[Performance, RebaselineReason]: ...
|
||||
) -> RebaselineResult: ...
|
||||
|
||||
|
||||
class StubRebaseliner(Rebaseliner):
|
||||
"""A no-calculator stub for tests that don't want the real calculator.
|
||||
|
||||
SAP10 certs pass through untouched — Effective Performance equals Lodged,
|
||||
reason ``"none"``. A pre-SAP10 cert genuinely needs rebaselining, which this
|
||||
stub does not do, so it raises rather than fabricating a "none". Production
|
||||
uses ``CalculatorRebaseliner`` (the calculator is load-bearing — ADR-0013
|
||||
amendment); this stub stays for orchestrator/repo unit tests.
|
||||
reason ``"none"``, and ``sap_result`` is ``None`` (no calculator ran). A
|
||||
pre-SAP10 cert genuinely needs rebaselining, which this stub does not do, so
|
||||
it raises rather than fabricating a "none". Production uses
|
||||
``CalculatorRebaseliner`` (the calculator is load-bearing — ADR-0013
|
||||
amendment); this stub stays for orchestrator/repo unit tests that don't
|
||||
exercise the bill.
|
||||
"""
|
||||
|
||||
def rebaseline(
|
||||
self, property_id: int, effective_epc: EpcPropertyData, lodged: Performance
|
||||
) -> tuple[Performance, RebaselineReason]:
|
||||
) -> RebaselineResult:
|
||||
sap_version = effective_epc.sap_version
|
||||
if sap_version is not None and sap_version < _SAP10_FLOOR:
|
||||
raise RebaselineNotImplemented(
|
||||
f"Property needs rebaselining (pre-SAP10 cert, sap_version="
|
||||
f"{sap_version}); ML rebaselining is not implemented yet"
|
||||
f"{sap_version}); this stub does not run the calculator"
|
||||
)
|
||||
return lodged, "none"
|
||||
return RebaselineResult(effective=lodged, reason="none", sap_result=None)
|
||||
|
|
|
|||
|
|
@ -42,14 +42,14 @@ class PropertyBaselineOrchestrator:
|
|||
for property_id, prop in zip(property_ids, properties, strict=True):
|
||||
effective_epc = prop.effective_epc
|
||||
lodged = lodged_performance(effective_epc)
|
||||
effective, reason = self._rebaseliner.rebaseline(
|
||||
rebaselined = self._rebaseliner.rebaseline(
|
||||
property_id, effective_epc, lodged
|
||||
)
|
||||
rhi = _require_rhi(effective_epc)
|
||||
baseline = PropertyBaselinePerformance(
|
||||
lodged=lodged,
|
||||
effective=effective,
|
||||
rebaseline_reason=reason,
|
||||
effective=rebaselined.effective,
|
||||
rebaseline_reason=rebaselined.reason,
|
||||
space_heating_kwh=rhi.space_heating_kwh,
|
||||
water_heating_kwh=rhi.water_heating_kwh,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -72,39 +72,45 @@ class _StubCalculator(SapCalculator):
|
|||
def test_pre_10_2_cert_is_rebaselined_to_the_calculator_output() -> None:
|
||||
# Arrange — a SAP 10.0 cert: lodged figures are a superseded methodology, so
|
||||
# the calculator's output becomes Effective Performance (ADR-0013 amendment).
|
||||
calculator = _StubCalculator(
|
||||
_sap_result(sap_score=70, co2_kg_per_yr=1900.0, primary_energy_kwh_per_m2=185.4)
|
||||
sap_result = _sap_result(
|
||||
sap_score=70, co2_kg_per_yr=1900.0, primary_energy_kwh_per_m2=185.4
|
||||
)
|
||||
calculator = _StubCalculator(sap_result)
|
||||
rebaseliner = CalculatorRebaseliner(calculator)
|
||||
epc = _epc(sap_version=10.0)
|
||||
|
||||
# Act
|
||||
effective, reason = rebaseliner.rebaseline(
|
||||
result = rebaseliner.rebaseline(
|
||||
property_id=10, effective_epc=epc, lodged=_lodged()
|
||||
)
|
||||
|
||||
# Assert — calculated Performance: band from the score, CO2 kg->t, PEUI rounded.
|
||||
assert effective == Performance(
|
||||
# Assert — calculated Performance: band from the score, CO2 kg->t, PEUI rounded;
|
||||
# the SapResult rides on the result for Bill Derivation.
|
||||
assert result.effective == Performance(
|
||||
sap_score=70, epc_band=Epc.C, co2_emissions=1.9, primary_energy_intensity=185
|
||||
)
|
||||
assert reason == "pre_sap10"
|
||||
assert result.reason == "pre_sap10"
|
||||
assert result.sap_result is sap_result
|
||||
|
||||
|
||||
def test_a_10_2_cert_keeps_the_lodged_figures() -> None:
|
||||
# Arrange — a SAP 10.2 cert: the API's lodged figures are on-target, so they
|
||||
# stand; the calculator runs only to validate.
|
||||
calculator = _StubCalculator(_sap_result(sap_score=72))
|
||||
sap_result = _sap_result(sap_score=72)
|
||||
calculator = _StubCalculator(sap_result)
|
||||
rebaseliner = CalculatorRebaseliner(calculator)
|
||||
epc = _epc(sap_version=10.2)
|
||||
|
||||
# Act
|
||||
effective, reason = rebaseliner.rebaseline(
|
||||
result = rebaseliner.rebaseline(
|
||||
property_id=10, effective_epc=epc, lodged=_lodged()
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert effective == _lodged()
|
||||
assert reason == "none"
|
||||
# Assert — lodged kept as effective, but the SapResult still rides along for
|
||||
# Bill Derivation (the bill prices it regardless of which figures win).
|
||||
assert result.effective == _lodged()
|
||||
assert result.reason == "none"
|
||||
assert result.sap_result is sap_result
|
||||
|
||||
|
||||
def test_a_10_2_cert_logs_divergence_when_the_calculator_disagrees(
|
||||
|
|
|
|||
|
|
@ -29,16 +29,18 @@ def test_sap10_epc_is_not_rebaselined_so_effective_equals_lodged() -> None:
|
|||
rebaseliner = StubRebaseliner()
|
||||
|
||||
# Act
|
||||
effective, reason = rebaseliner.rebaseline(10, epc, lodged)
|
||||
result = rebaseliner.rebaseline(10, epc, lodged)
|
||||
|
||||
# Assert — Effective Performance equals Lodged, reason "none".
|
||||
assert effective == lodged
|
||||
assert reason == "none"
|
||||
# Assert — Effective Performance equals Lodged, reason "none", no SapResult
|
||||
# (the stub runs no calculator).
|
||||
assert result.effective == lodged
|
||||
assert result.reason == "none"
|
||||
assert result.sap_result is None
|
||||
|
||||
|
||||
def test_pre_sap10_epc_raises_because_rebaselining_is_not_implemented() -> None:
|
||||
# Arrange — a cert lodged under a pre-SAP10 schema genuinely needs ML
|
||||
# rebaselining, which does not exist yet; the stub must not fabricate a
|
||||
# Arrange — a cert lodged under a pre-SAP10 schema genuinely needs
|
||||
# rebaselining, which this stub does not do; it must not fabricate a
|
||||
# "none" answer for it.
|
||||
epc = _epc(sap_version=9.94)
|
||||
rebaseliner = StubRebaseliner()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue