Model/domain/modelling/simulation.py
Khalim Conn-Kowlessar 350f4c8e76 feat(modelling): Overlay Applicator folds EpcSimulation onto EpcPropertyData
EpcSimulation is the Simulation Overlay — a narrow all-optional partial
mirror of EpcPropertyData/SapBuildingPart (wall surface first), targeting
building parts by BuildingPartIdentifier (composition, not inheritance).
apply_simulations(baseline, simulations) deep-copies the baseline, folds
overlays in order (later wins on a shared field) via a generic non-None
field write, and returns a throwaway EpcPropertyData for the calculator;
the baseline is never mutated.

Four behaviour tests (hand-built EPD from the 000490 fixture, no PDF):
targeted-write-leaves-others-untouched, empty-overlay no-op, sequential
last-wins, baseline-immutability. pyright strict clean.

Slice 1 of the Modelling stage rebuild (ADR-0016). Closes #1153.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 22:13:51 +00:00

38 lines
1.3 KiB
Python

"""The Simulation Overlay (`EpcSimulation`) — the change a single Measure
Option makes to a Property's EpcPropertyData.
An all-optional partial mirror of EpcPropertyData / SapBuildingPart, covering
the retrofit-relevant surface only (wall fields first). It is *not* an
EpcPropertyData — composition, not inheritance — and carries no scores.
Building parts are targeted by `BuildingPartIdentifier` so a measure addresses
the exact `SapBuildingPart` (the main wall vs an extension). See CONTEXT.md.
"""
from dataclasses import dataclass, field
from typing import Mapping, Optional
from datatypes.epc.domain.epc_property_data import BuildingPartIdentifier
@dataclass(frozen=True)
class BuildingPartOverlay:
"""All-optional partial of `SapBuildingPart` (wall surface first).
A `None` field means "leave the baseline value unchanged".
"""
wall_insulation_type: Optional[int] = None
def _no_building_parts() -> dict[BuildingPartIdentifier, BuildingPartOverlay]:
return {}
@dataclass(frozen=True)
class EpcSimulation:
"""A Simulation Overlay: the per-building-part changes a Measure Option
makes, keyed by `BuildingPartIdentifier`."""
building_parts: Mapping[BuildingPartIdentifier, BuildingPartOverlay] = field(
default_factory=_no_building_parts
)