S0380.206: Eq D1 Q_space uses the DHW boiler's own (204) share, not (202)

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 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-03 16:10:42 +00:00
parent e440e2df2e
commit d1ae87c7e9
4 changed files with 69 additions and 4 deletions

View file

@ -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(

View file

@ -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(

View file

@ -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

View file

@ -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,