Model/domain/modelling/simulation.py
Khalim Conn-Kowlessar 68aa80c174 feat(modelling): overlay models solid-wall insulation (IWI/EWI), pinned
Slice 1 of solid-wall insulation. BuildingPartOverlay gains a
wall_insulation_thickness field; the generic applicator already folds it onto
SapBuildingPart by name. With wall_insulation_type=1 (EWI) / 3 (IWI) + 100 mm,
the calculator derives the post-insulation U-value (§5.8 documentary path,
λ=0.04 default) — and for IWI also lowers the thermal-mass parameter. Two new
Elmhurst before/after cascade pins (solid-brick EWI + IWI, cert 001431)
reproduce the re-lodged after at abs(diff) <= 1e-4 across SAP/CO2/PE.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 15:11:26 +00:00

63 lines
2.5 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
# Added solid-wall insulation depth (mm) — drives the calculator's Table 6
# bucket / §5.8 documentary U-value for EWI (`wall_insulation_type=1`) and
# IWI (`wall_insulation_type=3`); λ defaults to 0.04 W/m·K in the calculator.
wall_insulation_thickness: Optional[int] = None
roof_insulation_thickness: Optional[int] = None
floor_insulation_thickness: Optional[int] = None
floor_insulation_type_str: Optional[str] = None
@dataclass(frozen=True)
class VentilationOverlay:
"""All-optional partial of `SapVentilation` — the whole-dwelling ventilation
change a Measure Option makes (e.g. retrofit MEV). Unlike a
`BuildingPartOverlay` this targets no building part; it folds onto the
dwelling's single `sap_ventilation`.
`mechanical_ventilation_kind` names the SAP10.2 §2 mechanical-ventilation
kind (the `MechanicalVentilationKind` enum name, e.g.
``"EXTRACT_OR_PIV_OUTSIDE"`` for decentralised MEV). A `None` field means
"leave the baseline value unchanged".
"""
mechanical_ventilation_kind: Optional[str] = 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`, plus an optional whole-dwelling
`ventilation` change (the Measure Dependency surface — ADR-0016)."""
building_parts: Mapping[BuildingPartIdentifier, BuildingPartOverlay] = field(
default_factory=_no_building_parts
)
ventilation: Optional[VentilationOverlay] = None