Thread the off-peak meter flag and high-rate fractions onto SapResult 🟩

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-24 17:45:29 +00:00
parent d3a4426ca4
commit 3f5cd550cb
2 changed files with 53 additions and 0 deletions

View file

@ -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,
)

View file

@ -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.