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}")