diff --git a/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py b/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py index dd77c45d..54814ce2 100644 --- a/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py +++ b/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py @@ -118,6 +118,15 @@ _LIVING_AREA_FRACTION_MIN: Final[float] = 0.13 _PENCE_TO_GBP: Final[float] = 0.01 _DEFAULT_THERMAL_MASS_PARAMETER_KJ_PER_M2_K: Final[float] = 250.0 _DEFAULT_PUMPS_FANS_KWH_PER_YR: Final[float] = 130.0 +# SAP10.2 Table 4f cascade — annual pumps + fans electricity by main +# heating system category. The Elmhurst gas-combi cohort lodges 115 +# (230c central heating pump, post-2013 install) + 45 (230e main +# heating flue fan, balanced/condensing) = 160 kWh/yr. Heat pumps, +# warm-air, oil/biomass, electric storage etc. use different rows +# (Table 4f spec lines 7905-8076) — deferred until a fixture exercises. +_PUMPS_FANS_KWH_BY_MAIN_CATEGORY: Final[dict[int, float]] = { + 2: 160.0, # Gas-fired boilers (115 pump + 45 flue fan) +} # SAP10.2 Table 6d note 1: "average or unknown" overshading is the # default for existing dwellings. RdSAP doesn't lodge a per-dwelling # overshading code so §5 always uses AVERAGE → Z_L = 0.83. @@ -1033,6 +1042,10 @@ def cert_to_inputs( main_code = main.sap_main_heating_code if main is not None else None main_category = main.main_heating_category if main is not None else None main_fuel = _main_fuel_code(main) + pumps_fans_kwh = _PUMPS_FANS_KWH_BY_MAIN_CATEGORY.get( + main_category if main_category is not None else -1, + _DEFAULT_PUMPS_FANS_KWH_PER_YR, + ) primary_age = ( epc.sap_building_parts[0].construction_age_band if epc.sap_building_parts else None ) @@ -1287,7 +1300,7 @@ def cert_to_inputs( thermal_mass_parameter_kj_per_m2_k=_DEFAULT_THERMAL_MASS_PARAMETER_KJ_PER_M2_K, main_heating_efficiency=eff, hot_water_kwh_per_yr=hw_kwh, - pumps_fans_kwh_per_yr=_DEFAULT_PUMPS_FANS_KWH_PER_YR, + pumps_fans_kwh_per_yr=pumps_fans_kwh, lighting_kwh_per_yr=lighting_kwh, space_heating_fuel_cost_gbp_per_kwh=_space_heating_fuel_cost_gbp_per_kwh( main, epc.sap_energy_source.meter_type, prices @@ -1320,7 +1333,7 @@ def cert_to_inputs( main=main, energy_requirements_result=energy_requirements_result, hot_water_kwh=hw_kwh, - pumps_fans_kwh=_DEFAULT_PUMPS_FANS_KWH_PER_YR, + pumps_fans_kwh=pumps_fans_kwh, lighting_kwh=lighting_kwh, cooling_kwh=energy_requirements_result.cooling_fuel_kwh_per_yr, ), diff --git a/packages/domain/src/domain/sap/worksheet/tests/test_e2e_elmhurst_sap_score.py b/packages/domain/src/domain/sap/worksheet/tests/test_e2e_elmhurst_sap_score.py index 9a565217..3a7358f6 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/test_e2e_elmhurst_sap_score.py +++ b/packages/domain/src/domain/sap/worksheet/tests/test_e2e_elmhurst_sap_score.py @@ -217,6 +217,37 @@ def test_elmhurst_end_to_end_lighting_kwh_per_yr_matches_u985_worksheet( assert result.lighting_kwh_per_yr == pytest.approx(expected_kwh, abs=1e-4) +@pytest.mark.parametrize( + "fixture, expected_kwh", + [ + (_w000474, 160.0), + (_w000490, 160.0), + ], + ids=["000474", "000490"], +) +def test_elmhurst_end_to_end_pumps_fans_kwh_matches_u985_worksheet( + fixture: object, expected_kwh: float +) -> None: + """Component-level pin on `SapResult.pumps_fans_kwh_per_yr` for the + e2e fixtures. PDF (231) for both 000474 + 000490: 115 (central + heating pump (230c)) + 45 (main heating flue fan (230e)) = 160. + + Pre-fix `cert_to_inputs` hardcoded 130 kWh/yr via + `_DEFAULT_PUMPS_FANS_KWH_PER_YR`. The shortfall (-30 kWh × elec + price = -£4) was the dominant remaining residual on 000490 after + Appendix L + secondary heating + ventilation closures — pushed + continuous SAP +0.38 over PDF → integer 58 vs 57. + """ + # Arrange + epc = fixture.build_epc() # type: ignore[attr-defined] + + # Act + result = Sap10Calculator().calculate(epc) + + # Assert + assert result.pumps_fans_kwh_per_yr == pytest.approx(expected_kwh, abs=1e-3) + + def test_elmhurst_000490_end_to_end_secondary_heating_fuel_kwh_matches_u985_worksheet() -> None: """Component-level e2e pin on `SapResult.secondary_heating_fuel_kwh_per_yr` for 000490 — cert lodges secondary heating system "Electricity Electric