From 024244ec59b2f518f079d434d40c6ab698bee633 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 22 May 2026 23:13:48 +0000 Subject: [PATCH] =?UTF-8?q?Slice=2021d:=20=C2=A73=20cascade=20pins=20+=20h?= =?UTF-8?q?eat=5Ftransmission=5Fsection=5Ffrom=5Fcert=20helper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extracts `heat_transmission_section_from_cert(epc)` wrapping the §3 inline call in cert_to_inputs (window-area/window-U/dwelling-exposure plumbing). Replaces the inline call. Adds §3 cascade pins for the four aggregate line refs: (31) total_external_element_area_m2 (33) fabric_heat_loss_w_per_k (36) thermal_bridging_w_per_k (37) total_w_per_k Results at abs=1e-4 (1/24 PASS): fixture | LINE_31 diff | LINE_33 diff | LINE_36 diff | LINE_37 diff --------|--------------|--------------|--------------|------------- 000474 | 0.0014 | 0.086 | 0.0002 | 0.086 000477 | 0.0004 | 0.105 | ✓ | 0.104 000480 | 0.006 | 0.017 | 0.0009 | 0.018 000487 | 8.82 | 37.88 | 1.32 | 39.21 000490 | 0.000 | 0.064 | 0.000 | 0.064 000516 | 0.012 | 0.183 | 0.002 | 0.184 Three buckets: - 000487 (RR fixture defect): large gaps — fixture lodges Simplified Type 1 RR but PDF has detailed §3.10 lodgement including a U=0.86 external gable. Slice 22 closes (mirrors S16a). - 000474/000477/000480/000490/000516 (precision residuals): LINE_33 drifts 0.02-0.18 W/K — sub-display-precision (PDF lodges to 4 d.p. per element, our calc combines full-precision per-storey perimeters + 4-d.p. U values). The aggregate diff of ~0.1 W/K is just over the abs=1e-4 floor but well under the worksheet's display granularity. Cascade pins now: §1 (12 PASS) + §2 (96 PASS) + §3 (1 PASS, 23 FAIL). Co-Authored-By: Claude Opus 4.7 --- .../src/domain/sap/rdsap/cert_to_inputs.py | 40 ++++++++++------ .../tests/test_section_cascade_pins.py | 48 ++++++++++++++++++- 2 files changed, 73 insertions(+), 15 deletions(-) diff --git a/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py b/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py index b9745fd0..c79cccff 100644 --- a/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py +++ b/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py @@ -76,6 +76,7 @@ from domain.sap.worksheet.internal_gains import ( ) from domain.sap.worksheet.heat_transmission import ( DwellingExposure, + HeatTransmission, heat_transmission_from_cert, ) from domain.sap.climate.appendix_u import external_temperature_c @@ -690,6 +691,28 @@ def _ventilation_counts(vent: Optional[SapVentilation]) -> _VentilationCounts: ) +def heat_transmission_section_from_cert(epc: EpcPropertyData) -> HeatTransmission: + """SAP 10.2 §3 cert→inputs cascade for `heat_transmission_from_cert`. + + Wraps the `_window_total_area_and_avg_u` + `_dwelling_exposure` + derivations cert_to_inputs makes internally and returns the full + `HeatTransmission` (every (26)..(37) line ref breakdown). Exposed + so cascade pin tests can assert each §3 line ref against the U985 + PDF. + """ + window_total_area, window_avg_u = _window_total_area_and_avg_u(epc.sap_windows) + exposure = _dwelling_exposure(epc.dwelling_type) + return heat_transmission_from_cert( + epc, + window_total_area_m2=window_total_area, + window_avg_u_value=window_avg_u, + door_count=epc.door_count, + insulated_door_count=epc.insulated_door_count, + insulated_door_u_value=epc.insulated_door_u_value, + exposure=exposure, + ) + + def ventilation_from_cert(epc: EpcPropertyData) -> VentilationResult: """SAP 10.2 §2 cert→inputs cascade for `ventilation_from_inputs`. @@ -1050,20 +1073,9 @@ def cert_to_inputs( (inspection_date ≥ 2025-07-01) rather than a per-cert price override.""" dim = dimensions_from_cert(epc) - window_total_area, window_avg_u = _window_total_area_and_avg_u(epc.sap_windows) - exposure = _dwelling_exposure(epc.dwelling_type) - ht = heat_transmission_from_cert( - epc, - window_total_area_m2=window_total_area, - window_avg_u_value=window_avg_u, - door_count=epc.door_count, - insulated_door_count=epc.insulated_door_count, - insulated_door_u_value=epc.insulated_door_u_value, - exposure=exposure, - ) - - # SAP §2 ventilation cascade — see `ventilation_from_cert` for the - # cert→inputs mapping rules + spec-default conventions. + # SAP §3 heat transmission + §2 ventilation cascades — see the + # respective `_from_cert` helpers for cert→inputs mapping rules. + ht = heat_transmission_section_from_cert(epc) ventilation = ventilation_from_cert(epc) main = _first_main_heating(epc) diff --git a/packages/domain/src/domain/sap/worksheet/tests/test_section_cascade_pins.py b/packages/domain/src/domain/sap/worksheet/tests/test_section_cascade_pins.py index 172ff27a..800c48de 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/test_section_cascade_pins.py +++ b/packages/domain/src/domain/sap/worksheet/tests/test_section_cascade_pins.py @@ -16,7 +16,11 @@ from typing import Final import pytest -from domain.sap.rdsap.cert_to_inputs import cert_to_inputs, ventilation_from_cert +from domain.sap.rdsap.cert_to_inputs import ( + cert_to_inputs, + heat_transmission_section_from_cert, + ventilation_from_cert, +) from domain.sap.worksheet.dimensions import dimensions_from_cert from domain.sap.worksheet.tests import ( _elmhurst_worksheet_000474 as _w000474, @@ -187,3 +191,45 @@ def test_section_2_line_19_sheltered_sides_matches_pdf(fixture_name: str) -> Non assert vent.sheltered_sides == expected, ( f"§2 (19) {fixture_name}: actual={vent.sheltered_sides}, expected={expected}" ) + + +# ============================================================================ +# §3 Heat losses + heat loss parameter — LINE_31/33/36/37 aggregates +# ============================================================================ + +_SECTION_3_PINS: Final[tuple[tuple[str, str], ...]] = ( + # (fixture_attr, HeatTransmission attr) + ("LINE_31_TOTAL_EXTERNAL_AREA_M2", "total_external_element_area_m2"), + ("LINE_33_FABRIC_HEAT_LOSS_W_PER_K", "fabric_heat_loss_w_per_k"), + ("LINE_36_THERMAL_BRIDGING_W_PER_K", "thermal_bridging_w_per_k"), + ("LINE_37_TOTAL_FABRIC_HEAT_LOSS_W_PER_K", "total_w_per_k"), +) + + +@pytest.mark.parametrize( + "fixture_name,fixture_attr,result_attr", + [ + (fix, line, attr) + for fix in _FIXTURES + for line, attr in _SECTION_3_PINS + ], + ids=lambda x: x if isinstance(x, str) else None, +) +def test_section_3_line_refs_match_pdf( + fixture_name: str, fixture_attr: str, result_attr: str +) -> None: + """§3 cascade pins — every (31)/(33)/(36)/(37) aggregate matches the + U985 PDF to abs=1e-4. Per-element breakdowns (26)/(27)/(28a)/(29a)/ + (30)/(32) are not currently lodged in fixture constants — they're + asserted indirectly via the aggregates.""" + # Arrange + mod = _FIXTURES[fixture_name] + epc = mod.build_epc() # type: ignore[attr-defined] + expected = getattr(mod, fixture_attr) + + # Act + ht = heat_transmission_section_from_cert(epc) + actual = getattr(ht, result_attr) + + # Assert + _pin(actual, expected, f"§3 {fixture_attr} {fixture_name}")