mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Sibling migration to the sap10_calculator move — `domain.ml` now lives
at the root-level layout (`domain/sap10_ml/`) matching the pattern
already used by `domain.addresses`, `domain.tasks`, `domain.postcode`,
and `domain.sap10_calculator`.
Changes:
- `git mv packages/domain/src/domain/ml → domain/sap10_ml` (19 files;
history preserved).
- Subpackage rename: `domain.ml` → `domain.sap10_ml`. 32 references
rewritten across .py and .md files: 11 internal + 21 external
(datatypes/epc/domain/mapper.py, 14 files in domain/sap10_calculator,
2 backend tests, 2 ADRs, 1 README, 1 design doc).
- Path-string updates: `pytest.ini` testpath
`packages/domain/src/domain/ml/tests` → `domain/sap10_ml/tests` so
ML tests stay in the default auto-discovered sweep. `CONTEXT.md`
also updated.
`packages/domain/src/domain/` is now empty — the workspace `domain/`
tree has been fully migrated. Together with the `domain/__init__.py`
deletions from the sap10_calculator commit (29ac35cc), `domain` is
now a single root-level namespace package with subpackages
{addresses, sap10_calculator, sap10_ml, tasks} + the standalone
`postcode.py` module.
Verified:
- Focused sweep (backend mapper-chain + sap10_calculator worksheet
e2e + golden fixtures): 99 passed / 19 failed — identical baseline.
- Wider sweep (all sap10_calculator + sap10_ml): 1654 passed / 20
failed (same pre-existing failures).
- domain/sap10_ml/tests: 210/210 PASSED at new path.
- Pyright net-zero: heat_transmission.py 13, cert_to_inputs.py 35,
mapper.py 33, rdsap_uvalues.py 1 (all unchanged from baseline).
Note: `packages/domain/pyproject.toml` still declares
`packages = ["src/domain"]` for the hatchling wheel — that target
directory is now empty and the wheel build is effectively a no-op.
Retiring the workspace package or repointing the wheel is a follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
365 lines
14 KiB
Python
365 lines
14 KiB
Python
"""Test fixtures for EpcMlTransform tests.
|
|
|
|
`make_minimal_sap10_epc()` constructs a valid EpcPropertyData with the smallest
|
|
sensible defaults for required fields; target values are passed by kwarg so each
|
|
test parametrises only the fields it cares about.
|
|
|
|
`make_window()` builds a SapWindow with sensible SAP10 defaults; pass the fields
|
|
relevant to the test (orientation / dimensions / glazing / draught proofing).
|
|
"""
|
|
|
|
from datetime import date
|
|
from typing import Optional, Union
|
|
|
|
from datatypes.epc.domain.epc_property_data import (
|
|
BuildingPartIdentifier,
|
|
EpcPropertyData,
|
|
InstantaneousWwhrs,
|
|
MainHeatingDetail,
|
|
PhotovoltaicArray,
|
|
PhotovoltaicSupply,
|
|
PhotovoltaicSupplyNoneOrNoDetails,
|
|
PvBatteries,
|
|
PvBattery,
|
|
RenewableHeatIncentive,
|
|
SapBuildingPart,
|
|
SapEnergySource,
|
|
SapFloorDimension,
|
|
SapHeating,
|
|
SapRoofWindow,
|
|
SapRoomInRoof,
|
|
SapVentilation,
|
|
SapWindow,
|
|
WindTurbineDetails,
|
|
WindowTransmissionDetails,
|
|
)
|
|
|
|
|
|
def make_pv_array(
|
|
*,
|
|
peak_power: float = 2.0,
|
|
pitch: int = 2,
|
|
orientation: int = 5,
|
|
overshading: int = 1,
|
|
) -> PhotovoltaicArray:
|
|
"""Build a PhotovoltaicArray with SAP10 defaults (2 kW, S-facing)."""
|
|
return PhotovoltaicArray(
|
|
peak_power=peak_power,
|
|
pitch=pitch,
|
|
orientation=orientation,
|
|
overshading=overshading,
|
|
)
|
|
|
|
|
|
def make_main_heating_detail(
|
|
*,
|
|
main_fuel_type: Union[int, str] = 26, # mains gas (not community)
|
|
heat_emitter_type: Union[int, str] = 1,
|
|
main_heating_control: Union[int, str] = 2106,
|
|
emitter_temperature: Union[int, str] = 1,
|
|
main_heating_category: Optional[int] = 2,
|
|
has_fghrs: bool = False,
|
|
fan_flue_present: Optional[bool] = True,
|
|
boiler_flue_type: Optional[int] = 2,
|
|
central_heating_pump_age: Optional[int] = 0,
|
|
main_heating_number: Optional[int] = 1,
|
|
main_heating_index_number: Optional[int] = None,
|
|
main_heating_data_source: Optional[int] = None,
|
|
sap_main_heating_code: Optional[int] = None,
|
|
) -> MainHeatingDetail:
|
|
"""Build a MainHeatingDetail with SAP10 API defaults (mains gas boiler).
|
|
Pass `main_heating_index_number` to point at a PCDB record (typical
|
|
cert convention is `main_heating_data_source=1` + a PCDB pointer for
|
|
PCDB-listed systems; `main_heating_data_source=2` + `sap_main_heating_
|
|
code` for Table 4b-lodged systems)."""
|
|
return MainHeatingDetail(
|
|
has_fghrs=has_fghrs,
|
|
main_fuel_type=main_fuel_type,
|
|
heat_emitter_type=heat_emitter_type,
|
|
emitter_temperature=emitter_temperature,
|
|
main_heating_control=main_heating_control,
|
|
fan_flue_present=fan_flue_present,
|
|
boiler_flue_type=boiler_flue_type,
|
|
central_heating_pump_age=central_heating_pump_age,
|
|
main_heating_number=main_heating_number,
|
|
main_heating_category=main_heating_category,
|
|
main_heating_index_number=main_heating_index_number,
|
|
main_heating_data_source=main_heating_data_source,
|
|
sap_main_heating_code=sap_main_heating_code,
|
|
)
|
|
|
|
|
|
def make_sap_heating(
|
|
*,
|
|
main_heating_details: Optional[list[MainHeatingDetail]] = None,
|
|
has_fixed_air_conditioning: bool = False,
|
|
water_heating_code: Optional[int] = 901,
|
|
water_heating_fuel: Optional[int] = 26,
|
|
cylinder_size: Optional[Union[int, str]] = None,
|
|
cylinder_insulation_thickness_mm: Optional[int] = None,
|
|
secondary_fuel_type: Optional[int] = None,
|
|
secondary_heating_type: Optional[int] = None,
|
|
number_baths: Optional[int] = None,
|
|
electric_shower_count: Optional[int] = None,
|
|
mixer_shower_count: Optional[int] = None,
|
|
) -> SapHeating:
|
|
"""Build a SapHeating with SAP10 API defaults."""
|
|
return SapHeating(
|
|
instantaneous_wwhrs=InstantaneousWwhrs(),
|
|
main_heating_details=main_heating_details
|
|
if main_heating_details is not None
|
|
else [make_main_heating_detail()],
|
|
has_fixed_air_conditioning=has_fixed_air_conditioning,
|
|
water_heating_code=water_heating_code,
|
|
water_heating_fuel=water_heating_fuel,
|
|
cylinder_size=cylinder_size,
|
|
cylinder_insulation_thickness_mm=cylinder_insulation_thickness_mm,
|
|
secondary_fuel_type=secondary_fuel_type,
|
|
secondary_heating_type=secondary_heating_type,
|
|
number_baths=number_baths,
|
|
electric_shower_count=electric_shower_count,
|
|
mixer_shower_count=mixer_shower_count,
|
|
)
|
|
|
|
|
|
def make_floor_dimension(
|
|
*,
|
|
total_floor_area_m2: float = 50.0,
|
|
room_height_m: float = 2.5,
|
|
party_wall_length_m: float = 5.0,
|
|
heat_loss_perimeter_m: float = 20.0,
|
|
floor: Optional[int] = 0,
|
|
) -> SapFloorDimension:
|
|
"""Build a SapFloorDimension with sensible defaults."""
|
|
return SapFloorDimension(
|
|
room_height_m=room_height_m,
|
|
total_floor_area_m2=total_floor_area_m2,
|
|
party_wall_length_m=party_wall_length_m,
|
|
heat_loss_perimeter_m=heat_loss_perimeter_m,
|
|
floor=floor,
|
|
)
|
|
|
|
|
|
def make_building_part(
|
|
*,
|
|
identifier: BuildingPartIdentifier = BuildingPartIdentifier.MAIN,
|
|
construction_age_band: str = "B",
|
|
wall_construction: Union[int, str] = 3,
|
|
wall_insulation_type: Union[int, str] = 2,
|
|
wall_thickness_measured: bool = True,
|
|
party_wall_construction: Union[int, str] = 1,
|
|
roof_construction: Optional[int] = 4,
|
|
floor_dimensions: Optional[list[SapFloorDimension]] = None,
|
|
sap_room_in_roof: Optional[SapRoomInRoof] = None,
|
|
) -> SapBuildingPart:
|
|
"""Build a SapBuildingPart with sensible SAP10 defaults."""
|
|
return SapBuildingPart(
|
|
identifier=identifier,
|
|
construction_age_band=construction_age_band,
|
|
wall_construction=wall_construction,
|
|
wall_insulation_type=wall_insulation_type,
|
|
wall_thickness_measured=wall_thickness_measured,
|
|
party_wall_construction=party_wall_construction,
|
|
roof_construction=roof_construction,
|
|
sap_floor_dimensions=floor_dimensions
|
|
if floor_dimensions is not None
|
|
else [make_floor_dimension()],
|
|
sap_room_in_roof=sap_room_in_roof,
|
|
)
|
|
|
|
|
|
def make_window(
|
|
*,
|
|
orientation: Union[int, str] = 5, # SAP10: 1=N, 2=NE, 3=E, 4=SE, 5=S, 6=SW, 7=W, 8=NW
|
|
width: float = 1.0,
|
|
height: float = 1.0,
|
|
draught_proofed: bool = True,
|
|
glazing_type: Union[int, str] = 2, # "double glazing 2002-2022"
|
|
glazing_gap: Union[int, str] = "16+",
|
|
window_type: Union[int, str] = 1,
|
|
window_location: Union[int, str] = 0,
|
|
window_wall_type: Union[int, str] = 1,
|
|
permanent_shutters_present: Union[bool, str] = False,
|
|
frame_material: Optional[str] = "PVC",
|
|
frame_factor: Optional[float] = 0.7, # SAP10.2 Table 6c PVC default;
|
|
# mirrors the Elmhurst mapper's
|
|
# surfaced value from Summary §11.
|
|
window_transmission_details: Optional[WindowTransmissionDetails] = None,
|
|
solar_transmittance: Optional[float] = None,
|
|
u_value: float = 2.8,
|
|
) -> SapWindow:
|
|
"""Build a SapWindow with SAP10 defaults; override the fields the test cares about.
|
|
|
|
`solar_transmittance` is a shortcut that builds a
|
|
`WindowTransmissionDetails(u_value, data_source=1, solar_transmittance)`
|
|
when `window_transmission_details` isn't supplied directly — keeps
|
|
Elmhurst §6 fixture literals tight.
|
|
"""
|
|
if window_transmission_details is None and solar_transmittance is not None:
|
|
window_transmission_details = WindowTransmissionDetails(
|
|
u_value=u_value, data_source=1, solar_transmittance=solar_transmittance,
|
|
)
|
|
return SapWindow(
|
|
frame_material=frame_material,
|
|
glazing_gap=glazing_gap,
|
|
orientation=orientation,
|
|
window_type=window_type,
|
|
glazing_type=glazing_type,
|
|
window_width=width,
|
|
window_height=height,
|
|
draught_proofed=draught_proofed,
|
|
window_location=window_location,
|
|
window_wall_type=window_wall_type,
|
|
permanent_shutters_present=permanent_shutters_present,
|
|
frame_factor=frame_factor,
|
|
window_transmission_details=window_transmission_details,
|
|
)
|
|
|
|
|
|
def make_minimal_sap10_epc(
|
|
*,
|
|
energy_rating_current: Optional[int] = None,
|
|
co2_emissions_current: Optional[float] = None,
|
|
energy_consumption_current: Optional[int] = None,
|
|
space_heating_kwh: float = 0.0,
|
|
water_heating_kwh: float = 0.0,
|
|
total_floor_area_m2: float = 70.0,
|
|
door_count: int = 0,
|
|
habitable_rooms_count: int = 0,
|
|
heated_rooms_count: int = 0,
|
|
wet_rooms_count: int = 0,
|
|
extensions_count: int = 0,
|
|
open_chimneys_count: int = 0,
|
|
insulated_door_count: int = 0,
|
|
cfl_fixed_lighting_bulbs_count: int = 0,
|
|
led_fixed_lighting_bulbs_count: int = 0,
|
|
incandescent_fixed_lighting_bulbs_count: int = 0,
|
|
low_energy_fixed_lighting_bulbs_count: Optional[int] = None,
|
|
solar_water_heating: bool = False,
|
|
has_hot_water_cylinder: bool = False,
|
|
has_fixed_air_conditioning: bool = False,
|
|
percent_draughtproofed: Optional[int] = None,
|
|
energy_rating_average: Optional[int] = None,
|
|
environmental_impact_current: Optional[int] = None,
|
|
dwelling_type: str = "Mid-terrace house",
|
|
tenure: str = "1",
|
|
transaction_type: str = "1",
|
|
property_type: Optional[str] = None,
|
|
built_form: Optional[str] = None,
|
|
region_code: Optional[str] = None,
|
|
country_code: Optional[str] = None,
|
|
sap_windows: Optional[list[SapWindow]] = None,
|
|
sap_roof_windows: Optional[list[SapRoofWindow]] = None,
|
|
sap_building_parts: Optional[list[SapBuildingPart]] = None,
|
|
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,
|
|
mechanical_ventilation: Optional[int] = None,
|
|
mechanical_vent_duct_type: Optional[int] = None,
|
|
blocked_chimneys_count: Optional[int] = None,
|
|
pressure_test: Optional[int] = None,
|
|
sap_ventilation: Optional[SapVentilation] = None,
|
|
postcode: str = "A1 1AA",
|
|
) -> EpcPropertyData:
|
|
"""Construct a minimal valid SAP10 EpcPropertyData with parametrisable targets."""
|
|
return EpcPropertyData(
|
|
dwelling_type=dwelling_type,
|
|
inspection_date=date(2025, 6, 1),
|
|
tenure=tenure,
|
|
transaction_type=transaction_type,
|
|
address_line_1="1 Test Street",
|
|
postcode=postcode,
|
|
post_town="Testtown",
|
|
roofs=[],
|
|
walls=[],
|
|
floors=[],
|
|
main_heating=[],
|
|
door_count=door_count,
|
|
sap_heating=sap_heating if sap_heating is not None else SapHeating(
|
|
instantaneous_wwhrs=InstantaneousWwhrs(),
|
|
main_heating_details=[],
|
|
has_fixed_air_conditioning=False,
|
|
),
|
|
sap_windows=list(sap_windows) if sap_windows is not None else [],
|
|
sap_roof_windows=(
|
|
list(sap_roof_windows) if sap_roof_windows is not None else None
|
|
),
|
|
sap_energy_source=SapEnergySource(
|
|
mains_gas=mains_gas,
|
|
meter_type="Single",
|
|
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=electricity_smart_meter_present,
|
|
photovoltaic_arrays=list(photovoltaic_arrays)
|
|
if photovoltaic_arrays is not None
|
|
else None,
|
|
photovoltaic_supply=(
|
|
PhotovoltaicSupply(
|
|
none_or_no_details=PhotovoltaicSupplyNoneOrNoDetails(
|
|
percent_roof_area=photovoltaic_supply_percent_roof_area
|
|
)
|
|
)
|
|
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,
|
|
has_hot_water_cylinder=has_hot_water_cylinder,
|
|
has_fixed_air_conditioning=has_fixed_air_conditioning,
|
|
wet_rooms_count=wet_rooms_count,
|
|
extensions_count=extensions_count,
|
|
heated_rooms_count=heated_rooms_count,
|
|
open_chimneys_count=open_chimneys_count,
|
|
habitable_rooms_count=habitable_rooms_count,
|
|
insulated_door_count=insulated_door_count,
|
|
cfl_fixed_lighting_bulbs_count=cfl_fixed_lighting_bulbs_count,
|
|
led_fixed_lighting_bulbs_count=led_fixed_lighting_bulbs_count,
|
|
incandescent_fixed_lighting_bulbs_count=incandescent_fixed_lighting_bulbs_count,
|
|
low_energy_fixed_lighting_bulbs_count=low_energy_fixed_lighting_bulbs_count,
|
|
total_floor_area_m2=total_floor_area_m2,
|
|
sap_version=10.2,
|
|
energy_rating_current=energy_rating_current,
|
|
co2_emissions_current=co2_emissions_current,
|
|
energy_consumption_current=energy_consumption_current,
|
|
percent_draughtproofed=percent_draughtproofed,
|
|
energy_rating_average=energy_rating_average,
|
|
environmental_impact_current=environmental_impact_current,
|
|
property_type=property_type,
|
|
built_form=built_form,
|
|
region_code=region_code,
|
|
country_code=country_code,
|
|
mechanical_ventilation=mechanical_ventilation,
|
|
mechanical_vent_duct_type=mechanical_vent_duct_type,
|
|
blocked_chimneys_count=blocked_chimneys_count,
|
|
pressure_test=pressure_test,
|
|
sap_ventilation=sap_ventilation,
|
|
renewable_heat_incentive=RenewableHeatIncentive(
|
|
space_heating_kwh=space_heating_kwh,
|
|
water_heating_kwh=water_heating_kwh,
|
|
),
|
|
)
|