from typing import Dict, Union, Optional from epc_data.attributes.attribute_utils import extract_thermal_transmittence, search_split_description class RoofAttributes: def __init__(self, description): """ :param description: Description of the roof. """ self.description: str = description def clean(self) -> Dict[str, Union[str, bool, int, None]]: """ We aim to extract features about the roof, so we can characterise it. We will check: - If the roof is pitched - If there is a room roof - if there is a loft - If it has insulation - if so, what degree of insulation :return: Dictionary of attributes of the roof. """ description_lower = self.description.lower().strip() if "another dwelling above" in description_lower or "other premises above" in description_lower: return self._make_clean_output( is_valid="invalid" not in description_lower, at_rafters="at rafters" in description_lower, is_pitched=False, is_roof_room=False, has_loft=False, insulation_thickness=0, has_dwelling_above=True, assumed="assumed" in description_lower, is_flat="flat" in description_lower, is_thatched=False, thermal_transmittence=None, thermal_transmittence_unit=None ) is_pitched = "pitched" in description_lower is_roof_room = "roof room" in description_lower has_loft = "loft" in description_lower is_flat = "flat" in description_lower is_thatched = "thatched" in description_lower at_rafters = "at rafters" in description_lower thermal_transmittence, thermal_transmittence_unit, insulation_thickness = None, None, None if "insulation" in description_lower or "insulated" in description_lower: insulation_thickness = self._find_insulation_thickness(description_lower, is_pitched, is_roof_room, is_flat) elif "thermal transmittance" in description_lower: thermal_transmittence, thermal_transmittence_unit = extract_thermal_transmittence(description_lower) elif is_thatched: # Search for these features: thermal_transmittence, thermal_transmittence_unit = extract_thermal_transmittence(description_lower) insulation_thickness = self._find_insulation_thickness( description_lower, is_pitched, is_roof_room, is_flat ) elif description_lower == "pitched": thermal_transmittence, thermal_transmittence_unit, insulation_thickness = None, None, None else: raise NotImplementedError("Not handled this") return self._make_clean_output( is_valid="invalid" not in description_lower, at_rafters=at_rafters, is_pitched=is_pitched, is_roof_room=is_roof_room, has_loft=has_loft, insulation_thickness=insulation_thickness, has_dwelling_above=False, assumed="assumed" in description_lower, is_flat=is_flat, is_thatched=is_thatched, thermal_transmittence=thermal_transmittence, thermal_transmittence_unit=thermal_transmittence_unit ) @staticmethod def _make_clean_output( is_valid: bool, at_rafters: bool, is_pitched: bool, is_roof_room: bool, has_loft: bool, insulation_thickness: str | int | None, has_dwelling_above: bool, assumed: bool, is_flat: bool, is_thatched: bool, thermal_transmittence: Optional[float], thermal_transmittence_unit: Optional[str] ) -> Dict[str, Union[bool, str, None]]: """ Utility function to ensure all the keys are present in the output. :param is_valid: True if the roof descrption is valid, False otherwise :param at_rafters: True if the insulation is at the rafters, False otherwise :param is_pitched: True if the roof is pitched, False otherwise :param is_roof_room: True if there is a room in the roof, False otherwise :param has_loft: True if there is a loft, False otherwise :param insulation_thickness: The thickness of the insulation :param has_dwelling_above: True if there is a dwelling above, False otherwise :param assumed: True if the roof type was assumed based on property age, False otherwise :param is_flat: True if the roof is flat, False otherwise :param is_thatched: True if the roof is thatched, False otherwise :param thermal_transmittence: The thermal transmittence value of the roof, if known :param thermal_transmittence_unit: The unit of thermal transmittence, if known :return: A dictionary containing all the information about the roof. """ return { "is_valid": is_valid, "at_rafters": at_rafters, "is_pitched": is_pitched, "is_roof_room": is_roof_room, "has_loft": has_loft, "insulation_thickness": insulation_thickness, "has_dwelling_above": has_dwelling_above, "assumed": assumed, "is_flat": is_flat, "is_thatched": is_thatched, "thermal_transmittence": thermal_transmittence, "thermal_transmittence_unit": thermal_transmittence_unit } @staticmethod def _find_insulation_thickness( description_lower: str, is_pitched: bool, is_roof_room: bool, is_flat: bool ) -> Union[int, str, None]: """ Finds insulation thickness in the description. :param description_lower: Lowercase description. :param is_pitched: Whether the roof is pitched. :param is_roof_room: Whether there is a room in the roof. :param is_flat: Whether the roof is flat. :return: Insulation thickness if found, else None. """ if "no insulation" in description_lower: return 0 if is_pitched: try: thickness = description_lower.split("pitched,")[-1].split("mm")[0].strip() if "+" in thickness: return thickness try: return int(thickness) except ValueError as int_error: raise ValueError(int_error) except ValueError as _: if "invalid input" in description_lower: return None desc = description_lower.split("pitched,")[-1].strip().split(" ")[0] return search_split_description(desc) if is_roof_room: desc_split_lookup = { "ceiling insulated": "average", "thatched": "average", } # Just search for specific phrases desc_split = description_lower.split("roof room(s),")[-1].strip() res = desc_split_lookup.get(desc_split) if res: return res desc = desc_split.split(" ")[0] return search_split_description(desc) if is_flat: # Just search for specific phrases desc = description_lower.split("flat,")[-1].lstrip().split(" ")[0] return search_split_description(desc) return None