From d7ed4dd9a4468d3a54c56ad3992c3145e324bdfa Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 17 Oct 2024 18:31:37 +0100 Subject: [PATCH] tweaking solar recommendations --- backend/Property.py | 3 +- backend/apis/GoogleSolarApi.py | 27 +++++++-------- backend/app/assumptions.py | 4 ++- backend/app/plan/router.py | 4 +-- etl/customers/remote_assessments/app.py | 7 +++- recommendations/Recommendations.py | 41 +++++++++++++++++++++-- recommendations/RoofRecommendations.py | 20 ++++++++++- recommendations/SolarPvRecommendations.py | 2 +- recommendations/WallRecommendations.py | 14 ++++---- 9 files changed, 92 insertions(+), 30 deletions(-) diff --git a/backend/Property.py b/backend/Property.py index 2f5341bb..31f207ab 100644 --- a/backend/Property.py +++ b/backend/Property.py @@ -538,7 +538,8 @@ class Property: "loft_insulation", "room_roof_insulation", "flat_roof_insulation", "solid_floor_insulation", "suspended_floor_insulation", "windows_glazing", "solar_pv", "heating", "hot_water_tank_insulation", - "heating_control", "secondary_heating", "cylinder_thermostat", "mixed_glazing" + "heating_control", "secondary_heating", "cylinder_thermostat", "mixed_glazing", + "extension_cavity_wall_insulation", ]: raise NotImplementedError( "Implement me, given type %s" % recommendation["type"] diff --git a/backend/apis/GoogleSolarApi.py b/backend/apis/GoogleSolarApi.py index f6e1b96d..ed7b7422 100644 --- a/backend/apis/GoogleSolarApi.py +++ b/backend/apis/GoogleSolarApi.py @@ -272,23 +272,10 @@ class GoogleSolarApi: roi_summary = [] for segment in roof_segment_summaries: - - if segment["panelsCount"] < min_panels: - continue - wattage = segment["panelsCount"] * self.insights_data["solarPotential"]["panelCapacityWatts"] generated_dc_energy = segment["yearlyEnergyDcKwh"] ratio = generated_dc_energy / wattage - if cost_instance is None: - cost = MCS_SOLAR_PV_COST_DATA["average_cost_per_kwh"] * (wattage / 1000) - else: - cost = cost_instance.solar_pv( - n_panels=segment["panelsCount"], - has_battery=False, - n_floors=property_instance.number_of_floors, - )["total"] - roi_summary.append( { "segmentIndex": segment["segmentIndex"], @@ -296,7 +283,6 @@ class GoogleSolarApi: "generated_dc_energy": generated_dc_energy, "ratio": ratio, "n_panels": segment["panelsCount"], - "cost": cost, "panneled_roof_area": self.panel_area * int(segment["panelsCount"]) } ) @@ -305,10 +291,21 @@ class GoogleSolarApi: if roi_summary.empty: continue + if roi_summary["n_panels"].sum() < min_panels: + continue + + if cost_instance is None: + total_cost = MCS_SOLAR_PV_COST_DATA["average_cost_per_kwh"] * (wattage / 1000) + else: + total_cost = cost_instance.solar_pv( + n_panels=roi_summary["n_panels"].sum(), + has_battery=False, + n_floors=property_instance.number_of_floors, + )["total"] + weighted_ratio = np.average( roi_summary["ratio"].values, weights=roi_summary["generated_dc_energy"].values ) - total_cost = roi_summary["cost"].sum() yearly_dc_energy = roi_summary["generated_dc_energy"].sum() panel_performance.append( diff --git a/backend/app/assumptions.py b/backend/app/assumptions.py index 5571e13b..79f2a087 100644 --- a/backend/app/assumptions.py +++ b/backend/app/assumptions.py @@ -4,8 +4,10 @@ PESSIMISTIC_ASHP_EFFICIENCY = 200 AVERAGE_ASHP_EFFICIENCY = 250 # Conservative estimate of the proportion of electricity that will be consumed, whereas the rest will -# be exported +# be exported. These are averages based on Google research. E.g +# https://www.nea.org.uk/who-we-are/innovation-technical-evaluation/solarpv/solarpv-batteries SOLAR_CONSUMPTION_PROPORTION = 0.5 +SOLAR_CONSUMPTION_WITH_BATTERY_PROPORTION = 0.7 # Typically, each solar panel takes up around 3.4 m2 of roof space under RdSAP. This was been verified in Elmhurst RDSAP_AREA_PER_PANEL = 3.4 diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py index d376b01e..925fb05b 100644 --- a/backend/app/plan/router.py +++ b/backend/app/plan/router.py @@ -369,9 +369,9 @@ def extract_property_request_data( property_non_invasive_recommendations["recommendations"] = str(transformed) property_valution = next(( - x for x in valuation_data if + float(x["value"]) for x in valuation_data if (str(x["uprn"]) == str(uprn)) - ), {}) + ), None) return patch, property_already_installed, property_non_invasive_recommendations, property_valution diff --git a/etl/customers/remote_assessments/app.py b/etl/customers/remote_assessments/app.py index ba3563cd..a0d01f7d 100644 --- a/etl/customers/remote_assessments/app.py +++ b/etl/customers/remote_assessments/app.py @@ -47,7 +47,12 @@ def app(): file_name=non_invasive_recommendations_filename ) - valuation_data = [{100050770761: 67_000}] + valuation_data = [ + { + "uprn": 100050770761, + "value": 67_000 + } + ] # Store valuation data to s3 valuation_filename = f"{USER_ID}/{PORTFOLIO_ID}/valuation.csv" save_csv_to_s3( diff --git a/recommendations/Recommendations.py b/recommendations/Recommendations.py index b9862585..aa7e041e 100644 --- a/recommendations/Recommendations.py +++ b/recommendations/Recommendations.py @@ -129,10 +129,11 @@ class Recommendations: phase += 1 # We handle recommendations covering specific non-invasive measures - self.wall_recomender.recommend_extended(measures=measures) + new_phase = self.wall_recomender.recommend_extended(phase=phase, measures=measures) if self.wall_recomender.extended_recommendations: property_recommendations.append(self.wall_recomender.extended_recommendations) # We don't have any phasing here + phase = new_phase self.roof_recommender.recommend(phase=phase, measures=measures, default_u_values=self.default_u_values) if self.roof_recommender.recommendations: @@ -481,6 +482,27 @@ class Recommendations: ]: # We don't have a percieved sap impact of mechanical ventilation or trickle vents, and we don't # have the capacity to score draught proofing + if rec["type"] == "extension_cavity_wall_insulation": + + previous_phase = [x for x in impact_summary if x["phase"] == (rec["phase"] - 1)] + if previous_phase: + sap = previous_phase[0]["sap"] + carbon = previous_phase[0]["carbon"] + heat_demand = previous_phase[0]["heat_demand"] + else: + sap = float(property_instance.data["current-energy-efficiency"]) + carbon = float(property_instance.data["co2-emissions-current"]) + heat_demand = float(property_instance.data["energy-consumption-current"]) + + impact_summary.append( + { + "phase": rec["phase"], + "recommendation_id": rec["recommendation_id"], + "sap": sap + rec["sap_points"], + "carbon": carbon - rec["co2_equivalent_savings"], + "heat_demand": heat_demand - rec["heat_demand"], + } + ) continue phase_energy_efficiency_metrics = { @@ -571,6 +593,17 @@ class Recommendations: property_phase_impact["carbon"], rec["co2_equivalent_savings"] ) + if rec["type"] == "loft_insulation": + # When we have a loft insulation recommendation, where there is an extension and the existing + # amount of loft insulation is already good, we limit the SAP points + # By limiting here, we don't change the value in current_phase_values. This means that the + # future recommendations won't have an impact that is too large + li_sap_limit = RoofRecommendations.get_loft_insulation_sap_limit( + property_instance.data["roof-energy-eff"], property_instance.data["extension-count"] + ) + if li_sap_limit is not None: + property_phase_impact["sap"] = min(property_phase_impact["sap"], li_sap_limit) + # Insert this information into the recommendation. if not rec.get("survey", False): rec["sap_points"] = property_phase_impact["sap"] @@ -672,7 +705,11 @@ class Recommendations: { "phase": r["phase"], "recommendation_id": r["recommendation_id"], - "solar_kwh_savings": r["initial_ac_kwh_per_year"] * assumptions.SOLAR_CONSUMPTION_PROPORTION, + "solar_kwh_savings": ( + r["initial_ac_kwh_per_year"] * assumptions.SOLAR_CONSUMPTION_PROPORTION + ) if not r["has_battery"] else ( + r["initial_ac_kwh_per_year"] * assumptions.SOLAR_CONSUMPTION_WITH_BATTERY_PROPORTION + ), } for recs in property_recommendations for r in recs if r["type"] == "solar_pv" ], columns=["phase", "recommendation_id", "solar_kwh_savings"]) diff --git a/recommendations/RoofRecommendations.py b/recommendations/RoofRecommendations.py index 52313121..c0fa4eb2 100644 --- a/recommendations/RoofRecommendations.py +++ b/recommendations/RoofRecommendations.py @@ -59,6 +59,23 @@ class RoofRecommendations: self.property.roof["is_flat"] ) + @classmethod + def get_loft_insulation_sap_limit(cls, roof_energy_eff, extension_count): + """ + Get the SAP limit for loft insulation + :param roof_energy_eff: + :return: + """ + + if extension_count == 0: + # No limit + return None + + if roof_energy_eff in ["Good", "Very Good"]: + return 1 + + return None + def mds_loft_insulation(self, phase): """ For usages within the mds report @@ -273,7 +290,7 @@ class RoofRecommendations: # loft is already partially insulated. # Note: This requirement is only for loft insulation if ( - (material["depth"] + insulation_thickness) < self.MINIMUM_RECOMMENDED_LOFT_INSULATION + material["depth"] < self.MINIMUM_RECOMMENDED_LOFT_INSULATION ) and is_pitched: continue @@ -295,6 +312,7 @@ class RoofRecommendations: # We allow a small tolerance for error so we don't discount the recommendation entirely if new_u_value <= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE: + lowest_selected_u_value = update_lowest_selected_u_value(lowest_selected_u_value, new_u_value) cost_result = self.costs.loft_and_flat_insulation( diff --git a/recommendations/SolarPvRecommendations.py b/recommendations/SolarPvRecommendations.py index eb4616ea..66c1d0c3 100644 --- a/recommendations/SolarPvRecommendations.py +++ b/recommendations/SolarPvRecommendations.py @@ -196,7 +196,7 @@ class SolarPvRecommendations: ] roof_area = self.property.roof_area - solar_configurations = panel_performance.head(3).reset_index(drop=True) + solar_configurations = panel_performance.head(6).reset_index(drop=True) # We combine each of these configurations with estimates with and without a battery for rank, recommendation_config in solar_configurations.iterrows(): diff --git a/recommendations/WallRecommendations.py b/recommendations/WallRecommendations.py index 88242709..c7917911 100644 --- a/recommendations/WallRecommendations.py +++ b/recommendations/WallRecommendations.py @@ -270,7 +270,7 @@ class WallRecommendations(Definitions): # If the u-value is within regulations, we don't do anything return - def recommend_extended(self, measures): + def recommend_extended(self, phase, measures): """ Where we have extended measures, such as extension insulation, which cannot typically be picked up from the EPC api, we handle the recommendation of these here @@ -283,22 +283,24 @@ class WallRecommendations(Definitions): measures_to_recommend = [measure for measure in measures if measure in extended_measures] if not measures_to_recommend: - return + return phase # We reset this to be empty self.extended_recommendations = [] + recommendation_phase = phase for measure in measures_to_recommend: if measure == "extension_cavity_wall_insulation": - recommendation = self.recommend_extension_cavity_wall_insulation() + recommendation = self.recommend_extension_cavity_wall_insulation(phase=recommendation_phase) else: raise NotImplementedError(f"Measure {measure} is not implemented") + recommendation_phase += 1 self.extended_recommendations.append(recommendation) - return + return recommendation_phase - def recommend_extension_cavity_wall_insulation(self): + def recommend_extension_cavity_wall_insulation(self, phase): """ This function produces the recommendation for extension cavity wall insulation :return: @@ -331,7 +333,7 @@ class WallRecommendations(Definitions): ) recommendation = { - "phase": None, + "phase": phase, "parts": [], "type": "extension_cavity_wall_insulation", "measure_type": "extension_cavity_wall_insulation",