Map full-SAP energy source, mains-gas inference and lighting bulbs 🟩

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jun-te Kim 2026-06-15 14:23:31 +00:00
parent cb4d080da2
commit acd0ed485d
3 changed files with 66 additions and 5 deletions

View file

@ -688,9 +688,17 @@ class EpcPropertyDataMapper:
door_count=door_count,
insulated_door_count=door_count,
insulated_door_u_value=insulated_door_u,
# D5: lighting outlet counts → bulb counts (full SAP lodges total +
# low-energy outlet counts, like the RdSAP 17.1 path).
cfl_fixed_lighting_bulbs_count=0,
led_fixed_lighting_bulbs_count=0,
incandescent_fixed_lighting_bulbs_count=0,
incandescent_fixed_lighting_bulbs_count=(
(schema.sap_energy_source.fixed_lighting_outlets_count or 0)
- (schema.sap_energy_source.low_energy_fixed_lighting_outlets_count or 0)
),
low_energy_fixed_lighting_bulbs_count=(
schema.sap_energy_source.low_energy_fixed_lighting_outlets_count or 0
),
# D4: full SAP lodges the measured U as text in the element
# description ("Average thermal transmittance X W/m²K"); carry it
# through so u_wall/u_floor/u_roof parse it instead of re-deriving
@ -714,14 +722,18 @@ class EpcPropertyDataMapper:
# the domain SapHeating the calculator consumes (PCDB efficiency via
# main_heating_index_number; water cascade via water_heating_code).
sap_heating=_sap_17_1_heating(schema),
# D5: mains_gas derived from the heating fuel (full SAP has no
# explicit flag); tariff → meter_type; wind turbines pass through.
sap_energy_source=SapEnergySource(
mains_gas=False,
meter_type="",
mains_gas=_sap_dwelling_on_mains_gas(schema),
meter_type=str(schema.sap_energy_source.electricity_tariff or ""),
pv_battery_count=0,
wind_turbines_count=0,
wind_turbines_count=schema.sap_energy_source.wind_turbines_count or 0,
gas_smart_meter_present=False,
is_dwelling_export_capable=False,
wind_turbines_terrain_type="",
wind_turbines_terrain_type=str(
schema.sap_energy_source.wind_turbine_terrain_type or ""
),
electricity_smart_meter_present=False,
),
)
@ -2504,6 +2516,19 @@ _SAP_LIVING_AREA_FRACTION_BY_ROOMS: Final[Dict[int, float]] = {
}
# SAP main_fuel_type code for mains gas.
_SAP_MAINS_GAS_FUEL_CODE: Final[int] = 1
def _sap_dwelling_on_mains_gas(schema: SapSchema17_1) -> bool:
"""D5: full SAP has no explicit mains-gas flag — infer it from whether any
main-heating system burns mains gas (fuel code 1)."""
return any(
d.main_fuel_type == _SAP_MAINS_GAS_FUEL_CODE
for d in schema.sap_heating.main_heating_details
)
def _sap_17_1_heating(schema: SapSchema17_1) -> SapHeating:
"""D6: map full-SAP `sap_heating` onto the domain `SapHeating`. Field names
differ from RdSAP `is_flue_fan_present``fan_flue_present`,

View file

@ -232,6 +232,28 @@ class TestFromSapSchema17_1LivingArea:
assert self._map("sap_17_1_flat.json").habitable_rooms_count == 3
class TestFromSapSchema17_1EnergySource:
"""Slice D5: full-SAP sap_energy_source → mains_gas (derived from heating
fuel), lighting outlet counts bulb counts, and the tariff/meter."""
@pytest.fixture
def sample(self) -> EpcPropertyData:
schema = from_dict(SapSchema17_1, load("sap_17_1.json"))
return EpcPropertyDataMapper.from_sap_schema_17_1(schema)
def test_mains_gas_derived_from_heating_fuel(self, sample: EpcPropertyData) -> None:
# Main heating fuel is mains gas (code 1) → mains_gas True.
assert sample.sap_energy_source.mains_gas is True
def test_lighting_outlet_counts_to_bulbs(self, sample: EpcPropertyData) -> None:
# 1 fixed outlet, 1 low-energy → 1 low-energy bulb, 0 incandescent.
assert sample.low_energy_fixed_lighting_bulbs_count == 1
assert sample.incandescent_fixed_lighting_bulbs_count == 0
def test_wind_turbine_fields_pass_through(self, sample: EpcPropertyData) -> None:
assert sample.sap_energy_source.wind_turbines_count == 0
class TestFromSapSchema17_1Heating:
"""Slice D6: full-SAP sap_heating (differing field names) maps onto the
domain SapHeating + MainHeatingDetail the calculator consumes."""

View file

@ -88,6 +88,19 @@ class SapBuildingPart:
building_part_number: Optional[int] = None
@dataclass
class SapEnergySource:
"""Electricity tariff, on-site generation and lighting. Lighting outlet
counts map to the engine's bulb counts; `mains_gas` is derived from the
heating fuel (full SAP has no explicit flag)."""
electricity_tariff: Optional[int] = None
wind_turbines_count: Optional[int] = None
wind_turbine_terrain_type: Optional[int] = None
fixed_lighting_outlets_count: Optional[int] = None
low_energy_fixed_lighting_outlets_count: Optional[int] = None
@dataclass
class SapMainHeatingDetail:
"""One main-heating system. Field names differ from RdSAP (e.g.
@ -157,6 +170,7 @@ class SapSchema17_1:
sap_opening_types: List[SapOpeningType]
sap_building_parts: List[SapBuildingPart]
sap_heating: SapHeating
sap_energy_source: SapEnergySource = field(default_factory=SapEnergySource)
# measured living-room area (m²); the engine consumes it via a back-solved
# habitable_rooms_count (Table 27). Optional — 100% present in the corpus.
living_area: Optional[Union[int, float]] = None