diff --git a/model_data/Property.py b/model_data/Property.py index df624130..757b4db2 100644 --- a/model_data/Property.py +++ b/model_data/Property.py @@ -32,6 +32,7 @@ class Property(BaseUtility): self.postcode = postcode self.address1 = address1 self.data = data + self.full_sap_epc = None if epc_client: self.epc_client = epc_client @@ -49,6 +50,10 @@ class Property(BaseUtility): # This will fail if a property does not have an EPC - this has been documented as a case to handle response = self.epc_client.domestic.search(params={"address": self.address1, "postcode": self.postcode}) + # Check if we have a full sap EPC + self.full_sap_epc = [r for r in response["rows"] if r["transaction-type"] == "new dwelling"] + self.full_sap_epc = self.full_sap_epc[0] if self.full_sap_epc else self.full_sap_epc + if len(response["rows"]) > 1: newest_response = [ r for r in response["rows"] if diff --git a/model_data/app.py b/model_data/app.py index c6968009..9f84c396 100644 --- a/model_data/app.py +++ b/model_data/app.py @@ -100,4 +100,27 @@ def handler(): # Now, given the components, we want to idenfity upgrade options import pandas as pd - walls_df = pd.DataFrame([p.walls for p in input_properties]) + walls_df = pd.DataFrame( + [{"address1": p.address1, **p.walls} for p in input_properties] + ) + + # Key values + # This is based on the standards set out by part L of the building regulations + WALLS_MAX_U_VALUE = 0.18 + + input_properties[1].data["address1"] + walls_df["address1"].values[1] + walls_df["original_description"].values[1] + # Walls + # Property 0 + # '28 Distillery Wharf', 'Average thermal transmittance 0.16 W/m-¦K' + # Because the insulation is already within the max threshold for new builds + # Also, I know that the property was built after 1990 so was built with insulation (let's get land registry data) + # It's likely not worth doing an insulation upgrade, however if anything, we would do internal wall insulation + # logic: + # if building built after 1990 + we're able to identify U-value + U-value less than 0.18 + # and if in or close to a conversation area, recommend internal wall insulation + # Property 1 + + from model_data.recommendations.WallRecommendations import WallRecommendations + self = WallRecommendations(property_instance=input_properties[0]) diff --git a/model_data/recommendations/WallRecommendations.py b/model_data/recommendations/WallRecommendations.py new file mode 100644 index 00000000..e4915716 --- /dev/null +++ b/model_data/recommendations/WallRecommendations.py @@ -0,0 +1,100 @@ +from model_data.Property import Property +import pandas as pd + +wall_parts = [ + { + "id": 1, + "type": "internal_wall_insulation", + "description": "Internal wall insulation", + "cost": None, + # The u-value here is just a placehoder for now and we probably want to have multiple + # options for internal wall insulation (e.g. 50mm, 100mm, 150mm), with different material types + # and costs + "u_value": 0.3 + }, + +] + + +class WallRecommendations: + YEAR_WALLS_BUILT_WITH_INSULATION = 1990 + U_VALUE_UNIT = 'w/m-¦k' + BUILDING_REGULATIONS_NEW_BUILD_L_MAX_U_VALUE = 0.18 + BUILDING_REGULATIONS_EXISTING_BUILD_L_MAX_U_VALUE = 0.3 + + def __init__(self, property_instance: Property): + self.property = property_instance + self.year_built = self._year_property_was_built() + + # Will contains a list of recommended measures + self.recommendations = [] + + def _year_property_was_built(self): + """ + Estimates when the property was built based on as much available data as possible. + + """ + + if self.property.full_sap_epc: + return pd.to_datetime(self.property.full_sap_epc["lodgement-date"]).year + + raise NotImplementedError("Implement me!") + + def recommend(self): + # if building built after 1990 + we're able to identify U-value + + # U-value less than 0.18 and if in or close to a conversation area, + # recommend internal wall insulation as a possible measure + + u_value = self.property.walls["thermal_transmittance"] + + is_cavity_wall = self.property.walls["is_cavity_wall"] + + if u_value: + if self.property.walls["thermal_transmittance_unit"] != self.U_VALUE_UNIT: + raise NotImplementedError("Haven't handled the case of other u value units yet") + + if (not is_cavity_wall) and (self.year_built >= self.YEAR_WALLS_BUILT_WITH_INSULATION) and ( + u_value >= self.BUILDING_REGULATIONS_EXISTING_BUILD_L_MAX_U_VALUE + ): + # Recommend internal wall insulation + iwi_parts = [part for part in wall_parts if part["type"] == "internal_wall_insulation"] + for part in iwi_parts: + _, new_u_value = self.calculate_u_value_uplift(u_value, part["u_value"]) + + self.recommendations.append( + { + **part, "new_u_value": new_u_value, + } + ) + + raise NotImplementedError("Not implemented yet") + + @staticmethod + def calculate_u_value_uplift(u_value, insulation_u_value): + """ + Calculates the U-value uplift (improvement) when applying internal wall insulation to a wall. + + + :param u_value: Float, Starting U-value of the wall (without insulation) in W/m²K. + :param insulation_u_value: Float, U-value of the internal wall insulation in W/m²K. + + Returns: + float: U-value uplift (improvement) achieved by applying internal wall insulation in W/m²K. + + Raises: + ZeroDivisionError: If either u_value or iwi_u_value is zero. + + Notes: + This function assumes 100% coverage of the internal wall insulation and does not account for other factors + such as thermal bridging or the specific configuration of the wall. + """ + + inverse_u_value = 1 / u_value + inverse_insulation_u_value = 1 / insulation_u_value + + inverse_u_total = inverse_u_value + inverse_insulation_u_value + new_u_value = 1 / inverse_u_total + + u_value_uplift = u_value - new_u_value + + return u_value_uplift, new_u_value