From a3bdcccfb13e38001cb236d57a5ba381cc22f3b7 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Wed, 1 Jul 2026 12:30:17 +0000 Subject: [PATCH] Normalize RdSAP lighting mapping on the degraded 17.0/19.0 paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PRD #1385, lighting family. The 17.0 and 19.0 API mappers hardcoded every fixed-lighting bulb count to 0, dropping the lodged lighting mix that the reference-complete 17.1/18.0/20.0 paths read. The schema lodges total + low-energy OUTLET counts (ADR-0028); mirror the reference split — low-energy outlets → low_energy bulbs, the remainder → incandescent — so the SAP lighting-energy calc sees the real low-energy credit instead of assuming none. led/cfl stay 0 (reduced-field certs don't split those); 21.0.x use a different bulb-based lighting schema and are out of scope. The `total - low_energy` subtraction is safe: all 37 local 17.0/19.0 fixtures have low_energy <= total (no negative incandescent), matching the reference paths that already do this unclamped. Verification: new test 2 passed; test_mapper_corpus 6002 passed; SAP-accuracy / RealCertExpectation regressions 56 passed (no pinned shifts); pyright unchanged (39 -> 39, pre-existing). Co-Authored-By: Claude Opus 4.8 (1M context) --- datatypes/epc/domain/mapper.py | 22 ++++++- .../epc/domain/test_mapper_lighting_parity.py | 57 +++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 tests/datatypes/epc/domain/test_mapper_lighting_parity.py diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index c4a33a514..fe947bf9c 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -603,9 +603,18 @@ class EpcPropertyDataMapper: insulated_door_count=schema.insulated_door_count, draughtproofed_door_count=None, percent_draughtproofed=schema.percent_draughtproofed, + # ADR-0028: the schema lodges total + low-energy OUTLET counts, not a + # bulb split — mirror the 17.1/18.0/20.0 paths (low-energy outlets → + # low_energy bulbs; the remainder → incandescent). Hardcoding all + # bulb counts to 0 dropped the lodged lighting mix, understating the + # low-energy lighting credit in the SAP lighting-energy calc. led_fixed_lighting_bulbs_count=0, cfl_fixed_lighting_bulbs_count=0, - incandescent_fixed_lighting_bulbs_count=0, + incandescent_fixed_lighting_bulbs_count=( + schema.fixed_lighting_outlets_count + - schema.low_energy_fixed_lighting_outlets_count + ), + low_energy_fixed_lighting_bulbs_count=schema.low_energy_fixed_lighting_outlets_count, roofs=EpcPropertyDataMapper._map_energy_elements(schema.roofs), walls=EpcPropertyDataMapper._map_energy_elements(schema.walls), floors=EpcPropertyDataMapper._map_energy_elements(schema.floors), @@ -1577,9 +1586,18 @@ class EpcPropertyDataMapper: insulated_door_count=schema.insulated_door_count, draughtproofed_door_count=None, percent_draughtproofed=schema.percent_draughtproofed, + # ADR-0028: the schema lodges total + low-energy OUTLET counts, not a + # bulb split — mirror the 17.1/18.0/20.0 paths (low-energy outlets → + # low_energy bulbs; the remainder → incandescent). Hardcoding all + # bulb counts to 0 dropped the lodged lighting mix, understating the + # low-energy lighting credit in the SAP lighting-energy calc. led_fixed_lighting_bulbs_count=0, cfl_fixed_lighting_bulbs_count=0, - incandescent_fixed_lighting_bulbs_count=0, + incandescent_fixed_lighting_bulbs_count=( + schema.fixed_lighting_outlets_count + - schema.low_energy_fixed_lighting_outlets_count + ), + low_energy_fixed_lighting_bulbs_count=schema.low_energy_fixed_lighting_outlets_count, roofs=EpcPropertyDataMapper._map_energy_elements(schema.roofs), walls=EpcPropertyDataMapper._map_energy_elements(schema.walls), floors=EpcPropertyDataMapper._map_energy_elements(schema.floors), diff --git a/tests/datatypes/epc/domain/test_mapper_lighting_parity.py b/tests/datatypes/epc/domain/test_mapper_lighting_parity.py new file mode 100644 index 000000000..c3e3316fd --- /dev/null +++ b/tests/datatypes/epc/domain/test_mapper_lighting_parity.py @@ -0,0 +1,57 @@ +"""Regression: the degraded RdSAP paths (17.0, 19.0) must carry the lodged +fixed-lighting outlet counts, like the reference-complete 17.1/18.0/20.0 paths. + +PRD #1385 (mapper normalization), lighting family. Both paths hardcoded every +bulb count to 0, dropping the lodged lighting mix. The schema lodges total + +low-energy OUTLET counts (ADR-0028); the reference paths split them as +low-energy → low_energy bulbs and the remainder → incandescent, feeding the SAP +lighting-energy calc. (21.0.x use a different bulb-based lighting schema and are +out of scope for this slice.) +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from datatypes.epc.domain.mapper import EpcPropertyDataMapper + +# 17.0 cert: fixed_lighting_outlets_count=10, low_energy=5 → incandescent 5. +_FIXTURE_17_0 = Path("tests/fixtures/epc_prediction/PE71NT/cert-26619d8e7f8e.json") +# 19.0 cert: fixed_lighting_outlets_count=8, low_energy=5 → incandescent 3. +_FIXTURE_19_0 = Path("tests/fixtures/epc_prediction/PE71NT/cert-21b634bc4b59.json") + + +def _load(path: Path, schema_type: str) -> dict[str, Any]: + data: dict[str, Any] = json.loads(path.read_text()) + assert data["schema_type"] == schema_type + return data + + +def test_17_0_path_carries_lighting_outlet_counts() -> None: + data = _load(_FIXTURE_17_0, "RdSAP-Schema-17.0") + assert data["fixed_lighting_outlets_count"] == 10 + assert data["low_energy_fixed_lighting_outlets_count"] == 5 + + epc = EpcPropertyDataMapper.from_api_response(data) + + # Was hardcoded 0/0/0. Low-energy outlets carry through; the remainder is + # incandescent; led/cfl stay 0 (reduced-field certs don't split those). + assert epc.low_energy_fixed_lighting_bulbs_count == 5 + assert epc.incandescent_fixed_lighting_bulbs_count == 5 + assert epc.led_fixed_lighting_bulbs_count == 0 + assert epc.cfl_fixed_lighting_bulbs_count == 0 + + +def test_19_0_path_carries_lighting_outlet_counts() -> None: + data = _load(_FIXTURE_19_0, "RdSAP-Schema-19.0") + assert data["fixed_lighting_outlets_count"] == 8 + assert data["low_energy_fixed_lighting_outlets_count"] == 5 + + epc = EpcPropertyDataMapper.from_api_response(data) + + assert epc.low_energy_fixed_lighting_bulbs_count == 5 + assert epc.incandescent_fixed_lighting_bulbs_count == 3 + assert epc.led_fixed_lighting_bulbs_count == 0 + assert epc.cfl_fixed_lighting_bulbs_count == 0