Model/domain/modelling/overlay_applicator.py
Khalim Conn-Kowlessar 7c59e9198a feat(modelling): Simulation Overlay grows a dwelling ventilation segment (#1161)
VentilationOverlay (all-optional partial of SapVentilation) + EpcSimulation.
ventilation; apply_simulations folds it onto sap_ventilation, creating one when
the baseline lodged none. This is the surface a Measure Dependency (ventilation)
writes — whole-dwelling, no building part.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 13:20:45 +00:00

55 lines
2.3 KiB
Python

"""The Overlay Applicator — folds an ordered set of Simulation Overlays onto
a baseline EpcPropertyData and returns a new one for the calculator.
Sequential fold: overlays are applied in order and a later overlay wins on a
field it shares with an earlier one. The baseline is never mutated; the
returned EpcPropertyData is throwaway (handed to the calculator for scoring,
then discarded). See ADR-0016.
"""
import copy
from dataclasses import fields
from typing import Optional, Sequence
from datatypes.epc.domain.epc_property_data import EpcPropertyData, SapVentilation
from domain.modelling.simulation import EpcSimulation, VentilationOverlay
def apply_simulations(
baseline: EpcPropertyData, simulations: Sequence[EpcSimulation]
) -> EpcPropertyData:
"""Return a copy of ``baseline`` with every Simulation Overlay's non-``None``
fields written onto the building part it targets, applied in order. A
whole-dwelling ``ventilation`` overlay folds onto ``sap_ventilation``
(creating one if the baseline lodged none)."""
result: EpcPropertyData = copy.deepcopy(baseline)
parts_by_id = {part.identifier: part for part in result.sap_building_parts}
for simulation in simulations:
for identifier, overlay in simulation.building_parts.items():
part = parts_by_id[identifier]
for overlay_field in fields(overlay):
value = getattr(overlay, overlay_field.name)
if value is not None:
setattr(part, overlay_field.name, value)
if simulation.ventilation is not None:
result.sap_ventilation = _fold_ventilation(
result.sap_ventilation, simulation.ventilation
)
return result
def _fold_ventilation(
baseline: Optional[SapVentilation], overlay: VentilationOverlay
) -> SapVentilation:
"""Write the overlay's non-``None`` fields onto a (copied) ``SapVentilation``,
starting a fresh one when the baseline lodged none."""
folded: SapVentilation = (
copy.deepcopy(baseline) if baseline is not None else SapVentilation()
)
for overlay_field in fields(overlay):
value = getattr(overlay, overlay_field.name)
if value is not None:
setattr(folded, overlay_field.name, value)
return folded