Normalize RdSAP lighting mapping on the degraded 17.0/19.0 paths

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) <noreply@anthropic.com>
This commit is contained in:
Jun-te Kim 2026-07-01 12:30:17 +00:00
parent cf4c56df2e
commit a3bdcccfb1
2 changed files with 77 additions and 2 deletions

View file

@ -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),

View file

@ -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