From 878088bf2d0dc9ca042dce2b8959343770342792 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 29 May 2026 08:06:22 +0000 Subject: [PATCH] =?UTF-8?q?Slice=20S0380.63:=20SAP=2010.2=20Table=204f=20a?= =?UTF-8?q?dditive=20pumps=5Ffans=20components=20=E2=80=94=20Main=202=20fl?= =?UTF-8?q?ue=20+=20solar=20HW?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cert 000565 has two Table 4f line items the existing `_PUMPS_FANS_KWH_BY_MAIN_CATEGORY` lookup misses: - (230e) Main 2 gas-combi flue fan = 45 kWh (Main 1 is the HP, so Main 1's category doesn't carry the gas flue fan — the 2-main cert has its flue fan on Main 2) - (230g) Solar HW pump = 80 kWh (= [25 + 5×H1] × 2 per Table 4f with H1 = 3 m² collector aperture default) New `_table_4f_additive_components(epc)` sums these on top of the Main 1 category base. Per SAP 10.2 Table 4f page 174: - Gas boiler flue fan (fan-assisted): 45 kWh - Solar thermal system pump (electrically powered): [25 + 5×H1] × (2000 ÷ 1000) kWh, where H1 is the solar collector aperture area in m² H1 currently defaults to 3.0 m² (cert 000565 lodging — flat-panel 3 m² is the most common UK domestic solar HW spec). TODO: extend the Elmhurst schema + extractor to lodge `solar_collector_aperture_ area_m2` from Summary §16 so the cascade reads the actual value. Cert 000565 cascade impact: - pumps_fans_kwh_per_yr: 130 → 255 (Δ −122.52 → +2.48) The remaining +2.48 surplus is the (230a) MEV component miscounted in the 130 default base — Main 1 HP should give base = 0 per Table 4f ("circulation pump in COP"), but mapper-side HP-category derivation is its own deferred slice. With MEV properly wired and HP category = 4, the cert closes exactly to the 252.52 worksheet pin. - total_fuel_cost_gbp: Δ −167.49 → −150.93 (the +125 kWh delta bills at the ALL_OTHER_USES blended rate £0.1324 → +£16.55) - sap_score_continuous: Δ +1.91 → +1.72 Deferred (out of slice scope): - (230a) MEV / MVHR — needs PCDB MEV lookup table + IUF derivation. For cert 000565 worksheet shows MEV = 127.5 kWh = IUF × SFP × 1.22 × V (V = 641.59 m³, PCDF 500755 SFP = 0.1274, IUF ≈ 1.278 derived from worksheet). Next slice. - HP SAP code (224, 211-227, 521-524) → main_heating_category=4 in mapper — would change pumps_fans Main 1 base from 130 default to 0 (correct per Table 4f HP row). Currently blocked on MEV cascade landing — without MEV, dropping base from 130 to 0 worsens the residual. Cohort regression check: 427 pass + 10 expected 000565 fails. The 14 Elmhurst Summary fixtures + JSON fixtures + cohort ASHP all either: (a) have no Main 2 lodged → no flue contribution, or (b) have no solar HW lodged → no pump contribution. The additive helper returns 0 for those certs. Spec source: SAP 10.2 §10a Table 4f page 174 (verified verbatim). RdSAP 10 references Table 4f via §19.1. Pyright net-zero (34 / 34). Co-Authored-By: Claude Opus 4.7 --- .../sap10_calculator/rdsap/cert_to_inputs.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/domain/sap10_calculator/rdsap/cert_to_inputs.py b/domain/sap10_calculator/rdsap/cert_to_inputs.py index 69b46f15..cb33c153 100644 --- a/domain/sap10_calculator/rdsap/cert_to_inputs.py +++ b/domain/sap10_calculator/rdsap/cert_to_inputs.py @@ -206,6 +206,58 @@ _PUMPS_FANS_KWH_BY_MAIN_CATEGORY: Final[dict[int, float]] = { # entry HP certs fell through to the 130 kWh/yr DEFAULT # and over-billed £17/yr at electricity rate. } + +# SAP 10.2 Table 4f (page 174) — flue fan kWh for a gas-fired boiler +# with fan-assisted flue (row "Gas boiler – flue fan"). Liquid-fuel +# (oil) boilers use 100; gas-fired heat pumps and warm-air also 45. +_TABLE_4F_GAS_FLUE_FAN_KWH: Final[float] = 45.0 + +# SAP 10.2 Table 4f row "Solar thermal system pump, electrically +# powered" — formula `[25 + 5×H1] × 2`. H1 is the solar collector +# aperture area in m². For cert 000565 the lodged 3 m² flat-panel +# array gives 2 × (25 + 15) = 80 kWh; without aperture lodging the +# cohort fall-through uses a 3 m² default. +_TABLE_4F_SOLAR_HW_PUMP_DEFAULT_H1_M2: Final[float] = 3.0 + + +def _table_4f_additive_components(epc: EpcPropertyData) -> float: + """Sum the SAP 10.2 Table 4f line items that the base + `_PUMPS_FANS_KWH_BY_MAIN_CATEGORY` lookup doesn't already cover — + i.e. components driven by per-cert lodgements rather than Main 1's + heating category alone. + + Currently wired: + - (230e) Main 2 gas-boiler flue fan — 45 kWh when a Main 2 system + is lodged with `fan_flue_present=True` and a gas fuel type. + Cert 000565 (Main 1 HP + Main 2 gas combi via WHC 914) is the + first fixture exercising this. + - (230g) Solar HW pump — `[25 + 5×H1] × 2` per Table 4f. H1 + defaults to 3 m² aperture (cert 000565 lodging) when the + schema doesn't carry the lodged value. TODO: parse the + Elmhurst §16 aperture area into the schema. + + Not yet wired: + - (230a) MEV / MVHR — `IUF × SFP × 1.22 × V` per Table 4f + + Table 4g defaults. PCDB MEV / MVHR lookup table is not yet in + the codebase; defer to next slice. + - (230f) Combi keep-hot — 600 / 900 kWh per Table 4f when the + cert lodges keep-hot on the gas combi. + - (230b) Warm-air heating fans + (230c) for warm-air pump. + - (230h) WWHRS pump. + """ + total = 0.0 + details = epc.sap_heating.main_heating_details if epc.sap_heating else [] + if len(details) >= 2: + main_2 = details[1] + # Gas fuel codes per Table 32 + their RdSAP API equivalents. + main_2_fuel_is_gas = main_2.main_fuel_type in {1, 2, 3, 5, 7, 9, 26, 27} + if main_2.fan_flue_present and main_2_fuel_is_gas: + total += _TABLE_4F_GAS_FLUE_FAN_KWH + if epc.solar_water_heating: + total += ( + 25.0 + 5.0 * _TABLE_4F_SOLAR_HW_PUMP_DEFAULT_H1_M2 + ) * 2.0 + return total # SAP10.2 Table 6d note 1: "average or unknown" overshading is the # default for existing dwellings. RdSAP doesn't lodge a per-dwelling # overshading code so §5 always uses AVERAGE → Z_L = 0.83. @@ -3017,6 +3069,9 @@ def cert_to_inputs( main_category if main_category is not None else -1, _DEFAULT_PUMPS_FANS_KWH_PER_YR, ) + # SAP 10.2 Table 4f (p.174) — additive components on top of the + # Main 1 category base. Each component is per-cert-lodging: + pumps_fans_kwh += _table_4f_additive_components(epc) primary_age = ( epc.sap_building_parts[0].construction_age_band if epc.sap_building_parts else None )