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 678d6b8c..7b19e8b4 100644 --- a/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py +++ b/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py @@ -84,6 +84,8 @@ from domain.sap.worksheet.solar_gains import ( from domain.sap.worksheet.heat_transmission import ( DwellingExposure, HeatTransmission, + _AREA_ROUND_DP, + _round_half_up, heat_transmission_from_cert, ) from domain.sap.climate.appendix_u import external_temperature_c @@ -348,9 +350,9 @@ def _is_timber_or_steel_frame(parts: list[SapBuildingPart]) -> bool: return isinstance(wc, int) and wc in (5, 6) -def _living_area_fraction(habitable_rooms_count: Optional[int]) -> float: - """RdSAP 10 Table 27 by `habitable_rooms_count`. Defaults to the - bottom of the table for ≥8 rooms; falls back to the SAP convention +def _living_area_fraction_default(habitable_rooms_count: Optional[int]) -> float: + """RdSAP 10 Table 27 (p.52) lookup by `habitable_rooms_count`. Defaults + to the bottom of the table for ≥8 rooms; falls back to the SAP convention 0.21 when count missing or zero.""" if not habitable_rooms_count or habitable_rooms_count <= 0: return _LIVING_AREA_FRACTION_DEFAULT @@ -359,6 +361,25 @@ def _living_area_fraction(habitable_rooms_count: Optional[int]) -> float: return _LIVING_AREA_FRACTION_MIN +def _living_area_fraction( + habitable_rooms_count: Optional[int], total_floor_area_m2: float +) -> float: + """SAP 10.2 §7 LINE_91 = Living area / TFA. + + RdSAP §9.2 (p.52): living area = Table 27 fraction × TFA. RdSAP §15 + (p.66) requires "All internal floor areas and living area: 2 d.p." at + the RdSAP→SAP boundary. So the materialised living area is rounded to + 2 d.p. half-up, then divided back by TFA to yield the LINE_91 that + feeds the §7 zone blend. This roundtrip is why fixtures lodge + e.g. 0.3001 (= 17.04/56.79) rather than the raw 0.30 Table 27 entry. + """ + fraction = _living_area_fraction_default(habitable_rooms_count) + if total_floor_area_m2 <= 0.0: + return fraction + living_area_m2 = _round_half_up(fraction * total_floor_area_m2, _AREA_ROUND_DP) + return living_area_m2 / total_floor_area_m2 + + def _window_total_area_and_avg_u(windows: list[SapWindow]) -> tuple[float, Optional[float]]: """Area-weighted total + U-value for the conduction worksheet.""" if not windows: @@ -877,7 +898,9 @@ def mean_internal_temperature_section_from_cert( total_floor_area_m2=dim.total_floor_area_m2, control_type=_control_type(main), responsiveness=_responsiveness(main), - living_area_fraction=_living_area_fraction(epc.habitable_rooms_count), + living_area_fraction=_living_area_fraction( + epc.habitable_rooms_count, dim.total_floor_area_m2 + ), control_temperature_adjustment_c=0.0, ) @@ -1407,7 +1430,9 @@ def cert_to_inputs( # for the Elmhurst corpus (cert-side mapping is a future slice). control_type_value = _control_type(main) responsiveness_value = _responsiveness(main) - living_area_fraction_value = _living_area_fraction(epc.habitable_rooms_count) + living_area_fraction_value = _living_area_fraction( + epc.habitable_rooms_count, dim.total_floor_area_m2 + ) monthly_total_gains_w = tuple( internal_gains_monthly_w[m] + solar_gains_monthly_w[m] for m in range(12) )