diff --git a/domain/sap10_calculator/calculator.py b/domain/sap10_calculator/calculator.py index 43226da1..364ad23d 100644 --- a/domain/sap10_calculator/calculator.py +++ b/domain/sap10_calculator/calculator.py @@ -178,6 +178,14 @@ class CalculatorInputs: hot_water_kwh_per_yr: float pumps_fans_kwh_per_yr: float lighting_kwh_per_yr: float + # Unregulated annual delivered electricity — output-only, NOT fed + # into ECF / cost / CO2 / primary energy / sap_score (regulated + # energy only). Surfaced for ADR-0014 BillDerivation's APPLIANCES + + # COOKING sections. `cooking_kwh_per_yr` is the SAP 10.2 Appendix L + # L20 (p.91) ELECTRICITY figure (138 + 28×N), not the L18 cooking + # heat gain. `appliances_kwh_per_yr` is the L13/L14/L16a annual E_A. + appliances_kwh_per_yr: float + cooking_kwh_per_yr: float space_heating_fuel_cost_gbp_per_kwh: float hot_water_fuel_cost_gbp_per_kwh: float other_fuel_cost_gbp_per_kwh: float @@ -357,6 +365,15 @@ class SapResult: hot_water_kwh_per_yr: float pumps_fans_kwh_per_yr: float lighting_kwh_per_yr: float + # Unregulated annual delivered electricity for ADR-0014 + # BillDerivation (APPLIANCES + COOKING sections). Output-only — these + # do NOT contribute to ecf / total_fuel_cost_gbp / co2_kg_per_yr / + # primary_energy_kwh_per_yr / sap_score. `cooking_kwh_per_yr` is the + # SAP 10.2 Appendix L L20 (p.91) ELECTRICITY estimate (138 + 28×N); + # the bill adapter should treat it as an electricity carrier (a + # gas-cooker split, if ever needed, is a separate follow-up). + appliances_kwh_per_yr: float + cooking_kwh_per_yr: float primary_energy_kwh_per_yr: float primary_energy_kwh_per_m2: float monthly: tuple[MonthlyEntry, ...] @@ -745,6 +762,8 @@ def calculate_sap_from_inputs(inputs: CalculatorInputs) -> SapResult: hot_water_kwh_per_yr=inputs.hot_water_kwh_per_yr, pumps_fans_kwh_per_yr=inputs.pumps_fans_kwh_per_yr, lighting_kwh_per_yr=inputs.lighting_kwh_per_yr, + appliances_kwh_per_yr=inputs.appliances_kwh_per_yr, + cooking_kwh_per_yr=inputs.cooking_kwh_per_yr, primary_energy_kwh_per_yr=primary_energy_kwh, primary_energy_kwh_per_m2=primary_energy_per_m2, monthly=monthly, diff --git a/domain/sap10_calculator/rdsap/cert_to_inputs.py b/domain/sap10_calculator/rdsap/cert_to_inputs.py index 83b4a378..5e3f5a77 100644 --- a/domain/sap10_calculator/rdsap/cert_to_inputs.py +++ b/domain/sap10_calculator/rdsap/cert_to_inputs.py @@ -6163,6 +6163,13 @@ def cert_to_inputs( hot_water_kwh_per_yr=hw_kwh, pumps_fans_kwh_per_yr=pumps_fans_kwh, lighting_kwh_per_yr=lighting_kwh, + # Unregulated annual delivered electricity for ADR-0014 + # BillDerivation — output-only, NOT wired into cost / CO2 / PE. + # Appliances: SAP 10.2 Appendix L L13/L14/L16a (sum of the §5 + # (68) monthly E_A). Cooking: Appendix L L20 (p.91) ELECTRICITY + # E_cook = 138 + 28×N, already summed in `cooking_monthly_kwh`. + appliances_kwh_per_yr=sum(appliances_monthly_kwh), + cooking_kwh_per_yr=sum(cooking_monthly_kwh), space_heating_fuel_cost_gbp_per_kwh=_space_heating_fuel_cost_gbp_per_kwh( main, _rdsap_tariff(epc), prices ), diff --git a/domain/sap10_calculator/tests/test_bre_worked_examples.py b/domain/sap10_calculator/tests/test_bre_worked_examples.py index 7aac35ee..274de785 100644 --- a/domain/sap10_calculator/tests/test_bre_worked_examples.py +++ b/domain/sap10_calculator/tests/test_bre_worked_examples.py @@ -115,6 +115,11 @@ def _baseline_dwelling() -> CalculatorInputs: hot_water_kwh_per_yr=2400.0, pumps_fans_kwh_per_yr=100.0, lighting_kwh_per_yr=600.0, + # Non-zero on purpose: unregulated loads that must NOT leak into + # cost / CO2 / PE / sap_score (the reconciliation assertions sum + # only the regulated end-uses, so a leak would surface here). + appliances_kwh_per_yr=2000.0, + cooking_kwh_per_yr=200.0, space_heating_fuel_cost_gbp_per_kwh=0.07, hot_water_fuel_cost_gbp_per_kwh=0.07, other_fuel_cost_gbp_per_kwh=0.07, diff --git a/domain/sap10_calculator/tests/test_calculator.py b/domain/sap10_calculator/tests/test_calculator.py index 9a5e3dfc..37e56d16 100644 --- a/domain/sap10_calculator/tests/test_calculator.py +++ b/domain/sap10_calculator/tests/test_calculator.py @@ -119,6 +119,11 @@ def _baseline_inputs() -> CalculatorInputs: hot_water_kwh_per_yr=2400.0, pumps_fans_kwh_per_yr=100.0, lighting_kwh_per_yr=600.0, + # Non-zero on purpose: these unregulated loads must NOT leak into + # cost / CO2 / PE / sap_score. The reconciliation assertions in + # this file sum only the regulated end-uses, so a leak surfaces here. + appliances_kwh_per_yr=2000.0, + cooking_kwh_per_yr=200.0, space_heating_fuel_cost_gbp_per_kwh=0.07, hot_water_fuel_cost_gbp_per_kwh=0.07, other_fuel_cost_gbp_per_kwh=0.07, diff --git a/domain/sap10_calculator/worksheet/tests/test_e2e_elmhurst_sap_score.py b/domain/sap10_calculator/worksheet/tests/test_e2e_elmhurst_sap_score.py index a51d6711..3b385b60 100644 --- a/domain/sap10_calculator/worksheet/tests/test_e2e_elmhurst_sap_score.py +++ b/domain/sap10_calculator/worksheet/tests/test_e2e_elmhurst_sap_score.py @@ -25,7 +25,10 @@ from typing import Final import pytest from domain.sap10_calculator.calculator import Sap10Calculator -from domain.sap10_calculator.rdsap.cert_to_inputs import cert_to_inputs +from domain.sap10_calculator.rdsap.cert_to_inputs import ( + cert_to_inputs, + water_heating_section_from_cert, +) from domain.sap10_calculator.worksheet.tests import ( _elmhurst_worksheet_000474 as _w000474, _elmhurst_worksheet_000477 as _w000477, @@ -220,3 +223,36 @@ def test_elmhurst_cert_to_inputs_monthly_infiltration_ach_matches_u985_worksheet assert inputs.monthly_infiltration_ach[m] == pytest.approx( fixture.LINE_25_EFFECTIVE_ACH[m], abs=1e-3 ), f"(25) month {m+1} drift" + + +def test_appliances_and_cooking_kwh_threaded_onto_sap_result() -> None: + """Appliances + cooking annual delivered electricity reach SapResult. + + ADR-0014 BillDerivation prices an APPLIANCES and a COOKING section, + so the unregulated electricity loads the rating cascade already + computes must be surfaced on SapResult (previously a downstream + adapter stubbed them at 0 kWh, understating the bill). Cooking is + the SAP 10.2 Appendix L L20 (p.91) ELECTRICITY estimate E_cook = + 138 + 28 × N (N = assumed occupancy) — distinct from the L18 cooking + heat GAIN (35 + 7N W) the §5 internal-gains cascade uses. Appliances + is the L13/L14/L16a annual E_A distributed monthly. + """ + # Arrange — a normal main-only gas-combi cert (non-zero TFA → non- + # zero appliances + cooking). + fixture = _FIXTURE_MODULES['000516'] + epc = fixture.build_epc() + water_heating = water_heating_section_from_cert(epc) + assert water_heating is not None # 000516 has a TFA, so HW is present + expected_cooking_kwh = 138.0 + 28.0 * water_heating.occupancy # Appendix L L20 + + # Act + inputs = cert_to_inputs(epc) + result = Sap10Calculator().calculate(epc) + + # Assert — both fields populated and threaded unchanged from + # CalculatorInputs onto SapResult; cooking equals the L20 electricity + # figure (NOT the larger L18 gain) to 1e-9. + assert inputs.appliances_kwh_per_yr > 0.0 + assert result.appliances_kwh_per_yr == inputs.appliances_kwh_per_yr + assert result.cooking_kwh_per_yr == inputs.cooking_kwh_per_yr + assert abs(result.cooking_kwh_per_yr - expected_cooking_kwh) <= 1e-9 diff --git a/tests/domain/property_baseline/test_calculator_rebaseliner.py b/tests/domain/property_baseline/test_calculator_rebaseliner.py index f22e152f..b20408a5 100644 --- a/tests/domain/property_baseline/test_calculator_rebaseliner.py +++ b/tests/domain/property_baseline/test_calculator_rebaseliner.py @@ -47,6 +47,8 @@ def _sap_result( hot_water_kwh_per_yr=0.0, pumps_fans_kwh_per_yr=0.0, lighting_kwh_per_yr=0.0, + appliances_kwh_per_yr=0.0, + cooking_kwh_per_yr=0.0, primary_energy_kwh_per_yr=0.0, primary_energy_kwh_per_m2=primary_energy_kwh_per_m2, monthly=(),