mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Fitting sealed glazing units changes two things beyond the pane's U/g
that the cascade reads, which the overlay didn't model — leaving the
double/secondary before→after pins ~0.7 SAP short (xfail):
1. Draught-proofing (RdSAP 10 §8.1). Sealed units draught-proof the panes
they replace, re-lodging the dwelling-level `percent_draughtproofed`
(cert 001431: 84 → 100). The §2 cascade reads that dwelling-level
value, so the overlay now carries it. `_recompute_percent_draughtproofed`
anchors on the lodged before-% — `after = round((round(before%/100 × N)
+ flips) / N × 100)`, N = openable windows (vertical + roof) + doors,
flips = upgraded panes that were not draught-proofed — so it's robust
to incomplete window extraction (unchanged openings are already in the
aggregate). ~0.3 SAP.
2. Frame factor (§6 solar gains). A replacement unit re-lodges its own
FF=0.70, overriding the pane it replaced — the two "single glazing,
known data" panes lodge FF 1.00 / 0.50 (one is 6.6 m²), so leaving them
unchanged understated solar gains by ~+150 kWh space heating. `WindowOverlay`
now carries `frame_factor`, written flat onto the window. ~0.4 SAP.
Wiring: `EpcSimulation.percent_draughtproofed` + `WindowOverlay.frame_factor`
new fields; `apply_simulations` / `_fold_window` write them; the glazing
generator computes both from the upgraded set and cert 001431's after.
Un-xfails `test_{double,secondary}_glazing_overlay_reproduces_the_relodged_after`
— both now pin SAP/CO2/PE to the relodged after within tolerance. Updates
the two `test_glazing_recommendation` overlay expectations for the new
`frame_factor`. 96 modelling tests pass; zero new pyright errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
116 lines
4.9 KiB
Python
116 lines
4.9 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
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class WindowOverlay:
|
|
"""All-optional partial of one `SapWindow` — the change a glazing Measure
|
|
makes to a single window (ADR-0022).
|
|
|
|
`glazing_type` is the SAP10.2 Table U2 code (drives only the §5 daylight
|
|
factor when a per-window U is lodged). `u_value` and `solar_transmittance`
|
|
are written into the window's `WindowTransmissionDetails` — where the
|
|
calculator reads heat loss and solar gain from — because our calculator
|
|
consumes the lodged values directly rather than deriving them from
|
|
`glazing_type`. `frame_factor` is written flat on the window (the §6
|
|
solar-gain area factor); a replacement unit re-lodges its own FF, which
|
|
can differ from the pane it replaced. A `None` field means "leave the
|
|
baseline value unchanged".
|
|
"""
|
|
|
|
glazing_type: Optional[int] = None
|
|
u_value: Optional[float] = None
|
|
solar_transmittance: Optional[float] = None
|
|
frame_factor: Optional[float] = None
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class LightingOverlay:
|
|
"""All-optional partial of the dwelling's fixed-lighting bulb counts — the
|
|
whole-dwelling lighting change a Measure Option makes (e.g. an all-LED
|
|
upgrade — ADR-0023). Unlike a `BuildingPartOverlay` or `WindowOverlay` this
|
|
targets no building part or window; its fields are the four **top-level**
|
|
`EpcPropertyData` bulb counts, folded directly by name.
|
|
|
|
The counts are absolute target states, not deltas (an all-LED upgrade sets
|
|
``led = total``, the rest 0). A `None` field means "leave the baseline value
|
|
unchanged".
|
|
"""
|
|
|
|
led_fixed_lighting_bulbs_count: Optional[int] = None
|
|
cfl_fixed_lighting_bulbs_count: Optional[int] = None
|
|
incandescent_fixed_lighting_bulbs_count: Optional[int] = None
|
|
low_energy_fixed_lighting_bulbs_count: Optional[int] = None
|
|
|
|
|
|
def _no_building_parts() -> dict[BuildingPartIdentifier, BuildingPartOverlay]:
|
|
return {}
|
|
|
|
|
|
def _no_windows() -> dict[int, WindowOverlay]:
|
|
return {}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class EpcSimulation:
|
|
"""A Simulation Overlay: the per-building-part changes a Measure Option
|
|
makes, keyed by `BuildingPartIdentifier`; per-window changes keyed by the
|
|
`sap_windows` index; plus an optional whole-dwelling `ventilation` change
|
|
(the Measure Dependency surface — ADR-0016)."""
|
|
|
|
building_parts: Mapping[BuildingPartIdentifier, BuildingPartOverlay] = field(
|
|
default_factory=_no_building_parts
|
|
)
|
|
windows: Mapping[int, WindowOverlay] = field(default_factory=_no_windows)
|
|
ventilation: Optional[VentilationOverlay] = None
|
|
lighting: Optional[LightingOverlay] = None
|
|
# Whole-dwelling RdSAP 10 §8.1 draught-proofing percentage, when a
|
|
# Measure changes it (e.g. glazing: sealed units draught-proof the
|
|
# panes they replace). The §2 cascade reads this dwelling-level value,
|
|
# so the overlay sets it directly. `None` leaves the baseline's value.
|
|
percent_draughtproofed: Optional[int] = None
|