From d1ae87c7e9a711be1cf9a0a9cd32c0a2f2eba55b Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 3 Jun 2026 16:10:42 +0000 Subject: [PATCH] S0380.206: Eq D1 Q_space uses the DHW boiler's own (204) share, not (202) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SAP 10.2 Appendix D §D2.1(2) Equation D1 blends the monthly water-heater efficiency by the ratio of the boiler's space-heating load to its water load. On a dual-main cert the DHW boiler does only its OWN share of space heating ((204) for Main 1, (205) for Main 2), but the cascade fed Eq D1 the dwelling total ((202) = 1 − secondary). That over-weighted η_winter and under-stated HW fuel — simulated case 6 (Main 1 serves DHW + 51% of space heat) was HW −78 kWh vs the worksheet. New `_water_heating_main_space_fraction` returns the DHW main's total- space share via `_water_heating_main` (WHC-901 → Main 1 (204); WHC-914 → Main 2 (205)); single-main / WHC-901 single systems get (202) = 1 − (201), so they are unchanged. Case 6 (219) HW now 4902.8601 EXACT. With S0380.205 (demand exact), case 6 now closes to 1e-4 on EVERY metric: SAP cont 71.6597, ECF 2.0316, cost 1162.5374, (211)+(213) 14736.9564, (219) 4902.8601, (231) 356, (232) 357.6571, CO2 5953.6679 (rating) / 4895.2137 (demand). Re-pin: 0240 (dual combi, WHC 901, Main 1 51%) HW rises slightly → PE +1.6893 → +1.8687, CO2 +0.0815 → +0.0907 (SAP 72 unchanged). Single-main certs unchanged (2360 pass + 0 fail). Co-Authored-By: Claude Opus 4.8 --- .../sap10_calculator/rdsap/cert_to_inputs.py | 37 ++++++++++++++++++- .../rdsap/test_golden_fixtures.py | 13 +++++-- .../_elmhurst_worksheet_001431_case6.py | 6 +++ .../worksheet/test_section_cascade_pins.py | 17 +++++++++ 4 files changed, 69 insertions(+), 4 deletions(-) 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,