diff --git a/domain/sap10_calculator/rdsap/cert_to_inputs.py b/domain/sap10_calculator/rdsap/cert_to_inputs.py index 6cec1313..13a7cf62 100644 --- a/domain/sap10_calculator/rdsap/cert_to_inputs.py +++ b/domain/sap10_calculator/rdsap/cert_to_inputs.py @@ -1479,6 +1479,35 @@ def _water_heating_main( return details[0] +def _water_heating_main_space_fraction( + epc: EpcPropertyData, secondary_fraction: float +) -> float: + """Fraction of TOTAL space heating provided by the DHW boiler — the + SAP 10.2 Appendix D §D2.1(2) Equation D1 Q_space weight. + + Eq D1's monthly water-heater efficiency blends η_winter / η_summer by + the ratio of the boiler's space-heating load to its water load. On a + single-main / WHC-901 cert that load is the whole main share, + (202) = 1 − (201). On a dual-main cert the DHW boiler does ONLY its + own share — (204) for Main 1, (205) for Main 2 — so feeding it the + dwelling total over-weights η_winter and under-states HW fuel + (simulated case 6: Main 1 serves DHW + 51% of space heat; using 100% + of demand gave HW −78 kWh vs the worksheet).""" + details = epc.sap_heating.main_heating_details if epc.sap_heating else [] + main_fraction = 1.0 - secondary_fraction # (202) + if len(details) < 2: + return main_fraction + main_2 = details[1] + main_2_of_main = ( + main_2.main_heating_fraction / 100.0 + if main_2.main_heating_fraction is not None + else 0.0 + ) + if _water_heating_main(epc) is details[1]: + return main_fraction * main_2_of_main # (205) — DHW from Main 2 + return main_fraction * (1.0 - main_2_of_main) # (204) — DHW from Main 1 + + def _rdsap_tariff(epc: EpcPropertyData) -> Tariff: """Resolve the cert's Table 12a tariff column via RdSAP 10 §12 Rules 1-4 (page 62). Consults BOTH main heating systems — §12 @@ -6329,8 +6358,14 @@ def cert_to_inputs( # Q_space (kWh/month) per spec = (98c)m × (204) = (98c)m × (1 − # sec_frac) for single-main fixtures. if wh_result is not None: + # Eq D1 Q_space is the DHW boiler's OWN space-heating load — its + # (204)/(205) share of total — not the dwelling total (202). See + # `_water_heating_main_space_fraction`. + water_main_space_fraction = _water_heating_main_space_fraction( + epc, secondary_fraction_value + ) space_heating_monthly_useful_kwh = tuple( - q * (1.0 - secondary_fraction_value) + q * water_main_space_fraction for q in space_heating_result.total_space_heating_monthly_kwh ) hw_kwh = _apply_water_efficiency( diff --git a/tests/domain/sap10_calculator/rdsap/test_golden_fixtures.py b/tests/domain/sap10_calculator/rdsap/test_golden_fixtures.py index 381895b5..b17cc6f5 100644 --- a/tests/domain/sap10_calculator/rdsap/test_golden_fixtures.py +++ b/tests/domain/sap10_calculator/rdsap/test_golden_fixtures.py @@ -83,8 +83,8 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( cert_number="0240-0200-5706-2365-8010", actual_sap=73, expected_sap_resid=-1, - expected_pe_resid_kwh_per_m2=+1.6893, - expected_co2_resid_tonnes_per_yr=+0.0815, + expected_pe_resid_kwh_per_m2=+1.8687, + expected_co2_resid_tonnes_per_yr=+0.0907, notes=( "Detached house, TFA 118, age J, oil boiler PCDB-listed + PV + " "RR on BP[0]. Mapper DOES extract sap_room_in_roof.room_in_roof_" @@ -176,7 +176,14 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( "two-control blend. Lowers MIT ~0.037 °C → space-heating demand " "falls → PE +2.1519 → +1.6893, CO2 +0.1051 → +0.0815 (both " "closer to zero; SAP integer 72 unchanged). Verified 1e-4 " - "against the case-6 worksheet (87)/(90)/(98c)." + "against the case-6 worksheet (87)/(90)/(98c). " + "Slice S0380.206 fed Eq D1 the DHW boiler's OWN (204) space " + "share (Main 1 = 51%) instead of the dwelling total (202) — " + "the worksheet-validated case-6 fix that lands its (219) HW " + "exact. For 0240 this raises HW fuel slightly → PE +1.6893 → " + "+1.8687, CO2 +0.0815 → +0.0907 (SAP 72 unchanged). The lodged " + "73 carries Elmhurst's own residual; case 6 is the spec " + "authority per [[feedback-worksheet-not-api-reference]]." ), ), _GoldenExpectation( diff --git a/tests/domain/sap10_calculator/worksheet/_elmhurst_worksheet_001431_case6.py b/tests/domain/sap10_calculator/worksheet/_elmhurst_worksheet_001431_case6.py index 95e4824f..0ef2714e 100644 --- a/tests/domain/sap10_calculator/worksheet/_elmhurst_worksheet_001431_case6.py +++ b/tests/domain/sap10_calculator/worksheet/_elmhurst_worksheet_001431_case6.py @@ -92,6 +92,12 @@ LINE_231_PUMPS_FANS_KWH: Final[float] = 356.0 LINE_211_MAIN_1_FUEL_KWH: Final[float] = 7741.6458 LINE_213_MAIN_2_FUEL_KWH: Final[float] = 6995.3106 +# Worksheet (219) water-heating fuel (kWh/yr). The DHW boiler is Main 1 +# (WHC 901), which provides only 51% of space heating, so SAP 10.2 +# Appendix D Eq D1 weights η_winter by Main 1's (204) share — not the +# dwelling total — when blending the monthly water-heater efficiency. +LINE_219_HOT_WATER_FUEL_KWH: Final[float] = 4902.8601 + # Worksheet (70) "Pumps, fans" internal-gain (W), heating-season only # (Jun-Sep = 0). = 10 W = the two-main-system central-heating-pump pair # per SAP 10.2 Table 5a note a): Main 1 ("2013 or later" → 3 W) + Main 2 diff --git a/tests/domain/sap10_calculator/worksheet/test_section_cascade_pins.py b/tests/domain/sap10_calculator/worksheet/test_section_cascade_pins.py index 9ec6975f..2835eb16 100644 --- a/tests/domain/sap10_calculator/worksheet/test_section_cascade_pins.py +++ b/tests/domain/sap10_calculator/worksheet/test_section_cascade_pins.py @@ -301,6 +301,23 @@ def test_case6_main_2_emitter_and_control_extracted() -> None: assert main_2.main_heating_control == 2110 +def test_section_4_hot_water_fuel_case6_match_pdf() -> None: + """(219) water-heating fuel for simulated case 6. The DHW boiler (Main + 1, WHC 901) provides only 51% of space heating, so SAP 10.2 Appendix D + §D2.1(2) Equation D1 must weight η_winter by Main 1's (204) share, not + the dwelling total (202). Pre-S0380.206 the cascade fed Eq D1 the full + dwelling space load → over-weighted η_winter → HW −78 kWh.""" + # Arrange / Act — real cascade (the §2.4 helper skips the cylinder gate). + ci = cert_to_inputs(_w001431_case6.build_epc()) + + # Assert + _pin( + ci.hot_water_kwh_per_yr, + _w001431_case6.LINE_219_HOT_WATER_FUEL_KWH, + "§4 (219) case6", + ) + + def test_section_9a_per_system_fuel_case6_match_pdf() -> None: """(211)/(213) per-system space-heating fuel for simulated case 6. The dual oil boiler heats different parts (Main 1 radiators/2106 living,