mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
A Landlord Override's building_part is a positional index (0=main, 1=extension 1…, ADR-0004), but the gov-API EPC can label that slot differently (e.g. lodge the 2nd part as 'other', not 'extension_1'). The previous fix skipped such orphaned overrides, silently discarding the landlord's correction. Now the override falls back onto the EPC's part at that position (via _resolve_part), so the correction lands; only a position the EPC models no part at is skipped (no geometry to model a wholly-absent part). Replaces the skip-only behaviour. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
586 lines
21 KiB
Python
586 lines
21 KiB
Python
"""Behaviour of the Overlay Applicator: folding Simulation Overlays
|
|
(EpcSimulation) onto a baseline EpcPropertyData to produce a new one for
|
|
the calculator. See ADR-0016 and the Modelling glossary in CONTEXT.md.
|
|
"""
|
|
|
|
from datatypes.epc.domain.epc_property_data import (
|
|
BuildingPartIdentifier,
|
|
EpcPropertyData,
|
|
PhotovoltaicArray,
|
|
PvBatteries,
|
|
PvBattery,
|
|
SapBuildingPart,
|
|
SapVentilation,
|
|
)
|
|
from domain.modelling.simulation import (
|
|
BuildingPartOverlay,
|
|
EpcSimulation,
|
|
HeatingOverlay,
|
|
LightingOverlay,
|
|
SecondaryHeatingOverlay,
|
|
SolarOverlay,
|
|
VentilationOverlay,
|
|
WindowOverlay,
|
|
)
|
|
from domain.modelling.scoring.overlay_applicator import apply_simulations
|
|
from tests.domain.sap10_calculator.worksheet._elmhurst_worksheet_000490 import (
|
|
build_epc,
|
|
)
|
|
|
|
|
|
def _part(epc: EpcPropertyData, identifier: BuildingPartIdentifier) -> SapBuildingPart:
|
|
return next(p for p in epc.sap_building_parts if p.identifier is identifier)
|
|
|
|
|
|
def test_apply_writes_targeted_building_part_and_leaves_others_untouched() -> None:
|
|
# Arrange
|
|
baseline: EpcPropertyData = build_epc()
|
|
extension_before: int | str = _part(
|
|
baseline, BuildingPartIdentifier.EXTENSION_1
|
|
).wall_insulation_type
|
|
simulation = EpcSimulation(
|
|
building_parts={
|
|
BuildingPartIdentifier.MAIN: BuildingPartOverlay(wall_insulation_type=1)
|
|
}
|
|
)
|
|
|
|
# Act
|
|
result: EpcPropertyData = apply_simulations(baseline, [simulation])
|
|
|
|
# Assert
|
|
assert _part(result, BuildingPartIdentifier.MAIN).wall_insulation_type == 1
|
|
assert (
|
|
_part(result, BuildingPartIdentifier.EXTENSION_1).wall_insulation_type
|
|
== extension_before
|
|
)
|
|
|
|
|
|
def test_override_for_an_absent_semantic_part_lands_on_the_part_at_that_position() -> (
|
|
None
|
|
):
|
|
# `building_part` is a POSITIONAL index (0=main, 1=extension 1…, ADR-0004). The
|
|
# gov-API EPC can label its parts differently (e.g. a 2nd part lodged as `other`
|
|
# rather than `extension_1`). An `extension_1` override must still land on the
|
|
# part at position 1 — the landlord's correction is applied, not dropped.
|
|
# Arrange — build_epc() is [MAIN, EXTENSION_1]; relabel the 2nd part to OTHER so
|
|
# the EXTENSION_1 identifier is absent but position 1 still exists.
|
|
baseline: EpcPropertyData = build_epc()
|
|
baseline.sap_building_parts[1].identifier = BuildingPartIdentifier.OTHER
|
|
simulation = EpcSimulation(
|
|
building_parts={
|
|
BuildingPartIdentifier.EXTENSION_1: BuildingPartOverlay(
|
|
wall_insulation_type=3
|
|
)
|
|
}
|
|
)
|
|
|
|
# Act
|
|
result: EpcPropertyData = apply_simulations(baseline, [simulation])
|
|
|
|
# Assert — the override folded onto the part at position 1 (the OTHER part).
|
|
assert _part(result, BuildingPartIdentifier.OTHER).wall_insulation_type == 3
|
|
|
|
|
|
def test_override_with_no_part_at_that_position_is_skipped() -> None:
|
|
# When there is genuinely no part at the override's position (the EPC models
|
|
# fewer parts than the index), the override is skipped rather than crashing —
|
|
# we cannot model an extension we have no geometry for.
|
|
# Arrange — build_epc() has 2 parts (positions 0, 1); position 2 is absent.
|
|
baseline: EpcPropertyData = build_epc()
|
|
simulation = EpcSimulation(
|
|
building_parts={
|
|
BuildingPartIdentifier.MAIN: BuildingPartOverlay(wall_insulation_type=1),
|
|
BuildingPartIdentifier.EXTENSION_2: BuildingPartOverlay(
|
|
wall_insulation_type=1
|
|
),
|
|
}
|
|
)
|
|
|
|
# Act
|
|
result: EpcPropertyData = apply_simulations(baseline, [simulation])
|
|
|
|
# Assert — the present part got its overlay; nothing was added for position 2.
|
|
assert _part(result, BuildingPartIdentifier.MAIN).wall_insulation_type == 1
|
|
assert len(result.sap_building_parts) == len(baseline.sap_building_parts)
|
|
|
|
|
|
def test_flat_roof_construction_type_folds_onto_the_part() -> None:
|
|
# ADR-0033: a flat-roof landlord override sets `roof_construction_type` so the
|
|
# calculator's flat path (`"flat" in roof_construction_type`) fires the
|
|
# age-band default. Proves the generic field loop wires the new overlay field.
|
|
# Arrange
|
|
baseline: EpcPropertyData = build_epc()
|
|
simulation = EpcSimulation(
|
|
building_parts={
|
|
BuildingPartIdentifier.MAIN: BuildingPartOverlay(
|
|
roof_construction_type="Flat"
|
|
)
|
|
}
|
|
)
|
|
|
|
# Act
|
|
result: EpcPropertyData = apply_simulations(baseline, [simulation])
|
|
|
|
# Assert
|
|
assert _part(result, BuildingPartIdentifier.MAIN).roof_construction_type == "Flat"
|
|
|
|
|
|
def test_empty_simulation_is_a_no_op() -> None:
|
|
# Arrange
|
|
baseline: EpcPropertyData = build_epc()
|
|
|
|
# Act
|
|
result: EpcPropertyData = apply_simulations(baseline, [EpcSimulation()])
|
|
|
|
# Assert
|
|
assert result == baseline
|
|
|
|
|
|
def test_later_simulation_wins_on_a_shared_field() -> None:
|
|
# Arrange
|
|
baseline: EpcPropertyData = build_epc()
|
|
first = EpcSimulation(
|
|
building_parts={
|
|
BuildingPartIdentifier.MAIN: BuildingPartOverlay(wall_insulation_type=1)
|
|
}
|
|
)
|
|
second = EpcSimulation(
|
|
building_parts={
|
|
BuildingPartIdentifier.MAIN: BuildingPartOverlay(wall_insulation_type=2)
|
|
}
|
|
)
|
|
|
|
# Act
|
|
result: EpcPropertyData = apply_simulations(baseline, [first, second])
|
|
|
|
# Assert
|
|
assert _part(result, BuildingPartIdentifier.MAIN).wall_insulation_type == 2
|
|
|
|
|
|
def test_apply_writes_dwelling_ventilation_onto_sap_ventilation() -> None:
|
|
# Arrange — a Measure Dependency overlay targets the whole-dwelling
|
|
# ventilation system (no building part), e.g. retrofit MEV.
|
|
baseline: EpcPropertyData = build_epc()
|
|
simulation = EpcSimulation(
|
|
ventilation=VentilationOverlay(
|
|
mechanical_ventilation_kind="EXTRACT_OR_PIV_OUTSIDE"
|
|
)
|
|
)
|
|
|
|
# Act
|
|
result: EpcPropertyData = apply_simulations(baseline, [simulation])
|
|
|
|
# Assert
|
|
assert result.sap_ventilation is not None
|
|
assert (
|
|
result.sap_ventilation.mechanical_ventilation_kind
|
|
== "EXTRACT_OR_PIV_OUTSIDE"
|
|
)
|
|
|
|
|
|
def test_ventilation_overlay_creates_sap_ventilation_when_baseline_has_none() -> None:
|
|
# Arrange — a naturally-ventilated baseline that lodged no SapVentilation.
|
|
baseline: EpcPropertyData = build_epc()
|
|
baseline.sap_ventilation = None
|
|
simulation = EpcSimulation(
|
|
ventilation=VentilationOverlay(
|
|
mechanical_ventilation_kind="EXTRACT_OR_PIV_OUTSIDE"
|
|
)
|
|
)
|
|
|
|
# Act
|
|
result: EpcPropertyData = apply_simulations(baseline, [simulation])
|
|
|
|
# Assert
|
|
assert isinstance(result.sap_ventilation, SapVentilation)
|
|
assert (
|
|
result.sap_ventilation.mechanical_ventilation_kind
|
|
== "EXTRACT_OR_PIV_OUTSIDE"
|
|
)
|
|
|
|
|
|
def test_ventilation_overlay_leaves_building_parts_and_baseline_untouched() -> None:
|
|
# Arrange
|
|
baseline: EpcPropertyData = build_epc()
|
|
main_before: int | str = _part(
|
|
baseline, BuildingPartIdentifier.MAIN
|
|
).wall_insulation_type
|
|
simulation = EpcSimulation(
|
|
ventilation=VentilationOverlay(
|
|
mechanical_ventilation_kind="EXTRACT_OR_PIV_OUTSIDE"
|
|
)
|
|
)
|
|
|
|
# Act
|
|
result: EpcPropertyData = apply_simulations(baseline, [simulation])
|
|
|
|
# Assert — ventilation overlay touches only sap_ventilation; the baseline
|
|
# is never mutated.
|
|
assert _part(result, BuildingPartIdentifier.MAIN).wall_insulation_type == main_before
|
|
assert baseline.sap_ventilation is not None
|
|
assert baseline.sap_ventilation.mechanical_ventilation_kind is None
|
|
|
|
|
|
def test_baseline_is_not_mutated() -> None:
|
|
# Arrange
|
|
baseline: EpcPropertyData = build_epc()
|
|
original: int | str = _part(
|
|
baseline, BuildingPartIdentifier.MAIN
|
|
).wall_insulation_type
|
|
|
|
# Act
|
|
_: EpcPropertyData = apply_simulations(
|
|
baseline,
|
|
[
|
|
EpcSimulation(
|
|
building_parts={
|
|
BuildingPartIdentifier.MAIN: BuildingPartOverlay(
|
|
wall_insulation_type=1
|
|
)
|
|
}
|
|
)
|
|
],
|
|
)
|
|
|
|
# Assert
|
|
assert (
|
|
_part(baseline, BuildingPartIdentifier.MAIN).wall_insulation_type == original
|
|
)
|
|
|
|
|
|
def test_apply_folds_a_window_overlay_by_index_into_transmission_details() -> None:
|
|
# Arrange — window 0 starts double (glazing_type 2, U 2.8, g 0.76); the
|
|
# overlay upgrades it to a modern double spec, writing the U-value and
|
|
# solar-g into the nested WindowTransmissionDetails (ADR-0022).
|
|
baseline: EpcPropertyData = build_epc()
|
|
|
|
# Act — target window 0 by its sap_windows index.
|
|
result: EpcPropertyData = apply_simulations(
|
|
baseline,
|
|
[
|
|
EpcSimulation(
|
|
windows={
|
|
0: WindowOverlay(
|
|
glazing_type=5, u_value=1.40, solar_transmittance=0.72
|
|
)
|
|
}
|
|
)
|
|
],
|
|
)
|
|
|
|
# Assert — glazing_type set on the window; U/g routed into the transmission
|
|
# details (where the cascade reads them); other windows untouched.
|
|
upgraded = result.sap_windows[0]
|
|
assert upgraded.glazing_type == 5
|
|
assert upgraded.window_transmission_details is not None
|
|
assert abs(upgraded.window_transmission_details.u_value - 1.40) <= 1e-9
|
|
assert abs(upgraded.window_transmission_details.solar_transmittance - 0.72) <= 1e-9
|
|
assert result.sap_windows[1].window_transmission_details is not None
|
|
assert abs(result.sap_windows[1].window_transmission_details.u_value - 2.8) <= 1e-9
|
|
|
|
|
|
def test_baseline_windows_are_not_mutated_by_a_window_overlay() -> None:
|
|
# Arrange
|
|
baseline: EpcPropertyData = build_epc()
|
|
assert baseline.sap_windows[0].window_transmission_details is not None
|
|
original_u: float = baseline.sap_windows[0].window_transmission_details.u_value
|
|
|
|
# Act
|
|
_: EpcPropertyData = apply_simulations(
|
|
baseline,
|
|
[EpcSimulation(windows={0: WindowOverlay(u_value=1.40)})],
|
|
)
|
|
|
|
# Assert
|
|
assert baseline.sap_windows[0].window_transmission_details is not None
|
|
assert (
|
|
baseline.sap_windows[0].window_transmission_details.u_value == original_u
|
|
)
|
|
|
|
|
|
def test_apply_writes_dwelling_lighting_onto_top_level_bulb_counts() -> None:
|
|
# Arrange — a whole-dwelling lighting change (no building part), e.g. an
|
|
# all-LED upgrade folded onto the top-level bulb counts (ADR-0023).
|
|
baseline: EpcPropertyData = build_epc()
|
|
simulation = EpcSimulation(
|
|
lighting=LightingOverlay(
|
|
led_fixed_lighting_bulbs_count=8,
|
|
cfl_fixed_lighting_bulbs_count=0,
|
|
incandescent_fixed_lighting_bulbs_count=0,
|
|
low_energy_fixed_lighting_bulbs_count=0,
|
|
)
|
|
)
|
|
|
|
# Act
|
|
result: EpcPropertyData = apply_simulations(baseline, [simulation])
|
|
|
|
# Assert
|
|
assert result.led_fixed_lighting_bulbs_count == 8
|
|
assert result.cfl_fixed_lighting_bulbs_count == 0
|
|
assert result.incandescent_fixed_lighting_bulbs_count == 0
|
|
assert result.low_energy_fixed_lighting_bulbs_count == 0
|
|
|
|
|
|
def test_apply_folds_a_heating_overlay_across_all_five_locations() -> None:
|
|
# Arrange — a whole-system HHR storage bundle replacing 000490's gas combi
|
|
# (fuel 26, control 2106, no cylinder, mains_gas True). The heating overlay
|
|
# is the deepest surface: it writes across main_heating_details[0],
|
|
# sap_heating, the top-level EpcPropertyData, and sap_energy_source at once
|
|
# (ADR-0024).
|
|
baseline: EpcPropertyData = build_epc()
|
|
simulation = EpcSimulation(
|
|
heating=HeatingOverlay(
|
|
main_fuel_type=30,
|
|
sap_main_heating_code=409,
|
|
main_heating_control=2404,
|
|
water_heating_code=903,
|
|
water_heating_fuel=30,
|
|
cylinder_size=2,
|
|
cylinder_insulation_type=1,
|
|
cylinder_insulation_thickness_mm=120,
|
|
has_hot_water_cylinder=True,
|
|
meter_type="18 Hour",
|
|
mains_gas=False,
|
|
)
|
|
)
|
|
|
|
# Act
|
|
result: EpcPropertyData = apply_simulations(baseline, [simulation])
|
|
|
|
# Assert — every targeted field routed to its home object.
|
|
main = result.sap_heating.main_heating_details[0]
|
|
assert main.main_fuel_type == 30
|
|
assert main.sap_main_heating_code == 409
|
|
assert main.main_heating_control == 2404
|
|
assert result.sap_heating.water_heating_code == 903
|
|
assert result.sap_heating.water_heating_fuel == 30
|
|
assert result.sap_heating.cylinder_size == 2
|
|
assert result.sap_heating.cylinder_insulation_type == 1
|
|
assert result.sap_heating.cylinder_insulation_thickness_mm == 120
|
|
assert result.has_hot_water_cylinder is True
|
|
assert result.sap_energy_source is not None
|
|
assert result.sap_energy_source.meter_type == "18 Hour"
|
|
assert result.sap_energy_source.mains_gas is False
|
|
|
|
|
|
def test_secondary_heating_overlay_clears_the_lodged_secondary() -> None:
|
|
# Arrange — 000490 lodges a secondary system (SAP code 691, electric panel/
|
|
# convector/radiant heaters). Pin a fuel on it too so we prove the fold
|
|
# clears BOTH the type and the fuel (ADR-0028).
|
|
baseline: EpcPropertyData = build_epc()
|
|
baseline.sap_heating.secondary_fuel_type = 30
|
|
assert baseline.sap_heating.secondary_heating_type == 691
|
|
|
|
# Act — fold a removal overlay.
|
|
result: EpcPropertyData = apply_simulations(
|
|
baseline, [EpcSimulation(secondary_heating=SecondaryHeatingOverlay())]
|
|
)
|
|
|
|
# Assert — the secondary is gone from the dwelling handed to the calculator.
|
|
assert result.sap_heating.secondary_heating_type is None
|
|
assert result.sap_heating.secondary_fuel_type is None
|
|
|
|
|
|
def test_secondary_heating_removal_does_not_mutate_the_baseline() -> None:
|
|
# Arrange — 000490 lodges secondary SAP code 691.
|
|
baseline: EpcPropertyData = build_epc()
|
|
assert baseline.sap_heating.secondary_heating_type == 691
|
|
|
|
# Act — fold a removal overlay.
|
|
_: EpcPropertyData = apply_simulations(
|
|
baseline, [EpcSimulation(secondary_heating=SecondaryHeatingOverlay())]
|
|
)
|
|
|
|
# Assert — the baseline's secondary is untouched (the fold copies first).
|
|
assert baseline.sap_heating.secondary_heating_type == 691
|
|
|
|
|
|
def test_baseline_heating_is_not_mutated_by_a_heating_overlay() -> None:
|
|
# Arrange — 000490 lodges a mains-gas combi (fuel 26, control 2106, no
|
|
# cylinder, mains_gas True).
|
|
baseline: EpcPropertyData = build_epc()
|
|
original_fuel = baseline.sap_heating.main_heating_details[0].main_fuel_type
|
|
original_control = baseline.sap_heating.main_heating_details[0].main_heating_control
|
|
original_wh_code: int | None = baseline.sap_heating.water_heating_code
|
|
original_cylinder = baseline.has_hot_water_cylinder
|
|
assert baseline.sap_energy_source is not None
|
|
original_mains_gas = baseline.sap_energy_source.mains_gas
|
|
|
|
# Act — fold an HHR storage bundle.
|
|
_: EpcPropertyData = apply_simulations(
|
|
baseline,
|
|
[
|
|
EpcSimulation(
|
|
heating=HeatingOverlay(
|
|
main_fuel_type=30,
|
|
sap_main_heating_code=409,
|
|
main_heating_control=2404,
|
|
water_heating_code=903,
|
|
has_hot_water_cylinder=True,
|
|
mains_gas=False,
|
|
)
|
|
)
|
|
],
|
|
)
|
|
|
|
# Assert — the baseline's heating is untouched.
|
|
assert baseline.sap_heating.main_heating_details[0].main_fuel_type == original_fuel
|
|
assert (
|
|
baseline.sap_heating.main_heating_details[0].main_heating_control
|
|
== original_control
|
|
)
|
|
assert baseline.sap_heating.water_heating_code == original_wh_code
|
|
assert baseline.has_hot_water_cylinder == original_cylinder
|
|
assert baseline.sap_energy_source.mains_gas == original_mains_gas
|
|
|
|
|
|
def test_heating_index_overlay_clears_a_stale_sap_main_heating_code() -> None:
|
|
# Arrange — 000490's gas combi lodges a Table 4a code; an ASHP bundle sets a
|
|
# PCDB index instead. The two are mutually-exclusive efficiency anchors, so
|
|
# the stale code must be cleared or it wins the calculator's dispatch.
|
|
baseline: EpcPropertyData = build_epc()
|
|
baseline.sap_heating.main_heating_details[0].sap_main_heating_code = 104
|
|
|
|
# Act
|
|
result: EpcPropertyData = apply_simulations(
|
|
baseline,
|
|
[
|
|
EpcSimulation(
|
|
heating=HeatingOverlay(
|
|
main_heating_index_number=101413, main_heating_category=4
|
|
)
|
|
)
|
|
],
|
|
)
|
|
|
|
# Assert — the index is set and the old SAP code is gone.
|
|
main = result.sap_heating.main_heating_details[0]
|
|
assert main.main_heating_index_number == 101413
|
|
assert main.sap_main_heating_code is None
|
|
|
|
|
|
def test_heating_sap_code_overlay_clears_a_stale_index() -> None:
|
|
# Arrange — a dwelling with a PCDB-indexed system; an HHR storage bundle sets
|
|
# a Table 4a code instead, so the stale index must be cleared.
|
|
baseline: EpcPropertyData = build_epc()
|
|
baseline.sap_heating.main_heating_details[0].main_heating_index_number = 8262
|
|
|
|
# Act
|
|
result: EpcPropertyData = apply_simulations(
|
|
baseline,
|
|
[EpcSimulation(heating=HeatingOverlay(sap_main_heating_code=409))],
|
|
)
|
|
|
|
# Assert
|
|
main = result.sap_heating.main_heating_details[0]
|
|
assert main.sap_main_heating_code == 409
|
|
assert main.main_heating_index_number is None
|
|
|
|
|
|
def test_apply_folds_a_solar_overlay_onto_the_energy_source() -> None:
|
|
# Arrange — 000490 lodges no PV, not export-capable, no diverter. A Solar PV
|
|
# Option installs a two-segment array, ensures export, and adds a battery
|
|
# (ADR-0026). The solar overlay is the sixth surface; it writes onto
|
|
# sap_energy_source.
|
|
baseline: EpcPropertyData = build_epc()
|
|
arrays = [
|
|
PhotovoltaicArray(peak_power=4.8, pitch=2, orientation=5, overshading=1),
|
|
PhotovoltaicArray(peak_power=1.2, pitch=2, orientation=6, overshading=2),
|
|
]
|
|
simulation = EpcSimulation(
|
|
solar=SolarOverlay(
|
|
photovoltaic_arrays=arrays,
|
|
pv_diverter_present=True,
|
|
pv_connection=1,
|
|
is_dwelling_export_capable=True,
|
|
pv_batteries=PvBatteries(pv_battery=PvBattery(battery_capacity=5.0)),
|
|
)
|
|
)
|
|
|
|
# Act
|
|
result: EpcPropertyData = apply_simulations(baseline, [simulation])
|
|
|
|
# Assert — every field routed onto sap_energy_source.
|
|
source = result.sap_energy_source
|
|
assert source.photovoltaic_arrays is not None
|
|
assert [a.peak_power for a in source.photovoltaic_arrays] == [4.8, 1.2]
|
|
assert [a.orientation for a in source.photovoltaic_arrays] == [5, 6]
|
|
assert [a.overshading for a in source.photovoltaic_arrays] == [1, 2]
|
|
assert source.pv_diverter_present is True
|
|
assert source.pv_connection == 1
|
|
assert source.is_dwelling_export_capable is True
|
|
assert source.pv_batteries is not None
|
|
assert abs(source.pv_batteries.pv_battery.battery_capacity - 5.0) <= 1e-9
|
|
|
|
|
|
def test_solar_overlay_leaves_diverter_unset_when_omitted() -> None:
|
|
# Arrange — a combi dwelling gets PV without a diverter (nothing to divert
|
|
# to); the omitted field leaves the baseline False unchanged.
|
|
baseline: EpcPropertyData = build_epc()
|
|
simulation = EpcSimulation(
|
|
solar=SolarOverlay(
|
|
photovoltaic_arrays=[
|
|
PhotovoltaicArray(peak_power=3.2, pitch=2, orientation=5, overshading=1)
|
|
],
|
|
is_dwelling_export_capable=True,
|
|
)
|
|
)
|
|
|
|
# Act
|
|
result: EpcPropertyData = apply_simulations(baseline, [simulation])
|
|
|
|
# Assert — diverter untouched (still False), export flipped True.
|
|
assert result.sap_energy_source.pv_diverter_present is False
|
|
assert result.sap_energy_source.is_dwelling_export_capable is True
|
|
|
|
|
|
def test_baseline_energy_source_is_not_mutated_by_a_solar_overlay() -> None:
|
|
# Arrange
|
|
baseline: EpcPropertyData = build_epc()
|
|
original_export = baseline.sap_energy_source.is_dwelling_export_capable
|
|
original_arrays = baseline.sap_energy_source.photovoltaic_arrays
|
|
|
|
# Act
|
|
_: EpcPropertyData = apply_simulations(
|
|
baseline,
|
|
[
|
|
EpcSimulation(
|
|
solar=SolarOverlay(
|
|
photovoltaic_arrays=[
|
|
PhotovoltaicArray(
|
|
peak_power=3.2, pitch=2, orientation=5, overshading=1
|
|
)
|
|
],
|
|
is_dwelling_export_capable=True,
|
|
)
|
|
)
|
|
],
|
|
)
|
|
|
|
# Assert — the baseline's energy source is untouched.
|
|
assert baseline.sap_energy_source.is_dwelling_export_capable == original_export
|
|
assert baseline.sap_energy_source.photovoltaic_arrays == original_arrays
|
|
|
|
|
|
def test_baseline_lighting_is_not_mutated_by_a_lighting_overlay() -> None:
|
|
# Arrange — 000490 lodges 8 low-energy-unknown bulbs, 0 LED.
|
|
baseline: EpcPropertyData = build_epc()
|
|
original_led: int = baseline.led_fixed_lighting_bulbs_count
|
|
original_lel: int | None = baseline.low_energy_fixed_lighting_bulbs_count
|
|
|
|
# Act — fold an all-LED overlay (led = the 8 total).
|
|
_: EpcPropertyData = apply_simulations(
|
|
baseline,
|
|
[
|
|
EpcSimulation(
|
|
lighting=LightingOverlay(
|
|
led_fixed_lighting_bulbs_count=8,
|
|
low_energy_fixed_lighting_bulbs_count=0,
|
|
)
|
|
)
|
|
],
|
|
)
|
|
|
|
# Assert — the baseline's counts are untouched.
|
|
assert baseline.led_fixed_lighting_bulbs_count == original_led
|
|
assert baseline.low_energy_fixed_lighting_bulbs_count == original_lel
|