mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
feat(modelling): SecondaryHeatingOverlay clears the lodged secondary (ADR-0028)
The first overlay surface that sets fields to *absent* rather than to a target state: _fold_secondary_heating clears sap_heating.secondary_heating_type + secondary_fuel_type, so the calculator's Table 11 secondary-fraction split (SAP 10.2 §9a) routes 100% of space heating to the main. On an electric-storage main RdSAP §A.2.2 re-forces a default secondary, making removal a no-op there — left to the Optimiser to de-select (ADR-0028 decisions 2-3). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
6ce6e89de1
commit
9b286e4a22
3 changed files with 70 additions and 0 deletions
|
|
@ -21,6 +21,7 @@ from domain.modelling.simulation import (
|
|||
EpcSimulation,
|
||||
HeatingOverlay,
|
||||
LightingOverlay,
|
||||
SecondaryHeatingOverlay,
|
||||
SolarOverlay,
|
||||
VentilationOverlay,
|
||||
WindowOverlay,
|
||||
|
|
@ -54,12 +55,29 @@ def apply_simulations(
|
|||
_fold_lighting(result, simulation.lighting)
|
||||
if simulation.heating is not None:
|
||||
_fold_heating(result, simulation.heating)
|
||||
if simulation.secondary_heating is not None:
|
||||
_fold_secondary_heating(result, simulation.secondary_heating)
|
||||
if simulation.solar is not None:
|
||||
_fold_solar(result, simulation.solar)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _fold_secondary_heating(
|
||||
epc: EpcPropertyData, overlay: SecondaryHeatingOverlay
|
||||
) -> None:
|
||||
"""Strip the dwelling's lodged secondary heating system (ADR-0028) — the one
|
||||
fold that sets fields to *absent* rather than to a target state. Clears
|
||||
`secondary_heating_type` + `secondary_fuel_type` on `sap_heating`, so the
|
||||
calculator's Table 11 split routes 100% of space heating to the main (or, on
|
||||
an electric-storage main, re-forces the §A.2.2 default — a no-op the
|
||||
Optimiser de-selects)."""
|
||||
if not overlay.remove:
|
||||
return
|
||||
epc.sap_heating.secondary_heating_type = None
|
||||
epc.sap_heating.secondary_fuel_type = None
|
||||
|
||||
|
||||
# `HeatingOverlay` fields grouped by the object they target — the deepest fold,
|
||||
# spanning the primary `MainHeatingDetail`, `sap_heating`, the top-level
|
||||
# `EpcPropertyData`, and `sap_energy_source` (ADR-0024).
|
||||
|
|
|
|||
|
|
@ -140,6 +140,24 @@ class HeatingOverlay:
|
|||
mains_gas: Optional[bool] = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SecondaryHeatingOverlay:
|
||||
"""The change the Secondary Heating Removal Measure makes (ADR-0028): strip
|
||||
the dwelling's lodged secondary heating system so the main serves 100% of
|
||||
space heating. Unlike every other overlay — which writes a *target state*
|
||||
and treats ``None`` as "leave unchanged" — this overlay *clears* the
|
||||
secondary fields (`secondary_heating_type`, `secondary_fuel_type`) to
|
||||
absent. Its presence on an `EpcSimulation` is the signal; `remove` carries
|
||||
the intent explicitly.
|
||||
|
||||
On an electric-storage main RdSAP §A.2.2 forces a default secondary back, so
|
||||
removal is a no-op there — the Optimiser de-selects those (it owns the
|
||||
economics); eligibility still offers them.
|
||||
"""
|
||||
|
||||
remove: bool = True
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SolarOverlay:
|
||||
"""All-optional partial of the dwelling's PV-bearing energy source — the
|
||||
|
|
@ -194,4 +212,5 @@ class EpcSimulation:
|
|||
ventilation: Optional[VentilationOverlay] = None
|
||||
lighting: Optional[LightingOverlay] = None
|
||||
heating: Optional[HeatingOverlay] = None
|
||||
secondary_heating: Optional[SecondaryHeatingOverlay] = None
|
||||
solar: Optional[SolarOverlay] = None
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ from domain.modelling.simulation import (
|
|||
EpcSimulation,
|
||||
HeatingOverlay,
|
||||
LightingOverlay,
|
||||
SecondaryHeatingOverlay,
|
||||
SolarOverlay,
|
||||
VentilationOverlay,
|
||||
WindowOverlay,
|
||||
|
|
@ -292,6 +293,38 @@ def test_apply_folds_a_heating_overlay_across_all_five_locations() -> None:
|
|||
assert result.sap_energy_source.mains_gas is False
|
||||
|
||||
|
||||
def test_secondary_heating_overlay_clears_the_lodged_secondary() -> None:
|
||||
# Arrange — 000490 lodges a secondary system (SAP code 691, electric panel/
|
||||
# convector/radiant heaters). Pin a fuel on it too so we prove the fold
|
||||
# clears BOTH the type and the fuel (ADR-0028).
|
||||
baseline: EpcPropertyData = build_epc()
|
||||
baseline.sap_heating.secondary_fuel_type = 30
|
||||
assert baseline.sap_heating.secondary_heating_type == 691
|
||||
|
||||
# Act — fold a removal overlay.
|
||||
result: EpcPropertyData = apply_simulations(
|
||||
baseline, [EpcSimulation(secondary_heating=SecondaryHeatingOverlay())]
|
||||
)
|
||||
|
||||
# Assert — the secondary is gone from the dwelling handed to the calculator.
|
||||
assert result.sap_heating.secondary_heating_type is None
|
||||
assert result.sap_heating.secondary_fuel_type is None
|
||||
|
||||
|
||||
def test_secondary_heating_removal_does_not_mutate_the_baseline() -> None:
|
||||
# Arrange — 000490 lodges secondary SAP code 691.
|
||||
baseline: EpcPropertyData = build_epc()
|
||||
assert baseline.sap_heating.secondary_heating_type == 691
|
||||
|
||||
# Act — fold a removal overlay.
|
||||
_: EpcPropertyData = apply_simulations(
|
||||
baseline, [EpcSimulation(secondary_heating=SecondaryHeatingOverlay())]
|
||||
)
|
||||
|
||||
# Assert — the baseline's secondary is untouched (the fold copies first).
|
||||
assert baseline.sap_heating.secondary_heating_type == 691
|
||||
|
||||
|
||||
def test_baseline_heating_is_not_mutated_by_a_heating_overlay() -> None:
|
||||
# Arrange — 000490 lodges a mains-gas combi (fuel 26, control 2106, no
|
||||
# cylinder, mains_gas True).
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue