"""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