diff --git a/domain/sap10_calculator/rdsap/cert_to_inputs.py b/domain/sap10_calculator/rdsap/cert_to_inputs.py index db29fa0a..68c91620 100644 --- a/domain/sap10_calculator/rdsap/cert_to_inputs.py +++ b/domain/sap10_calculator/rdsap/cert_to_inputs.py @@ -5924,6 +5924,27 @@ def cert_to_inputs( has_balanced_mv=_has_balanced_mechanical_ventilation(epc), ) ) + # SAP 10.2 Table 4f note c) (PDF p.175): "Where there are two main + # heating systems include two figures from this table." A genuine + # second SPACE-heating main therefore contributes its own circulation + # pump alongside Main 1's. The "second main heating system" test is the + # same one §9a uses to split space-heating demand: a lodged + # `main_heating_fraction > 0`. This excludes DHW-only second mains + # (e.g. cert 000565 Main 2 = gas combi via WHC 914, fraction 0 — water + # heating only, no space-heating circulation pump). Simulated case 6 + # (dual oil boiler, 51% rads + 49% underfloor) lodges Main 1 "2013 or + # later" (41 kWh) + Main 2 unknown-date (115 kWh) → worksheet (230c) + # central-heating pump = 41 + 115 = 156. The Main 2 oil-boiler aux + # (230d) is already summed in `_table_4f_additive_components`; this + # adds only the circulation pump. + _pumps_main_details = ( + epc.sap_heating.main_heating_details if epc.sap_heating else [] + ) + if len(_pumps_main_details) >= 2: + _pumps_main_2 = _pumps_main_details[1] + _pumps_main_2_fraction = _pumps_main_2.main_heating_fraction + if _pumps_main_2_fraction is not None and _pumps_main_2_fraction > 0: + pumps_fans_kwh += _table_4f_circulation_pump_kwh(_pumps_main_2) pumps_fans_kwh += _table_4f_additive_components(epc) # Track the MEV/MVHR-fan portion separately so the cost cascade can # apply Table 12a Grid 2 `FANS_FOR_MECH_VENT` (0.58 high-frac on diff --git a/tests/domain/sap10_calculator/rdsap/test_golden_fixtures.py b/tests/domain/sap10_calculator/rdsap/test_golden_fixtures.py index 03f5fe93..847e7d0d 100644 --- a/tests/domain/sap10_calculator/rdsap/test_golden_fixtures.py +++ b/tests/domain/sap10_calculator/rdsap/test_golden_fixtures.py @@ -82,9 +82,9 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( _GoldenExpectation( cert_number="0240-0200-5706-2365-8010", actual_sap=73, - expected_sap_resid=+0, - expected_pe_resid_kwh_per_m2=+1.9459, - expected_co2_resid_tonnes_per_yr=+0.1226, + expected_sap_resid=-1, + expected_pe_resid_kwh_per_m2=+2.8092, + expected_co2_resid_tonnes_per_yr=+0.1385, 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_" @@ -136,7 +136,25 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( "45°-inclined solar gains. Validated against the simulated-" "case-6 worksheet ((27a) U_eff 2.1062). The inclined solar " "gain dominates → SAP cont 72.14 → 72.55 (resid -1 → +0 " - "EXACT), PE +3.9138 → +1.9459, CO2 +0.2213 → +0.1226." + "EXACT), PE +3.9138 → +1.9459, CO2 +0.2213 → +0.1226. " + "Slice S0380.201 added the SECOND main heating system's " + "circulation pump per SAP 10.2 Table 4f note c) (PDF p.175) " + "\"Where there are two main heating systems include two " + "figures from this table\" — gated on a lodged " + "main_heating_fraction > 0 (a genuine second SPACE-heating " + "main, excluding DHW-only second mains). This cert is dual-" + "main oil combi 51%/49%; Main 2 pump_age unknown (115 kWh) " + "joins Main 1's 115 → cascade pumps_fans 315 → 430 (+115 " + "kWh/yr). The fix was validated against the simulated-case-6 " + "worksheet, whose (231) = 356 decomposes as (230c) central-" + "heating pump 156 (= Main 1 41 + Main 2 115) + (230d) oil " + "boiler pump 200 — proving the two-pump treatment is spec-" + "correct. Cascade SAP cont 72.55 → 72.18 (integer 73 → 72, " + "resid +0 → -1), PE +1.9459 → +2.8092, CO2 +0.1226 → " + "+0.1385. The lodged 73 carries Elmhurst's own rounding/" + "residual (this cert is API-only with no worksheet); the " + "worksheet-backed case 6 is the spec authority for the " + "archetype 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 94b6d7e8..57a07537 100644 --- a/tests/domain/sap10_calculator/worksheet/_elmhurst_worksheet_001431_case6.py +++ b/tests/domain/sap10_calculator/worksheet/_elmhurst_worksheet_001431_case6.py @@ -69,6 +69,14 @@ LINE_27_WINDOWS_W_PER_K: Final[float] = 22.7408 LINE_27A_ROOF_WINDOWS_W_PER_K: Final[float] = 13.0375 LINE_31_TOTAL_EXTERNAL_AREA_M2: Final[float] = 336.13 +# Worksheet (231) "Total electricity for the above, kWh/year" (Block 1). +# Decomposes as (230c) central heating pump 156 + (230d) oil boiler pump +# 200. (230c) = 41 (Main 1 circ pump, "2013 or later") + 115 (Main 2 circ +# pump, unknown date) — the two-main-system circulation-pump pair per +# SAP 10.2 Table 4f note c. (230d) = 2 × 100 oil-boiler aux (already +# wired in `_table_4f_additive_components`). +LINE_231_PUMPS_FANS_KWH: Final[float] = 356.0 + def _summary_pdf_to_textract_style_pages(pdf_path: Path) -> list[str]: """Convert a Summary PDF into the per-page text format the 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 396247f6..c8db9ec1 100644 --- a/tests/domain/sap10_calculator/worksheet/test_section_cascade_pins.py +++ b/tests/domain/sap10_calculator/worksheet/test_section_cascade_pins.py @@ -278,6 +278,31 @@ def test_section_3_roof_windows_case6_match_pdf() -> None: ) +def test_section_4f_pumps_fans_case6_match_pdf() -> None: + """(231) pumps/fans pin for simulated case 6 — a DUAL-oil-boiler + detached dwelling. Worksheet (231) = 356 = (230c) central heating + pump 156 + (230d) oil boiler pump 200. (230c) is itself the two- + main-system circulation-pump pair per SAP 10.2 Table 4f note c + ("Where there are two main heating systems include two figures from + this table"): Main 1 41 kWh (pump age "2013 or later") + Main 2 115 + kWh (pump age unknown). The pre-S0380.201 cascade summed only Main 1's + circulation pump (41) and gave (231) = 241.""" + from domain.sap10_calculator.calculator import calculate_sap_from_inputs + + # Arrange + epc = _w001431_case6.build_epc() + + # Act + result = calculate_sap_from_inputs(cert_to_inputs(epc)) + + # Assert + _pin( + result.pumps_fans_kwh_per_yr, + _w001431_case6.LINE_231_PUMPS_FANS_KWH, + "§4f (231) case6", + ) + + # ============================================================================ # §4 Water heating — LINE_42..LINE_65 scalar + monthly tuples # ============================================================================