diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py index 90353052..6e4d8475 100644 --- a/backend/app/plan/router.py +++ b/backend/app/plan/router.py @@ -730,77 +730,80 @@ async def trigger_plan(body: PlanTriggerRequest): recommendations[property_id] = recommendations_with_impact # For Debugging - recommendation_impact_df = [] - for property_id in recommendations.keys(): - for recs_by_type in recommendations[property_id]: - for rec in recs_by_type: - recommendation_impact_df.append( - { - "property_id": property_id, - "uprn": [p.uprn for p in input_properties if p.id == property_id][0], - "address": [p.address for p in input_properties if p.id == property_id][0], - "recommendation_id": rec["recommendation_id"], - "type": rec["type"], - "description": rec["description"], - "sap_points": rec["sap_points"], - "co2_equivalent_savings": rec["co2_equivalent_savings"], - "heat_demand": rec["heat_demand"] - } - ) - recommendation_impact_df = pd.DataFrame(recommendation_impact_df) - - surveyed_uprns = [ - 10024087855, 121016117, 121016124, - 10024087902, 121016121, 121016128 - ] - recommendation_impact_df = recommendation_impact_df[recommendation_impact_df["uprn"].isin(surveyed_uprns)] - # recommendation_impact_df = recommendation_impact_df[recommendation_impact_df["type"].isin( - # ["windows_glazing", "internal_wall_insulation"]) + # recommendation_impact_df = [] + # for property_id in recommendations.keys(): + # for recs_by_type in recommendations[property_id]: + # for rec in recs_by_type: + # recommendation_impact_df.append( + # { + # "property_id": property_id, + # "uprn": [p.uprn for p in input_properties if p.id == property_id][0], + # "address": [p.address for p in input_properties if p.id == property_id][0], + # "recommendation_id": rec["recommendation_id"], + # "type": rec["type"], + # "description": rec["description"], + # "sap_points": rec["sap_points"], + # "co2_equivalent_savings": rec["co2_equivalent_savings"], + # "heat_demand": rec["heat_demand"] + # } + # ) + # recommendation_impact_df = pd.DataFrame(recommendation_impact_df) + # + # surveyed_uprns = [ + # 10024087855, 121016117, 121016124, + # 10024087902, 121016121, 121016128 # ] - - actual_impacts_df = pd.DataFrame( - [ - # 10024087855 - {"uprn": 10024087855, "type": "internal_wall_insulation", "actual_sap_points": 5}, - {"uprn": 10024087855, "type": "draught_proofing", "actual_sap_points": 2}, - {"uprn": 10024087855, "type": "low_energy_lighting", "actual_sap_points": 0}, - {"uprn": 10024087855, "type": "windows_glazing", "actual_sap_points": 4}, - # 121016117 - {"uprn": 121016117, "type": "internal_wall_insulation", "actual_sap_points": 6}, - {"uprn": 121016117, "type": "draught_proofing", "actual_sap_points": 1}, - {"uprn": 121016117, "type": "low_energy_lighting", "actual_sap_points": 1}, - {"uprn": 121016117, "type": "windows_glazing", "actual_sap_points": 4}, - # 121016124 - {"uprn": 121016124, "type": "internal_wall_insulation", "actual_sap_points": 8}, - {"uprn": 121016124, "type": "low_energy_lighting", "actual_sap_points": 2}, - {"uprn": 121016124, "type": "windows_glazing", "actual_sap_points": 5}, - # 10024087902 - {"uprn": 10024087902, "type": "room_roof_insulation", "actual_sap_points": 16}, - {"uprn": 10024087902, "type": "internal_wall_insulation", "actual_sap_points": 2}, - {"uprn": 10024087902, "type": "low_energy_lighting", "actual_sap_points": 0}, - # 121016121 - {"uprn": 121016121, "type": "internal_wall_insulation", "actual_sap_points": 5}, - {"uprn": 121016121, "type": "suspended_floor_insulation", "actual_sap_points": 2}, - {"uprn": 121016121, "type": "draught_proofing", "actual_sap_points": 1}, - {"uprn": 121016121, "type": "windows_glazing", "actual_sap_points": 3}, - # 121016128 - {"uprn": 121016128, "type": "internal_wall_insulation", "actual_sap_points": 6}, - {"uprn": 121016128, "type": "suspended_floor_insulation", "actual_sap_points": 1}, - {"uprn": 121016128, "type": "draught_proofing", "actual_sap_points": 1}, - {"uprn": 121016128, "type": "low_energy_lighting", "actual_sap_points": 1}, - {"uprn": 121016128, "type": "windows_glazing", "actual_sap_points": 3}, - ] - ) - - comparison = recommendation_impact_df.merge( - actual_impacts_df, how="inner", on=["uprn", "type"] - ) - - property_recs = recommendation_impact_df[recommendation_impact_df["uprn"] == 121016128] - property = [p for p in input_properties if p.uprn == 121016128][0] - print(property.data["current-energy-efficiency"]) - print(property_recs["sap_points"].sum()) - property_recs["address"] + # recommendation_impact_df = recommendation_impact_df[recommendation_impact_df["uprn"].isin(surveyed_uprns)] + # # recommendation_impact_df = recommendation_impact_df[recommendation_impact_df["type"].isin( + # # ["windows_glazing", "internal_wall_insulation"]) + # # ] + # + # actual_impacts_df = pd.DataFrame( + # [ + # # 10024087855 + # {"uprn": 10024087855, "type": "internal_wall_insulation", "actual_sap_points": 5}, + # {"uprn": 10024087855, "type": "draught_proofing", "actual_sap_points": 2}, + # {"uprn": 10024087855, "type": "low_energy_lighting", "actual_sap_points": 0}, + # {"uprn": 10024087855, "type": "windows_glazing", "actual_sap_points": 4}, + # # 121016117 + # {"uprn": 121016117, "type": "internal_wall_insulation", "actual_sap_points": 6}, + # {"uprn": 121016117, "type": "draught_proofing", "actual_sap_points": 1}, + # {"uprn": 121016117, "type": "low_energy_lighting", "actual_sap_points": 1}, + # {"uprn": 121016117, "type": "windows_glazing", "actual_sap_points": 4}, + # # 121016124 + # {"uprn": 121016124, "type": "internal_wall_insulation", "actual_sap_points": 8}, + # {"uprn": 121016124, "type": "low_energy_lighting", "actual_sap_points": 2}, + # {"uprn": 121016124, "type": "windows_glazing", "actual_sap_points": 5}, + # # 10024087902 + # {"uprn": 10024087902, "type": "room_roof_insulation", "actual_sap_points": 16}, + # {"uprn": 10024087902, "type": "internal_wall_insulation", "actual_sap_points": 2}, + # {"uprn": 10024087902, "type": "low_energy_lighting", "actual_sap_points": 0}, + # # 121016121 + # {"uprn": 121016121, "type": "internal_wall_insulation", "actual_sap_points": 5}, + # {"uprn": 121016121, "type": "suspended_floor_insulation", "actual_sap_points": 2}, + # {"uprn": 121016121, "type": "draught_proofing", "actual_sap_points": 1}, + # {"uprn": 121016121, "type": "windows_glazing", "actual_sap_points": 3}, + # # 121016128 + # {"uprn": 121016128, "type": "internal_wall_insulation", "actual_sap_points": 6}, + # {"uprn": 121016128, "type": "suspended_floor_insulation", "actual_sap_points": 1}, + # {"uprn": 121016128, "type": "draught_proofing", "actual_sap_points": 1}, + # {"uprn": 121016128, "type": "low_energy_lighting", "actual_sap_points": 1}, + # {"uprn": 121016128, "type": "windows_glazing", "actual_sap_points": 3}, + # ] + # ) + # + # comparison = recommendation_impact_df.merge( + # actual_impacts_df, how="inner", on=["uprn", "type"] + # ) + # + # print(recommendation_impact_df.groupby(["uprn"])["sap_points"].sum()) + # property_recs = recommendation_impact_df[recommendation_impact_df["uprn"] == 121016128] + # property = [p for p in input_properties if p.uprn == 121016128][0] + # print(property.data["current-energy-efficiency"]) + # print(property_recs["sap_points"].sum()) + # print(property_recs["type"]) + # print(float(property.data["current-energy-efficiency"]) + property_recs["sap_points"].sum()) + # recommendations[property.id][2][0]["simulation_config"] # from utils.s3 import read_dataframe_from_s3_parquet # training_data = read_dataframe_from_s3_parquet( diff --git a/backend/app/plan/schemas.py b/backend/app/plan/schemas.py index 0ddd9761..c08cdefc 100644 --- a/backend/app/plan/schemas.py +++ b/backend/app/plan/schemas.py @@ -36,7 +36,8 @@ SPECIFIC_MEASURES = [ # Solar "solar_pv", # Windows Glazing - "windows", + "double_glazing", + "secondary_glazing", # Mechanical ventilation "ventilation", # Other @@ -62,6 +63,7 @@ MEASURE_MAP = { "roof_insulation": ["loft_insulation", "flat_roof_insulation", "room_roof_insulation"], "floor_insulation": ["suspended_floor_insulation", "solid_floor_insulation"], "heating": ["boiler_upgrade", "high_heat_retention_storage_heater", "air_source_heat_pump"], + "windows": ["double_glazing", "secondary_glazing"], } diff --git a/recommendations/FloorRecommendations.py b/recommendations/FloorRecommendations.py index db18a458..d82162da 100644 --- a/recommendations/FloorRecommendations.py +++ b/recommendations/FloorRecommendations.py @@ -224,7 +224,9 @@ class FloorRecommendations(Definitions): simulation_config = { **floor_simulation_config, - "floor_thermal_transmittance_ending": new_u_value, + # We don't simulate the impact using this U-value, but rather the average because this + # variable is way too volatile. Will likely be removed from the model + "floor_thermal_transmittance_ending": 0.685593, } self.recommendations.append( diff --git a/recommendations/LightingRecommendations.py b/recommendations/LightingRecommendations.py index 92394c11..2b0e8724 100644 --- a/recommendations/LightingRecommendations.py +++ b/recommendations/LightingRecommendations.py @@ -1,3 +1,5 @@ +import pandas as pd + from backend.Property import Property from typing import List from recommendations.Costs import Costs @@ -30,6 +32,37 @@ class LightingRecommendations: self.material = material[0] self.recommendation = [] + @classmethod + def get_sap_limit(cls, lighting_energy_efficiency: str, lighting_proportion: float): + """ + Lighting seems to be a more straight forward measure to estimate SAP points for, based on the starting + energy efficiency rating. + + We seem to have the following brackes based on % of LEDs in outlets + Very poor: 0 - 9% + Poor: 10 - 24% + Average: 25 - 44% + Good: 45 - 69% + Very good: 70 - 100% + :return: + """ + + if lighting_energy_efficiency == "Very Good": + return 0 + + if lighting_energy_efficiency in ["Good", "Average"]: + return cls.SAP_LOWER_LIMIT + + # If lighting_energy_efficiency is missing, we'll use the proportion of low energy lighting + if not lighting_energy_efficiency or pd.isnull(lighting_energy_efficiency): + if lighting_proportion >= 0.7: + return 0 + if lighting_proportion >= 0.25: + return cls.SAP_LOWER_LIMIT + return cls.SAP_LIMIT + + return cls.SAP_LIMIT + @staticmethod def estimate_lighting_impact(number_of_bulbs: int): """ diff --git a/recommendations/Recommendations.py b/recommendations/Recommendations.py index d5e37f8e..526cb2a2 100644 --- a/recommendations/Recommendations.py +++ b/recommendations/Recommendations.py @@ -80,6 +80,13 @@ class Recommendations: inclusions_full = [MEASURE_MAP[x] if x in MEASURE_MAP else x for x in self.inclusions] exclusions_full = [MEASURE_MAP[x] if x in MEASURE_MAP else x for x in self.exclusions] + # We need to unlist any lists, but we should check if they're lists first + inclusions_full = [ + item for sublist in inclusions_full for item in (sublist if isinstance(sublist, list) else [sublist]) + ] + exclusions_full = [ + item for sublist in exclusions_full for item in (sublist if isinstance(sublist, list) else [sublist]) + ] # If inclusions and exclusions are empty, it means that nothing was specified, so we allow # all recommendation types @@ -163,9 +170,9 @@ class Recommendations: property_recommendations.append(self.lighting_recommender.recommendation) phase += 1 - if "windows" in measures and "mixed_glazing" not in non_invasive_recommendation_types: + if "mixed_glazing" not in non_invasive_recommendation_types: # If we have a mixed glazing recommendation, we prioritise this over the windows recommendation - self.windows_recommender.recommend(phase=phase) + self.windows_recommender.recommend(phase=phase, measures=measures) if self.windows_recommender.recommendation: property_recommendations.append(self.windows_recommender.recommendation) phase += 1 @@ -538,11 +545,11 @@ class Recommendations: # For the moment, we cap the number of SAP points that can be achieved by LEDs at 2 if rec["type"] == "low_energy_lighting": + lighting_sap_limit = LightingRecommendations.get_sap_limit( + property_instance.data["lighting-energy-eff"], + property_instance.lighting["low_energy_proportion"] + ) - if property_instance.data["low-energy-lighting"] < 50: - lighting_sap_limit = LightingRecommendations.SAP_LIMIT - else: - lighting_sap_limit = LightingRecommendations.SAP_LOWER_LIMIT property_phase_impact["sap"] = min(property_phase_impact["sap"], lighting_sap_limit) property_phase_impact["carbon"] = min( property_phase_impact["carbon"], rec["co2_equivalent_savings"] diff --git a/recommendations/WindowsRecommendations.py b/recommendations/WindowsRecommendations.py index bc91f801..235d9ee2 100644 --- a/recommendations/WindowsRecommendations.py +++ b/recommendations/WindowsRecommendations.py @@ -3,6 +3,7 @@ from typing import List import numpy as np from backend.Property import Property +from backend.app.plan.schemas import MEASURE_MAP from etl.epc_clean.epc_attributes.WindowAttributes import WindowAttributes from recommendations.Costs import Costs from recommendations.recommendation_utils import override_costs, check_simulation_difference @@ -32,7 +33,7 @@ class WindowsRecommendations: raise ValueError("There should only be one window glazing material") self.glazing_material = self.glazing_material[0] - def recommend(self, phase=0): + def recommend(self, measures=None, phase=0): """ This method will recommend the best possible glazing options for a property. @@ -41,14 +42,26 @@ class WindowsRecommendations: :return: """ + measures = MEASURE_MAP["windows"] if measures is None else measures + + # If we have no windows recs, leave + if not any(x in measures for x in MEASURE_MAP["windows"]): + return + # If the property is in a conservation area or is a listed building, it becomes more difficult to install # double glazing. Therefore, we don't recommend it. It is still possible but is not practical as it # requires planning permission and might require a more expensive window type, such as timber. number_of_windows = self.property.number_of_windows - is_secondary_glazing = self.property.restricted_measures or ( - self.property.windows["glazing_type"] == "secondary" - ) + + if "double_glazing" in measures and "secondary_glazing" not in measures: + is_secondary_glazing = False + elif "secondary_glazing" in measures and "double_glazing" not in measures: + is_secondary_glazing = True + else: + is_secondary_glazing = self.property.restricted_measures or ( + self.property.windows["glazing_type"] == "secondary" + ) windows_area = self.property.windows_area if not number_of_windows: