mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice 1 of the glazing generator (ADR-0022). `WindowOverlay` (all-optional partial of one SapWindow) + `EpcSimulation.windows` keyed by sap_windows index. The applicator folds it onto sap_windows[i]: glazing_type flat on the window, u_value/solar_transmittance routed into its WindowTransmissionDetails (created if absent) — the applicator's first nested write, because that's where the calculator reads window heat loss and solar gain. Baseline left unmutated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
221 lines
7 KiB
Python
221 lines
7 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,
|
|
SapBuildingPart,
|
|
SapVentilation,
|
|
)
|
|
from domain.modelling.simulation import (
|
|
BuildingPartOverlay,
|
|
EpcSimulation,
|
|
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_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
|
|
)
|