Model/domain/sap10_ml/tests/_fixtures.py
Khalim Conn-Kowlessar 68401c517a refactor: lift-and-shift packages/domain/src/domain/ml → domain/sap10_ml
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>
2026-05-26 13:01:35 +00:00

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