diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 7660a45b..d8754292 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -3061,19 +3061,26 @@ _RDSAP20_GLAZED_AREA_BAND_MULTIPLIER: dict[int, float] = { _RDSAP20_SYNTH_ORIENTATIONS: tuple[int, ...] = (1, 3, 5, 7) -def _synthesise_20_0_0_sap_windows(schema: RdSapSchema20_0_0) -> List[SapWindow]: - """ADR-0027 Reduced-Field Synthesis of `sap_windows` for a 20.0.0 cert. +def _synthesise_reduced_field_windows( + glazed_area: int, + total_floor_area: float, + glazing_type: int, +) -> List[SapWindow]: + """ADR-0028 shared core for pre-SAP10 reduced-field glazing synthesis. - 993/1000 certs carry no per-window array, only a glazed_area band + floor - area; synthesise total glazing as `ratio x TFA`, split 4-way across N/E/S/W - with `window_width = area/4, window_height = 1.0` (width x height is the only - quantity the calculator reads, so height=1 makes width carry the area - exactly — matching the existing Elmhurst precedent). + Certs with no per-window array carry only a glazed_area band + floor area; + synthesise total glazing as `ratio x TFA x band_multiplier`, split 4-way + across N/E/S/W with `window_width = area/4, window_height = 1.0` (width x + height is the only quantity the calculator reads, so height=1 makes width + carry the area exactly — the Elmhurst precedent). + + This is the single home of the inherited 20.0.0 coefficients across the + pre-SAP10 RdSAP family (20.0.0 / 18.0 / 17.1). `glazing_type` is pre-resolved + by each spec's seam (cascade + that spec's own "ND" handling), keeping spec + vocabulary out of the numeric core. """ - band_multiplier = _RDSAP20_GLAZED_AREA_BAND_MULTIPLIER.get(schema.glazed_area, 1.0) - total_area = ( - _RDSAP20_GLAZING_RATIO * float(schema.total_floor_area) * band_multiplier - ) + band_multiplier = _RDSAP20_GLAZED_AREA_BAND_MULTIPLIER.get(glazed_area, 1.0) + total_area = _RDSAP20_GLAZING_RATIO * float(total_floor_area) * band_multiplier per_window_area = total_area / len(_RDSAP20_SYNTH_ORIENTATIONS) return [ SapWindow( @@ -3081,11 +3088,7 @@ def _synthesise_20_0_0_sap_windows(schema: RdSapSchema20_0_0) -> List[SapWindow] glazing_gap=0, orientation=orientation, window_type=0, - # ADR-0027: 20.0.0 glazed_type codes 1-8+ND are identical to 21.0.1's, - # so reuse the verified 21.0.1 cascade (fixes code 1 "DG pre-2002" - # being mis-read as single). g⊥ comes from window_transmission_details - # (slice 6), so glazing_type only feeds the daylight g_L lookup. - glazing_type=_api_cascade_glazing_type(schema.multiple_glazing_type), + glazing_type=glazing_type, window_width=per_window_area, window_height=1.0, draught_proofed=False, @@ -3097,6 +3100,17 @@ def _synthesise_20_0_0_sap_windows(schema: RdSapSchema20_0_0) -> List[SapWindow] ] +def _synthesise_20_0_0_sap_windows(schema: RdSapSchema20_0_0) -> List[SapWindow]: + """ADR-0027/0028 seam: 20.0.0 glazed_type codes 1-8+ND are identical to + 21.0.1's, so route multiple_glazing_type through the verified cascade (fixes + code 1 "DG pre-2002" read as single), then call the shared core.""" + return _synthesise_reduced_field_windows( + schema.glazed_area, + schema.total_floor_area, + _api_cascade_glazing_type(schema.multiple_glazing_type), + ) + + # ADR-0028: multiple_glazing_type "ND" (Not Defined, 69/1000 18.0 certs) has no # cascade mapping — treat as the DG-modal default (cascade code 2 → daylight g_L # 0.80, matching the calculator's `_G_LIGHT_DEFAULT` for unknown glazing). @@ -3104,46 +3118,21 @@ _RDSAP18_ND_GLAZING_TYPE: int = 2 def _synthesise_18_0_sap_windows(schema: RdSapSchema18_0) -> List[SapWindow]: - """ADR-0028 Reduced-Field Synthesis of `sap_windows` for an 18.0 cert. - - A separate seam from the 20.0.0 helper so 18.0 can diverge as we learn more, - but it reuses the inherited 20.0.0 coefficients unchanged (ADR-0028: the - 18.0 corpus can't self-fit — 958/1000 band-1 with no measured band-1 windows - — and its band-4 rich certs reproduce `0.148 x 1.51 = 0.223`). 990/1000 certs - carry no per-window array, only a glazed_area band + floor area; synthesise - total glazing as `ratio x TFA`, split 4-way across N/E/S/W with - `window_width = area/4, window_height = 1.0`. - """ - band_multiplier = _RDSAP20_GLAZED_AREA_BAND_MULTIPLIER.get(schema.glazed_area, 1.0) - total_area = ( - _RDSAP20_GLAZING_RATIO * float(schema.total_floor_area) * band_multiplier - ) - per_window_area = total_area / len(_RDSAP20_SYNTH_ORIENTATIONS) - # ADR-0028: 18.0 glazed_type codes 1-8 are identical to 20.0.0's (verified - # against epc_codes.csv), so reuse the verified cascade for integer codes; - # the "ND" string falls back to the DG-modal default. + """ADR-0028 seam: reuses the inherited 20.0.0 coefficients via the shared + core (the 18.0 corpus can't self-fit — 958/1000 band-1 with no measured + band-1 windows — and its band-4 rich certs reproduce `0.148 x 1.51 = 0.223`). + 18.0 glazed_type codes 1-8 are identical to 20.0.0's (verified vs + epc_codes.csv): route integer codes through the verified cascade; the "ND" + string falls back to the DG-modal default. Own seam so 18.0 can diverge.""" mgt = schema.multiple_glazing_type glazing_type = ( _api_cascade_glazing_type(mgt) if isinstance(mgt, int) else _RDSAP18_ND_GLAZING_TYPE ) - return [ - SapWindow( - frame_material=None, - glazing_gap=0, - orientation=orientation, - window_type=0, - glazing_type=glazing_type, - window_width=per_window_area, - window_height=1.0, - draught_proofed=False, - window_location=0, - window_wall_type=0, - permanent_shutters_present=False, - ) - for orientation in _RDSAP20_SYNTH_ORIENTATIONS - ] + return _synthesise_reduced_field_windows( + schema.glazed_area, schema.total_floor_area, glazing_type + ) # ADR-0028: multiple_glazing_type "ND" (Not Defined, 56/1000 17.1 certs) → the @@ -3152,43 +3141,20 @@ _RDSAP17_1_ND_GLAZING_TYPE: int = 2 def _synthesise_17_1_sap_windows(schema: RdSapSchema17_1) -> List[SapWindow]: - """ADR-0028 Reduced-Field Synthesis of `sap_windows` for a 17.1 cert. - - A separate seam from the 18.0/20.0.0 helpers so 17.1 can diverge, but it - reuses the inherited 20.0.0 coefficients unchanged (ADR-0028: 969/1000 band-1 - with no measured band-1 windows; its band-4 rich certs reproduce the model). - 990/1000 certs carry no per-window array — synthesise total glazing as - `ratio x TFA`, split 4-way across N/E/S/W with height=1.0. - """ - band_multiplier = _RDSAP20_GLAZED_AREA_BAND_MULTIPLIER.get(schema.glazed_area, 1.0) - total_area = ( - _RDSAP20_GLAZING_RATIO * float(schema.total_floor_area) * band_multiplier - ) - per_window_area = total_area / len(_RDSAP20_SYNTH_ORIENTATIONS) - # ADR-0028: 17.1 glazed_type codes 1-8 are identical to 20.0.0's (verified - # against epc_codes.csv); the "ND" string falls back to the DG-modal default. + """ADR-0028 seam: reuses the inherited 20.0.0 coefficients via the shared + core (969/1000 band-1 with no measured band-1 windows; band-4 rich certs + reproduce the model). 17.1 glazed_type codes 1-8 are identical to 20.0.0's: + route integer codes through the verified cascade; the "ND" string falls back + to the DG-modal default. Own seam so 17.1 can diverge.""" mgt = schema.multiple_glazing_type glazing_type = ( _api_cascade_glazing_type(mgt) if isinstance(mgt, int) else _RDSAP17_1_ND_GLAZING_TYPE ) - return [ - SapWindow( - frame_material=None, - glazing_gap=0, - orientation=orientation, - window_type=0, - glazing_type=glazing_type, - window_width=per_window_area, - window_height=1.0, - draught_proofed=False, - window_location=0, - window_wall_type=0, - permanent_shutters_present=False, - ) - for orientation in _RDSAP20_SYNTH_ORIENTATIONS - ] + return _synthesise_reduced_field_windows( + schema.glazed_area, schema.total_floor_area, glazing_type + ) # GOV.UK API `glazing_type` integer → (u_value W/m²K, g_perpendicular,