mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-19 17:03:02 +00:00
Floor insulation is withheld when it cannot lower the derived floor U 🟩
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
128981289a
commit
fb3516ee95
2 changed files with 87 additions and 0 deletions
|
|
@ -15,6 +15,7 @@ from datatypes.epc.domain.epc_property_data import (
|
||||||
SapBuildingPart,
|
SapBuildingPart,
|
||||||
)
|
)
|
||||||
from domain.building_geometry import ground_floor_area
|
from domain.building_geometry import ground_floor_area
|
||||||
|
from domain.modelling.generators.floor_u_value_gate import floor_insulation_lowers_u
|
||||||
from domain.modelling.measure_type import MeasureType
|
from domain.modelling.measure_type import MeasureType
|
||||||
from domain.modelling.recommendation import Cost, MeasureOption, Recommendation
|
from domain.modelling.recommendation import Cost, MeasureOption, Recommendation
|
||||||
from domain.modelling.simulation import BuildingPartOverlay, EpcSimulation
|
from domain.modelling.simulation import BuildingPartOverlay, EpcSimulation
|
||||||
|
|
@ -69,6 +70,16 @@ def recommend_floor_insulation(
|
||||||
if measure_type is None:
|
if measure_type is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Floor U-Value Gate (ADR-0051, floor arm): withhold the Option when it
|
||||||
|
# cannot lower the floor's derived U — e.g. an at-regs modern age band,
|
||||||
|
# where the as-built and insulated cascades resolve to the same value.
|
||||||
|
if not floor_insulation_lowers_u(
|
||||||
|
main,
|
||||||
|
epc.country_code,
|
||||||
|
insulation_thickness_mm=_RECOMMENDED_FLOOR_THICKNESS_MM,
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
|
||||||
product = products.get(measure_type)
|
product = products.get(measure_type)
|
||||||
area: float = ground_floor_area(epc, BuildingPartIdentifier.MAIN)
|
area: float = ground_floor_area(epc, BuildingPartIdentifier.MAIN)
|
||||||
cost = Cost(
|
cost = Cost(
|
||||||
|
|
|
||||||
76
domain/modelling/generators/floor_u_value_gate.py
Normal file
76
domain/modelling/generators/floor_u_value_gate.py
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
"""The Floor U-Value Gate — the ground-floor arm of ADR-0051.
|
||||||
|
|
||||||
|
A floor insulation Option is offered only when it would actually lower the
|
||||||
|
ground floor's derived U-value: on a modern construction age band RdSAP's
|
||||||
|
BS EN ISO 13370 / Table 19 cascade assigns an as-built floor its at-regs
|
||||||
|
U-value and the post-measure state resolves to the same value — the measure
|
||||||
|
costs thousands for +0.00 SAP (portfolio 796: solid_floor_insulation offered
|
||||||
|
at £3-4k on age-band-L dwellings 709782/709790).
|
||||||
|
|
||||||
|
Same thin-mapping philosophy as the Wall U-Value Gate (`wall_u_value_gate`):
|
||||||
|
the calculator's rich extras (surveyor description, lodged `floor_u_value`)
|
||||||
|
are deliberately omitted — both sides are computed identically, so a genuine
|
||||||
|
no-op still compares equal, and omissions only ever bias toward OFFERING.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any, Final, Optional
|
||||||
|
|
||||||
|
from datatypes.epc.domain.epc_property_data import SapBuildingPart
|
||||||
|
from domain.sap10_ml.rdsap_uvalues import Country, u_floor
|
||||||
|
|
||||||
|
# Improvements below this are float noise, not a physical change. NOT a
|
||||||
|
# materiality threshold — economics stay with the Optimiser (ADR-0016/0024).
|
||||||
|
_NOISE_FLOOR_W_PER_M2K: Final[float] = 0.01
|
||||||
|
|
||||||
|
|
||||||
|
def _int_or_none(value: Any) -> Optional[int]:
|
||||||
|
return value if isinstance(value, int) else None
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_thickness_mm(value: Any) -> Optional[int]:
|
||||||
|
if value is None or isinstance(value, int):
|
||||||
|
return value
|
||||||
|
if isinstance(value, str) and value.strip().isdigit():
|
||||||
|
return int(value.strip())
|
||||||
|
return None # "NI" / free text — thickness not known
|
||||||
|
|
||||||
|
|
||||||
|
def floor_insulation_lowers_u(
|
||||||
|
main: SapBuildingPart,
|
||||||
|
country_code: Optional[str],
|
||||||
|
*,
|
||||||
|
insulation_thickness_mm: int,
|
||||||
|
) -> bool:
|
||||||
|
"""Whether insulating the main part's ground floor to
|
||||||
|
``insulation_thickness_mm`` lowers its derived U-value by more than float
|
||||||
|
noise — the Floor U-Value Gate (ADR-0051)."""
|
||||||
|
country: Country = Country.from_code(country_code)
|
||||||
|
age_band = main.construction_age_band
|
||||||
|
ground = next(
|
||||||
|
(fd for fd in main.sap_floor_dimensions if fd.floor == 0),
|
||||||
|
main.sap_floor_dimensions[0] if main.sap_floor_dimensions else None,
|
||||||
|
)
|
||||||
|
construction: Optional[int] = (
|
||||||
|
_int_or_none(ground.floor_construction) if ground is not None else None
|
||||||
|
)
|
||||||
|
area = ground.total_floor_area_m2 if ground is not None else None
|
||||||
|
perimeter = ground.heat_loss_perimeter_m if ground is not None else None
|
||||||
|
current_u: float = u_floor(
|
||||||
|
country,
|
||||||
|
age_band,
|
||||||
|
construction,
|
||||||
|
_parse_thickness_mm(main.floor_insulation_thickness),
|
||||||
|
area,
|
||||||
|
perimeter,
|
||||||
|
main.wall_thickness_mm,
|
||||||
|
)
|
||||||
|
post_u: float = u_floor(
|
||||||
|
country,
|
||||||
|
age_band,
|
||||||
|
construction,
|
||||||
|
insulation_thickness_mm,
|
||||||
|
area,
|
||||||
|
perimeter,
|
||||||
|
main.wall_thickness_mm,
|
||||||
|
)
|
||||||
|
return current_u - post_u > _NOISE_FLOOR_W_PER_M2K
|
||||||
Loading…
Add table
Reference in a new issue