mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice 1 of the lighting generator (ADR-0023): the first whole-dwelling, top-level overlay surface. LightingOverlay carries the four fixed-lighting bulb-count fields by their exact EPC names (all Optional, absolute counts) + EpcSimulation.lighting. The applicator's _fold_lighting writes the non-None counts directly onto the result EpcPropertyData by name (setattr) — simpler than ventilation's nested fold since the counts live top-level. Baseline unmutated; pyright strict clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
108 lines
4.4 KiB
Python
108 lines
4.4 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,
|
|
SapWindow,
|
|
WindowTransmissionDetails,
|
|
)
|
|
from domain.modelling.simulation import (
|
|
EpcSimulation,
|
|
LightingOverlay,
|
|
VentilationOverlay,
|
|
WindowOverlay,
|
|
)
|
|
|
|
|
|
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)
|
|
for index, window_overlay in simulation.windows.items():
|
|
_fold_window(result.sap_windows[index], window_overlay)
|
|
if simulation.ventilation is not None:
|
|
result.sap_ventilation = _fold_ventilation(
|
|
result.sap_ventilation, simulation.ventilation
|
|
)
|
|
if simulation.lighting is not None:
|
|
_fold_lighting(result, simulation.lighting)
|
|
|
|
return result
|
|
|
|
|
|
def _fold_lighting(epc: EpcPropertyData, overlay: LightingOverlay) -> None:
|
|
"""Write a `LightingOverlay`'s non-``None`` bulb counts onto the (copied)
|
|
dwelling's top-level fields by name — the four counts live directly on
|
|
`EpcPropertyData`, so the fold writes onto it, not a nested object."""
|
|
for overlay_field in fields(overlay):
|
|
value = getattr(overlay, overlay_field.name)
|
|
if value is not None:
|
|
setattr(epc, overlay_field.name, value)
|
|
|
|
|
|
def _fold_window(window: SapWindow, overlay: WindowOverlay) -> None:
|
|
"""Write a `WindowOverlay`'s non-``None`` fields onto a (copied) window:
|
|
``glazing_type`` flat on the window, ``u_value`` / ``solar_transmittance``
|
|
into its `WindowTransmissionDetails` (where the cascade reads them), starting
|
|
a fresh one when the window lodged none."""
|
|
if overlay.glazing_type is not None:
|
|
window.glazing_type = overlay.glazing_type
|
|
if overlay.u_value is None and overlay.solar_transmittance is None:
|
|
return
|
|
details: Optional[WindowTransmissionDetails] = window.window_transmission_details
|
|
if details is None:
|
|
# data_source 1 = manufacturer-lodged (the case the cascade's per-window
|
|
# U path keys on); both values must be present to start fresh.
|
|
window.window_transmission_details = WindowTransmissionDetails(
|
|
u_value=overlay.u_value if overlay.u_value is not None else 0.0,
|
|
data_source=1,
|
|
solar_transmittance=(
|
|
overlay.solar_transmittance
|
|
if overlay.solar_transmittance is not None
|
|
else 0.0
|
|
),
|
|
)
|
|
return
|
|
if overlay.u_value is not None:
|
|
details.u_value = overlay.u_value
|
|
if overlay.solar_transmittance is not None:
|
|
details.solar_transmittance = overlay.solar_transmittance
|
|
|
|
|
|
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
|