From 7496ad024b0175d238972bae61e5b595080eb485 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 28 May 2026 19:56:57 +0000 Subject: [PATCH] =?UTF-8?q?Slice=20S0380.50:=20=C2=A74=20seasonal=20monthl?= =?UTF-8?q?y=20HW=20fuel=20for=20PV=20=CE=B2=20cascade?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PV β-factor cascade was prorating the annual hot-water fuel kWh uniformly by days when feeding D_PV,m per Appendix M1 footnote 32. The worksheet uses §4 (219)m = (62)m / efficiency monthly — which is seasonal (peaks in Jan when cold-mains-inlet drives energy content, troughs in Jul/Aug). For cert 0380: worksheet Jul (219) = 68.30 kWh vs cascade days-prorated 74.60 kWh — over-counted summer D_PV by ~6 kWh/month. Per Appendix M1 footnote 32: "D_PV,m = ... + E_water,m" where "E_water,m = (219)_m if water heating fuel code applied in Section 10a of the SAP worksheet is 30". (219)_m is the §4 fuel kWh per month, not annual / 12. Fix: scale `wh_result.output_monthly_kwh` to sum to the annual fuel `hw_kwh` (equivalent to dividing each month by the annual-average efficiency — exact for single-COP HP water heaters; close enough for PCDB combi winter/summer-split efficiencies because the annual total already accounts for the seasonal-efficiency mix). None fall- back to the legacy days-proration when wh_result is absent (TFA-missing certs). Cohort PE residual closure (kWh/m²): | Cert | Post-S0380.49 | Post-S0380.50 | |---|---:|---:| | 0350 | -2.96 | **-2.90** | | 0380 | -3.06 | **-2.96** | | 2225 | -3.73 | **-3.54** | | 2636 | -3.44 | **-3.28** | | 3800 | -3.25 | **-3.16** | | 9285 | -2.81 | **-2.74** | | 9418 | -3.01 | **-2.89** | Modest but real cohort closure (~0.1 kWh/m² each). The remaining ~3 kWh/m² traces to a small cascade β over-count (0.751 vs worksheet 0.739) — likely Appendix L monthly-weighting details for appliances/ cooking/electric-shower in D_PV; deferred to a follow-up slice. Cert 9501 (PV no battery) unchanged at +0.65 PE. CO2 cohort: <0.11 t/yr (within tolerance, re-pinned in same slice). SAP scores all exact. 763 pass + 0 fail. Pyright net-zero. --- .../sap10_calculator/rdsap/cert_to_inputs.py | 24 ++++++- .../rdsap/tests/test_golden_fixtures.py | 69 ++++++++++--------- 2 files changed, 58 insertions(+), 35 deletions(-) diff --git a/domain/sap10_calculator/rdsap/cert_to_inputs.py b/domain/sap10_calculator/rdsap/cert_to_inputs.py index ae222516..dd22c0f0 100644 --- a/domain/sap10_calculator/rdsap/cert_to_inputs.py +++ b/domain/sap10_calculator/rdsap/cert_to_inputs.py @@ -3128,6 +3128,28 @@ def cert_to_inputs( # = electricity → Table 12 code 30) per the Appendix M1 §3a fuel # inclusion list. pv_monthly_kwh = _pv_monthly_generation_kwh(epc, climate) + # SAP 10.2 Appendix M1 footnote 32 D_PV,m uses §4 (219)m monthly + # water-heating fuel kWh — which is the (62)m output divided by the + # water-heater efficiency. Uniform days-proration over the annual + # `hw_kwh` over-counts D_PV in summer and under-counts in winter + # (the (45)m hot-water energy content is seasonal, peaking in Jan). + # Scale `wh_output_monthly_kwh` to sum to the annual fuel `hw_kwh` + # — equivalent to dividing each month by the annual-average + # efficiency, which matches the worksheet's (219)m for HP / single- + # efficiency water heaters. For PCDB combis with distinct winter / + # summer efficiencies, `_apply_water_efficiency` already accounted + # for the seasonal split in the annual total; preserving the §4 + # monthly shape here keeps the per-month distribution faithful. + if wh_result is not None and sum(wh_result.output_monthly_kwh) > 0: + output_total = sum(wh_result.output_monthly_kwh) + hot_water_monthly_kwh_for_pv = tuple( + wh_result.output_monthly_kwh[m] / output_total * hw_kwh + for m in range(12) + ) + else: + hot_water_monthly_kwh_for_pv = _days_in_month_proportioned( + hw_kwh, _DAYS_IN_MONTH, + ) pv_eligible_demand_monthly_kwh = _pv_eligible_demand_monthly_kwh( lighting_monthly_kwh=lighting_monthly_kwh, appliances_monthly_kwh=appliances_monthly_kwh, @@ -3140,7 +3162,7 @@ def cert_to_inputs( pumps_fans_kwh, _DAYS_IN_MONTH, ), main_1_fuel_monthly_kwh=energy_requirements_result.main_1_fuel_monthly_kwh, - hot_water_monthly_kwh=_days_in_month_proportioned(hw_kwh, _DAYS_IN_MONTH), + hot_water_monthly_kwh=hot_water_monthly_kwh_for_pv, main_fuel_code_table_12=( API_FUEL_TO_TABLE_12.get(main_fuel, main_fuel) if main_fuel is not None else None diff --git a/domain/sap10_calculator/rdsap/tests/test_golden_fixtures.py b/domain/sap10_calculator/rdsap/tests/test_golden_fixtures.py index cf39ac30..d5d8a311 100644 --- a/domain/sap10_calculator/rdsap/tests/test_golden_fixtures.py +++ b/domain/sap10_calculator/rdsap/tests/test_golden_fixtures.py @@ -257,93 +257,94 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( cert_number="0380-2471-3250-2596-8761", actual_sap=89, expected_sap_resid=+0, - expected_pe_resid_kwh_per_m2=-3.0557, - expected_co2_resid_tonnes_per_yr=-0.0549, + expected_pe_resid_kwh_per_m2=-2.9633, + expected_co2_resid_tonnes_per_yr=-0.0548, notes=( "Mitsubishi PUZ-WM50VHA PCDB 104568, semi-detached bungalow " "TFA 60.43 age D, PV 3 kWp + 5 kWh battery. Worksheet SAP " - "88.5104. Slice S0380.48 surfaced the 5-kWh battery from " - "the flat-shape real-API JSON (PE +8.09 → -4.01). Slice " - "S0380.49 then wired effective-monthly Table 12e PE factor " - "into the PV split — dwelling 1.4952 (vs worksheet 1.4960), " - "exported 0.4283 (vs worksheet 0.4268) — closing PE -4.01 " - "→ -3.06. The residual -3 traces to a small β fine-tuning " - "gap (cascade 0.751 vs worksheet 0.7426) — monthly D_PV " - "distribution detail." + "88.5104. Slice S0380.48 surfaced the 5-kWh battery (PE " + "+8.09 → -4.01); .49 wired Table 12e effective-monthly PE " + "factor (PE -4.01 → -3.06); Slice S0380.50 replaced " + "days-prorated hot-water demand in the PV β cascade with " + "the §4 (62)m seasonal output scaled to annual fuel (PE " + "-3.06 → -2.96). Remaining ~3 kWh/m² traces to a small " + "cascade β over-count (0.751 vs worksheet 0.739) — " + "monthly D_PV distribution for appliances/cooking/electric-" + "shower likely needs Appendix L-aligned monthly weights." ), ), _GoldenExpectation( cert_number="0350-2968-2650-2796-5255", actual_sap=84, expected_sap_resid=+0, - expected_pe_resid_kwh_per_m2=-2.9642, - expected_co2_resid_tonnes_per_yr=-0.0865, + expected_pe_resid_kwh_per_m2=-2.8973, + expected_co2_resid_tonnes_per_yr=-0.0864, notes=( "Mitsubishi PUZ-WM50VHA PCDB 104568, ASHP cohort cert with " - "PV + 5 kWh battery. Worksheet SAP 84.1367. Slice S0380.49 " - "Table 12e PE factor wiring closed PE -3.58 → -2.96." + "PV + 5 kWh battery. Worksheet SAP 84.1367. Slice S0380.50 " + "HW seasonal-monthly fix closed PE -2.96 → -2.90." ), ), _GoldenExpectation( cert_number="2225-3062-8205-2856-7204", actual_sap=89, expected_sap_resid=+0, - expected_pe_resid_kwh_per_m2=-3.7259, - expected_co2_resid_tonnes_per_yr=-0.0729, + expected_pe_resid_kwh_per_m2=-3.5393, + expected_co2_resid_tonnes_per_yr=-0.0726, notes=( "Mitsubishi PUZ-WM50VHA PCDB 104568, ASHP cohort cert with " - "PV + 5 kWh battery. Worksheet SAP 88.7921. Slice S0380.49 " - "Table 12e PE factor wiring closed PE -4.50 → -3.73." + "PV + 5 kWh battery. Worksheet SAP 88.7921. Slice S0380.50 " + "HW seasonal-monthly fix closed PE -3.73 → -3.54." ), ), _GoldenExpectation( cert_number="2636-0525-2600-0401-2296", actual_sap=86, expected_sap_resid=+0, - expected_pe_resid_kwh_per_m2=-3.4364, - expected_co2_resid_tonnes_per_yr=-0.0603, + expected_pe_resid_kwh_per_m2=-3.2775, + expected_co2_resid_tonnes_per_yr=-0.0601, notes=( "Mitsubishi PUZ-WM50VHA PCDB 104568, ASHP cohort cert with " "PV + 5 kWh battery + 3.74 m² cantilever + 12.76 m² alt wall. " - "Worksheet SAP 86.2641. Slice S0380.49 Table 12e PE factor " - "wiring closed PE -4.14 → -3.44." + "Worksheet SAP 86.2641. Slice S0380.50 HW seasonal-monthly " + "fix closed PE -3.44 → -3.28." ), ), _GoldenExpectation( cert_number="3800-8515-0922-3398-3563", actual_sap=86, expected_sap_resid=+0, - expected_pe_resid_kwh_per_m2=-3.2511, - expected_co2_resid_tonnes_per_yr=-0.0166, + expected_pe_resid_kwh_per_m2=-3.1623, + expected_co2_resid_tonnes_per_yr=-0.0165, notes=( "Mitsubishi PUZ-WM50VHA PCDB 104568, ASHP cohort cert with " - "PV + 5 kWh battery. Worksheet SAP 86.1458. Slice S0380.49 " - "Table 12e PE factor wiring closed PE -4.01 → -3.25." + "PV + 5 kWh battery. Worksheet SAP 86.1458. Slice S0380.50 " + "HW seasonal-monthly fix closed PE -3.25 → -3.16." ), ), _GoldenExpectation( cert_number="9285-3062-0205-7766-7200", actual_sap=84, expected_sap_resid=+0, - expected_pe_resid_kwh_per_m2=-2.8078, - expected_co2_resid_tonnes_per_yr=-0.1003, + expected_pe_resid_kwh_per_m2=-2.7361, + expected_co2_resid_tonnes_per_yr=-0.1002, notes=( "Mitsubishi PUZ-WM50VHA PCDB 104568, ASHP cohort cert with " - "PV + 5 kWh battery. Worksheet SAP 84.1369. Slice S0380.49 " - "Table 12e PE factor wiring closed PE -3.46 → -2.81." + "PV + 5 kWh battery. Worksheet SAP 84.1369. Slice S0380.50 " + "HW seasonal-monthly fix closed PE -2.81 → -2.74." ), ), _GoldenExpectation( cert_number="9418-3062-8205-3566-7200", actual_sap=85, expected_sap_resid=+0, - expected_pe_resid_kwh_per_m2=-3.0084, - expected_co2_resid_tonnes_per_yr=-0.0485, + expected_pe_resid_kwh_per_m2=-2.8890, + expected_co2_resid_tonnes_per_yr=-0.0483, notes=( "Daikin Altherma EDLQ05CAV3 PCDB 102421 (heating_duration " "code '24' — continuous, all days at Th) + 5 kWh battery. " - "Worksheet SAP 84.6305. Slice S0380.49 Table 12e PE factor " - "wiring closed PE -3.76 → -3.01." + "Worksheet SAP 84.6305. Slice S0380.50 HW seasonal-monthly " + "fix closed PE -3.01 → -2.89." ), ), )