From d529f91a8ef95eb88aa47fe112f0805795041eaf Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 26 May 2026 22:05:51 +0000 Subject: [PATCH] =?UTF-8?q?Slice=20100b:=20API=20TFA=20=E2=80=94=20include?= =?UTF-8?q?=20per-bp=20RR=20floor=20area=20in=20continuous=20TFA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `_total_floor_area_from_building_parts` previously summed only `sap_floor_dimensions[*].total_floor_area`; the RR floor area lives under `sap_room_in_roof.floor_area` per RdSAP §3.9 convention and was dropped from the per-bp TFA sum. Cert 9501 (113.08 m² real TFA, of which 31.8 m² is RR) showed TFA 81.28 on the API path — the cascade then under-computed occupancy N (Appendix J), HW kWh (Appendix J), lighting kWh (Appendix L), and internal gains. Add the RR contribution to the sum. The top-level `schema.total_floor_area` scalar (integer-rounded for cert 9501: 113 vs raw 113.08) is still the fallback when no per-bp dims are lodged. Re-pinned residuals (improvements — TFA now includes the previously- dropped RR storey): - 0240: SAP -15 → -14, PE +15.69 → +12.49, CO2 +0.90 → +0.70 - 6035: PE +49.51 → +48.30, CO2 +1.14 → +1.10 Effect on cert 9501 API path: TFA 81.28 → 113.08 (= worksheet 113.08 exact). SAP delta still -9.32 vs worksheet — the remaining gap is dominated by the missing PV credit (£250 — next slice). Co-Authored-By: Claude Opus 4.7 --- datatypes/epc/domain/mapper.py | 27 ++++++++++++++----- .../rdsap/tests/test_golden_fixtures.py | 10 +++---- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index c6ba82a1..542dac15 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -1875,15 +1875,24 @@ def _measurement_value(field: Any) -> float: def _total_floor_area_from_building_parts(building_parts: Any) -> Optional[float]: - """Sum per-bp `sap_floor_dimensions[*].total_floor_area` to recover the - precise TFA. The GOV.UK EPB API JSON's top-level `total_floor_area` - is rounded to the integer (cert 001479: 30.45+30.77+5.37+1.92 = 68.51 - → lodged 69), but the worksheet computes continuous SAP from the - unrounded geometry. `epc.total_floor_area_m2` is read directly by + """Sum per-bp `sap_floor_dimensions[*].total_floor_area` (plus each bp's + `sap_room_in_roof.floor_area` when present) to recover the precise + TFA. + + The GOV.UK EPB API JSON's top-level `total_floor_area` is rounded to + the integer (cert 001479: 30.45+30.77+5.37+1.92 = 68.51 → lodged 69), + but the worksheet computes continuous SAP from the unrounded + geometry. `epc.total_floor_area_m2` is read directly by `water_heating_from_cert` to derive occupancy N (Appendix J), which drives HW, lighting (Appendix L), and internal-gains kWh — so the - rounded scalar shifts SAP by ~+0.0006 on cert 001479. Returns None - when no per-bp dims are lodged so callers fall back to the scalar.""" + rounded scalar shifts SAP by ~+0.0006 on cert 001479. RR floor area + is NOT in `sap_floor_dimensions` on the API path (it lives under + `sap_room_in_roof.floor_area` per RdSAP §3.9 convention), so cert + 9501's RR storey (31.8 m²) was previously dropped from the per-bp + TFA sum → TFA 81.28 vs worksheet 113.08. + + Returns None when no per-bp dims are lodged so callers fall back to + the scalar.""" if not building_parts: return None total = 0.0 @@ -1893,6 +1902,10 @@ def _total_floor_area_from_building_parts(building_parts: Any) -> Optional[float for fd in floor_dims: total += _measurement_value(fd.total_floor_area) found = True + rir: Any = getattr(bp, "sap_room_in_roof", None) + if rir is not None and getattr(rir, "floor_area", None) is not None: + total += _measurement_value(rir.floor_area) + found = True return total if found 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 e2637995..edac9197 100644 --- a/domain/sap10_calculator/rdsap/tests/test_golden_fixtures.py +++ b/domain/sap10_calculator/rdsap/tests/test_golden_fixtures.py @@ -74,9 +74,9 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( _GoldenExpectation( cert_number="0240-0200-5706-2365-8010", actual_sap=73, - expected_sap_resid=-15, - expected_pe_resid_kwh_per_m2=+15.6873, - expected_co2_resid_tonnes_per_yr=+0.8999, + expected_sap_resid=-14, + expected_pe_resid_kwh_per_m2=+12.4941, + expected_co2_resid_tonnes_per_yr=+0.6957, 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_" @@ -135,8 +135,8 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( cert_number="6035-7729-2309-0879-2296", actual_sap=70, expected_sap_resid=-6, - expected_pe_resid_kwh_per_m2=+49.5139, - expected_co2_resid_tonnes_per_yr=+1.1423, + expected_pe_resid_kwh_per_m2=+48.3043, + expected_co2_resid_tonnes_per_yr=+1.1019, notes=( "Mid-terrace, TFA 128, age A, gas combi Table 4b code 104. " "Slice 59 per-bp window apportionment tightens all 3 "