"""The floor Recommendation Generator. Detects an uninsulated ground floor and its construction (suspended timber vs solid) and emits a Recommendation whose single Measure Option carries the matching insulation Simulation Overlay and a priced Cost. A floor is one construction, so — like a cavity wall — there is one Option, chosen by detection. No scoring, no persistence (ADR-0016). """ from typing import Optional, Union from datatypes.epc.domain.epc_property_data import ( BuildingPartIdentifier, EpcPropertyData, SapBuildingPart, ) from domain.building_geometry import ground_floor_area from domain.modelling.measure_type import MeasureType from domain.modelling.recommendation import Cost, MeasureOption, Recommendation from domain.modelling.simulation import BuildingPartOverlay, EpcSimulation from repositories.product.product_repository import ProductRepository # Recommended ground-floor insulation depth (mm). _RECOMMENDED_FLOOR_THICKNESS_MM = 100 # Insulating an as-built floor re-lodges its insulation as retro-fitted. The # calculator keys on this for a suspended timber floor's sealed/unsealed # determination (cert_to_inputs: "retro" + no U-value → sealed), so the # overlay must set it or the suspended-floor cascade leaves a ~1.4 SAP gap # (see test_elmhurst_cascade_pins). _RETROFITTED_INSULATION = "Retro-fitted" def _is_uninsulated(thickness: Optional[Union[str, int]]) -> bool: """A lodged floor-insulation thickness of nothing / blank / zero is an uninsulated floor; any positive thickness is already insulated.""" if thickness is None: return True if isinstance(thickness, int): return thickness == 0 return thickness.strip() in ("", "0") def _floor_measure_type(construction_type: Optional[str]) -> Optional[MeasureType]: """Map the lodged floor construction to the insulation Measure Type, or None when the construction is not a treatable suspended/solid floor.""" text = (construction_type or "").lower() if "suspended" in text: return MeasureType.SUSPENDED_FLOOR_INSULATION if "solid" in text: return MeasureType.SOLID_FLOOR_INSULATION return None def recommend_floor_insulation( epc: EpcPropertyData, products: ProductRepository ) -> Optional[Recommendation]: """Return a ground-floor insulation Recommendation for the main part's uninsulated ground floor, else None.""" main: SapBuildingPart = next( part for part in epc.sap_building_parts if part.identifier is BuildingPartIdentifier.MAIN ) if not _is_uninsulated(main.floor_insulation_thickness): return None measure_type = _floor_measure_type(main.floor_construction_type) if measure_type is None: return None product = products.get(measure_type) area: float = ground_floor_area(epc, BuildingPartIdentifier.MAIN) cost = Cost( total=area * product.unit_cost_per_m2, contingency_rate=product.contingency_rate, ) option = MeasureOption( measure_type=measure_type, description="Ground-floor insulation", overlay=EpcSimulation( building_parts={ BuildingPartIdentifier.MAIN: BuildingPartOverlay( floor_insulation_thickness=_RECOMMENDED_FLOOR_THICKNESS_MM, floor_insulation_type_str=_RETROFITTED_INSULATION, ) } ), cost=cost, material_id=product.id, ) return Recommendation(surface="Ground floor", options=(option,))