From 3f5cd550cb6790c989cad5d1688d8052c846f45d Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 24 Jun 2026 17:45:29 +0000 Subject: [PATCH] =?UTF-8?q?Thread=20the=20off-peak=20meter=20flag=20and=20?= =?UTF-8?q?high-rate=20fractions=20onto=20SapResult=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- domain/sap10_calculator/calculator.py | 22 +++++++++++++ .../sap10_calculator/test_calculator.py | 31 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/domain/sap10_calculator/calculator.py b/domain/sap10_calculator/calculator.py index 0b406d2a..c30234a7 100644 --- a/domain/sap10_calculator/calculator.py +++ b/domain/sap10_calculator/calculator.py @@ -355,6 +355,21 @@ class CalculatorInputs: main_2_heating_fuel_code: Optional[int] = None secondary_heating_fuel_code: Optional[int] = None hot_water_fuel_code: Optional[int] = None + # Off-Peak Meter day/night billing metadata for ADR-0014 Bill Derivation — + # output-only (NOT fed into cost / CO2 / PE / sap_score, already priced via + # the per-end-use cost factor fields above). `is_off_peak_meter` routes every + # electric end use to the off-peak carrier; the per-end-use High-Rate + # Fractions (SAP 10.2 Table 12a) drive the day/night split. cert_to_inputs + # supplies them from the same Table-12a helpers the cost cascade uses; + # defaults (`False` + 1.0) keep synthetic / standard-tariff constructions a + # no-op. They thread byte-identical onto `SapResult`. + is_off_peak_meter: bool = False + main_heating_high_rate_fraction: float = 1.0 + main_2_heating_high_rate_fraction: float = 1.0 + secondary_heating_high_rate_fraction: float = 1.0 + hot_water_high_rate_fraction: float = 1.0 + pumps_fans_high_rate_fraction: float = 1.0 + other_electricity_high_rate_fraction: float = 1.0 @dataclass(frozen=True) @@ -875,6 +890,13 @@ def calculate_sap_from_inputs(inputs: CalculatorInputs) -> SapResult: primary_energy_kwh_per_m2=primary_energy_per_m2, monthly=monthly, intermediate=intermediate, + is_off_peak_meter=inputs.is_off_peak_meter, + main_heating_high_rate_fraction=inputs.main_heating_high_rate_fraction, + main_2_heating_high_rate_fraction=inputs.main_2_heating_high_rate_fraction, + secondary_heating_high_rate_fraction=inputs.secondary_heating_high_rate_fraction, + hot_water_high_rate_fraction=inputs.hot_water_high_rate_fraction, + pumps_fans_high_rate_fraction=inputs.pumps_fans_high_rate_fraction, + other_electricity_high_rate_fraction=inputs.other_electricity_high_rate_fraction, ) diff --git a/tests/domain/sap10_calculator/test_calculator.py b/tests/domain/sap10_calculator/test_calculator.py index 5444a140..7ab7d3c0 100644 --- a/tests/domain/sap10_calculator/test_calculator.py +++ b/tests/domain/sap10_calculator/test_calculator.py @@ -165,6 +165,37 @@ def test_fuel_codes_and_pv_export_thread_unchanged_onto_sap_result() -> None: assert abs(result.pv_exported_kwh_per_yr - 850.0) <= 1e-9 +def test_off_peak_meter_flag_and_high_rate_fractions_thread_onto_sap_result() -> None: + """The Off-Peak Meter flag + per-end-use High-Rate Fractions surface on + SapResult unchanged (ADR-0014). Output-only metadata for Bill Derivation's + day/night split — they must thread byte-identical from CalculatorInputs + through `calculate_sap_from_inputs` and NOT perturb the SAP cost/score.""" + # Arrange — an off-peak dwelling: storage heating all-night (0.0), HW immersion + # all-night (0.0), other uses 0.90 / pumps 0.71 (7-hour Grid-2 fractions). + inputs = replace( + _baseline_inputs(), + is_off_peak_meter=True, + main_heating_high_rate_fraction=0.0, + main_2_heating_high_rate_fraction=0.5, + secondary_heating_high_rate_fraction=1.0, + hot_water_high_rate_fraction=0.0, + pumps_fans_high_rate_fraction=0.71, + other_electricity_high_rate_fraction=0.90, + ) + + # Act + result = calculate_sap_from_inputs(inputs) + + # Assert — threaded unchanged. + assert result.is_off_peak_meter is True + assert result.main_heating_high_rate_fraction == 0.0 + assert result.main_2_heating_high_rate_fraction == 0.5 + assert result.secondary_heating_high_rate_fraction == 1.0 + assert result.hot_water_high_rate_fraction == 0.0 + assert result.pumps_fans_high_rate_fraction == 0.71 + assert result.other_electricity_high_rate_fraction == 0.90 + + def test_pv_export_collapses_none_input_to_zero_on_sap_result() -> None: """`pv_exported_kwh_per_yr` is 0.0 (not None) on SapResult for no-PV.