mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
slice 11b: PV battery, wind turbine, energy source flags
Nine more energy-source features land: has_pv_battery, pv_battery_count, pv_battery_capacity_kwh (count × per-unit capacity from pv_batteries.pv_battery, nullable when count=0), has_wind_turbine, wind_turbine_count, mains_gas (the dominant fuel-deduction signal), and the three smart-meter / export booleans (electricity_smart_meter_present, gas_smart_meter_present, is_dwelling_export_capable). Closes the PV/solar feature group started in slice 11a. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
706d1b5b66
commit
559a2128b9
3 changed files with 192 additions and 6 deletions
|
|
@ -18,6 +18,8 @@ from datatypes.epc.domain.epc_property_data import (
|
|||
PhotovoltaicArray,
|
||||
PhotovoltaicSupply,
|
||||
PhotovoltaicSupplyNoneOrNoDetails,
|
||||
PvBatteries,
|
||||
PvBattery,
|
||||
RenewableHeatIncentive,
|
||||
SapBuildingPart,
|
||||
SapEnergySource,
|
||||
|
|
@ -25,6 +27,7 @@ from datatypes.epc.domain.epc_property_data import (
|
|||
SapHeating,
|
||||
SapRoomInRoof,
|
||||
SapWindow,
|
||||
WindTurbineDetails,
|
||||
WindowTransmissionDetails,
|
||||
)
|
||||
|
||||
|
|
@ -212,6 +215,13 @@ def make_minimal_sap10_epc(
|
|||
sap_heating: Optional[SapHeating] = None,
|
||||
photovoltaic_arrays: Optional[list[PhotovoltaicArray]] = None,
|
||||
photovoltaic_supply_percent_roof_area: Optional[int] = None,
|
||||
mains_gas: bool = True,
|
||||
electricity_smart_meter_present: bool = False,
|
||||
gas_smart_meter_present: bool = False,
|
||||
is_dwelling_export_capable: bool = False,
|
||||
pv_battery_count: int = 0,
|
||||
pv_battery_capacity_per_unit_kwh: Optional[float] = None,
|
||||
wind_turbines_count: int = 0,
|
||||
) -> EpcPropertyData:
|
||||
"""Construct a minimal valid SAP10 EpcPropertyData with parametrisable targets."""
|
||||
return EpcPropertyData(
|
||||
|
|
@ -234,14 +244,14 @@ def make_minimal_sap10_epc(
|
|||
),
|
||||
sap_windows=list(sap_windows) if sap_windows is not None else [],
|
||||
sap_energy_source=SapEnergySource(
|
||||
mains_gas=True,
|
||||
mains_gas=mains_gas,
|
||||
meter_type="Single",
|
||||
pv_battery_count=0,
|
||||
wind_turbines_count=0,
|
||||
gas_smart_meter_present=False,
|
||||
is_dwelling_export_capable=False,
|
||||
pv_battery_count=pv_battery_count,
|
||||
wind_turbines_count=wind_turbines_count,
|
||||
gas_smart_meter_present=gas_smart_meter_present,
|
||||
is_dwelling_export_capable=is_dwelling_export_capable,
|
||||
wind_turbines_terrain_type="Suburban",
|
||||
electricity_smart_meter_present=False,
|
||||
electricity_smart_meter_present=electricity_smart_meter_present,
|
||||
photovoltaic_arrays=list(photovoltaic_arrays)
|
||||
if photovoltaic_arrays is not None
|
||||
else None,
|
||||
|
|
@ -254,6 +264,20 @@ def make_minimal_sap10_epc(
|
|||
if photovoltaic_supply_percent_roof_area is not None
|
||||
else None
|
||||
),
|
||||
pv_batteries=(
|
||||
PvBatteries(
|
||||
pv_battery=PvBattery(
|
||||
battery_capacity=pv_battery_capacity_per_unit_kwh
|
||||
)
|
||||
)
|
||||
if pv_battery_capacity_per_unit_kwh is not None
|
||||
else None
|
||||
),
|
||||
wind_turbine_details=(
|
||||
WindTurbineDetails(hub_height=5.0, rotor_diameter=2.0)
|
||||
if wind_turbines_count > 0
|
||||
else None
|
||||
),
|
||||
),
|
||||
sap_building_parts=list(sap_building_parts) if sap_building_parts is not None else [],
|
||||
solar_water_heating=solar_water_heating,
|
||||
|
|
|
|||
|
|
@ -942,6 +942,104 @@ def test_to_row_treats_zero_percent_roof_area_as_no_pv() -> None:
|
|||
assert row["pv_percent_roof_area"] is None
|
||||
|
||||
|
||||
_ENERGY_SOURCE_FEATURES_NULLABLE: dict[str, tuple[type, bool, bool]] = {
|
||||
# name → (dtype, nullable, categorical)
|
||||
"has_pv_battery": (bool, False, False),
|
||||
"pv_battery_count": (int, False, False),
|
||||
"pv_battery_capacity_kwh": (float, True, False),
|
||||
"has_wind_turbine": (bool, False, False),
|
||||
"wind_turbine_count": (int, False, False),
|
||||
"mains_gas": (bool, False, False),
|
||||
"electricity_smart_meter_present": (bool, False, False),
|
||||
"gas_smart_meter_present": (bool, False, False),
|
||||
"is_dwelling_export_capable": (bool, False, False),
|
||||
}
|
||||
|
||||
|
||||
def test_schema_advertises_energy_source_features() -> None:
|
||||
# Arrange
|
||||
transform = EpcMlTransform()
|
||||
|
||||
# Act
|
||||
schema = transform.schema()
|
||||
|
||||
# Assert
|
||||
for name, (expected_dtype, expected_nullable, expected_categorical) in (
|
||||
_ENERGY_SOURCE_FEATURES_NULLABLE.items()
|
||||
):
|
||||
assert name in schema.feature_columns, name
|
||||
column = schema.feature_columns[name]
|
||||
assert column.dtype is expected_dtype, name
|
||||
assert column.nullable is expected_nullable, name
|
||||
assert column.categorical is expected_categorical, name
|
||||
|
||||
|
||||
def test_to_row_extracts_pv_battery_and_capacity() -> None:
|
||||
# Arrange — two batteries of 5.0 kWh each
|
||||
epc = make_minimal_sap10_epc(
|
||||
energy_rating_current=82,
|
||||
pv_battery_count=2,
|
||||
pv_battery_capacity_per_unit_kwh=5.0,
|
||||
)
|
||||
transform = EpcMlTransform()
|
||||
|
||||
# Act
|
||||
row = transform.to_row(epc)
|
||||
|
||||
# Assert
|
||||
assert row["has_pv_battery"] is True
|
||||
assert row["pv_battery_count"] == 2
|
||||
assert row["pv_battery_capacity_kwh"] == pytest.approx(10.0)
|
||||
|
||||
|
||||
def test_to_row_returns_no_pv_battery_when_count_zero() -> None:
|
||||
# Arrange — no battery
|
||||
epc = make_minimal_sap10_epc(energy_rating_current=82)
|
||||
transform = EpcMlTransform()
|
||||
|
||||
# Act
|
||||
row = transform.to_row(epc)
|
||||
|
||||
# Assert
|
||||
assert row["has_pv_battery"] is False
|
||||
assert row["pv_battery_count"] == 0
|
||||
assert row["pv_battery_capacity_kwh"] is None
|
||||
|
||||
|
||||
def test_to_row_flags_wind_turbine() -> None:
|
||||
# Arrange
|
||||
epc = make_minimal_sap10_epc(energy_rating_current=82, wind_turbines_count=1)
|
||||
transform = EpcMlTransform()
|
||||
|
||||
# Act
|
||||
row = transform.to_row(epc)
|
||||
|
||||
# Assert
|
||||
assert row["has_wind_turbine"] is True
|
||||
assert row["wind_turbine_count"] == 1
|
||||
|
||||
|
||||
def test_to_row_extracts_energy_source_booleans() -> None:
|
||||
# Arrange — gas + electricity smart meters, export capable
|
||||
epc = make_minimal_sap10_epc(
|
||||
energy_rating_current=82,
|
||||
mains_gas=True,
|
||||
electricity_smart_meter_present=True,
|
||||
gas_smart_meter_present=True,
|
||||
is_dwelling_export_capable=True,
|
||||
)
|
||||
transform = EpcMlTransform()
|
||||
|
||||
# Act
|
||||
row = transform.to_row(epc)
|
||||
|
||||
# Assert
|
||||
assert row["mains_gas"] is True
|
||||
assert row["electricity_smart_meter_present"] is True
|
||||
assert row["gas_smart_meter_present"] is True
|
||||
assert row["is_dwelling_export_capable"] is True
|
||||
|
||||
|
||||
def test_to_row_area_weights_window_u_value_and_solar_transmittance() -> None:
|
||||
# Arrange — two windows with transmission details; one without.
|
||||
sap_windows = [
|
||||
|
|
|
|||
|
|
@ -380,6 +380,43 @@ _FEATURE_COLUMNS: dict[str, ColumnSpec] = {
|
|||
dtype=int, nullable=True,
|
||||
description="Percent of roof covered by PV — populated only when capacity_source = 'estimated_from_roof_area'.",
|
||||
),
|
||||
# PV battery, wind turbine, energy source flags
|
||||
"has_pv_battery": ColumnSpec(
|
||||
dtype=bool, nullable=False,
|
||||
description="True if the property has at least one PV battery.",
|
||||
),
|
||||
"pv_battery_count": ColumnSpec(
|
||||
dtype=int, nullable=False, description="Number of PV batteries."
|
||||
),
|
||||
"pv_battery_capacity_kwh": ColumnSpec(
|
||||
dtype=float, nullable=True,
|
||||
description=(
|
||||
"Total PV battery capacity (kWh) — pv_battery_count × per-unit capacity "
|
||||
"from sap_energy_source.pv_batteries. Null when count=0."
|
||||
),
|
||||
),
|
||||
"has_wind_turbine": ColumnSpec(
|
||||
dtype=bool, nullable=False,
|
||||
description="True if the property has at least one wind turbine.",
|
||||
),
|
||||
"wind_turbine_count": ColumnSpec(
|
||||
dtype=int, nullable=False, description="Number of wind turbines."
|
||||
),
|
||||
"mains_gas": ColumnSpec(
|
||||
dtype=bool, nullable=False,
|
||||
description="Property is connected to mains gas (strong fuel-deduction signal).",
|
||||
),
|
||||
"electricity_smart_meter_present": ColumnSpec(
|
||||
dtype=bool, nullable=False,
|
||||
description="Electricity smart meter installed.",
|
||||
),
|
||||
"gas_smart_meter_present": ColumnSpec(
|
||||
dtype=bool, nullable=False, description="Gas smart meter installed."
|
||||
),
|
||||
"is_dwelling_export_capable": ColumnSpec(
|
||||
dtype=bool, nullable=False,
|
||||
description="Dwelling has an export-capable connection (eligible for SEG).",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -458,6 +495,7 @@ class EpcMlTransform:
|
|||
building_part_aggregates = _building_part_aggregates(epc.sap_building_parts)
|
||||
heating_aggregates = _heating_aggregates(epc.sap_heating)
|
||||
pv_aggregates = _pv_aggregates(epc.sap_energy_source)
|
||||
energy_source_other = _energy_source_other_aggregates(epc.sap_energy_source)
|
||||
return {
|
||||
# Features — geometry
|
||||
"total_floor_area_m2": epc.total_floor_area_m2,
|
||||
|
|
@ -496,6 +534,8 @@ class EpcMlTransform:
|
|||
**heating_aggregates,
|
||||
# Features — PV (capacity source + array aggregates by SAP octant)
|
||||
**pv_aggregates,
|
||||
# Features — battery, wind turbine, mains gas + smart meter flags
|
||||
**energy_source_other,
|
||||
# Targets
|
||||
"sap_score": epc.energy_rating_current,
|
||||
"co2_emissions": epc.co2_emissions_current,
|
||||
|
|
@ -569,6 +609,30 @@ def _pv_aggregates(es: SapEnergySource) -> dict[str, Any]:
|
|||
return aggregates
|
||||
|
||||
|
||||
def _energy_source_other_aggregates(es: SapEnergySource) -> dict[str, Any]:
|
||||
"""Pull battery, wind turbine, and household energy source flags.
|
||||
|
||||
Battery capacity multiplies pv_battery_count by the per-unit capacity carried
|
||||
on pv_batteries.pv_battery; null when no battery is present.
|
||||
"""
|
||||
battery_capacity_kwh: Optional[float] = None
|
||||
if es.pv_battery_count > 0 and es.pv_batteries is not None:
|
||||
battery_capacity_kwh = (
|
||||
es.pv_battery_count * es.pv_batteries.pv_battery.battery_capacity
|
||||
)
|
||||
return {
|
||||
"has_pv_battery": es.pv_battery_count > 0,
|
||||
"pv_battery_count": es.pv_battery_count,
|
||||
"pv_battery_capacity_kwh": battery_capacity_kwh,
|
||||
"has_wind_turbine": es.wind_turbines_count > 0,
|
||||
"wind_turbine_count": es.wind_turbines_count,
|
||||
"mains_gas": es.mains_gas,
|
||||
"electricity_smart_meter_present": es.electricity_smart_meter_present,
|
||||
"gas_smart_meter_present": es.gas_smart_meter_present,
|
||||
"is_dwelling_export_capable": es.is_dwelling_export_capable,
|
||||
}
|
||||
|
||||
|
||||
def _heating_aggregates(sap_heating: SapHeating) -> dict[str, Any]:
|
||||
"""Aggregate sap_heating into 15 heating-feature columns.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue