diff --git a/backend/Property.py b/backend/Property.py index 70a70307..7df947ce 100644 --- a/backend/Property.py +++ b/backend/Property.py @@ -800,13 +800,19 @@ class Property: to_update[k] = None return to_update - def get_full_property_data(self, current_valuation=None): + def get_full_property_data(self, current_valuation=None, needs_rebaselining=False, rebaselining_sap=0): """ This method extracts the data which is pushed to the database, containing core information, from the EPC about a property :return: """ + current_sap_rating = self.data["current-energy-efficiency"] + if needs_rebaselining: + current_sap_rating += rebaselining_sap + + current_epc_rating = sap_to_epc(current_sap_rating) + property_data = { "creation_status": "READY", "uprn": int(self.data["uprn"]), @@ -823,9 +829,12 @@ class Property: "number_of_rooms": self.number_of_rooms, "year_built": self.year_built, "tenure": self.data["tenure"], - "current_epc_rating": self.data["current-energy-rating"], - "current_sap_points": self.data["current-energy-efficiency"], + "current_epc_rating": current_epc_rating, + "current_sap_points": current_sap_rating, "current_valuation": current_valuation, + "original_sap_points": self.data["current-energy-efficiency"], + "is_sap_points_adjusted_for_installed_measures": needs_rebaselining, + "installed_measures_sap_point_adjustment": rebaselining_sap, } property_data = self._clean_upload_data(property_data) diff --git a/backend/app/db/functions/recommendations_functions.py b/backend/app/db/functions/recommendations_functions.py index 4fdd9324..7d448aa0 100644 --- a/backend/app/db/functions/recommendations_functions.py +++ b/backend/app/db/functions/recommendations_functions.py @@ -27,7 +27,6 @@ def prepare_plan_data( """ # Plan carbon savings co2_savings = sum([r["co2_equivalent_savings"] for r in default_recommendations]) - raise Exception("CHECK ME") post_co2_emissions = p.energy["co2_emissions"] - co2_savings # Plan bill savings diff --git a/backend/engine/engine.py b/backend/engine/engine.py index f4e3ad3f..e0c5fdb7 100644 --- a/backend/engine/engine.py +++ b/backend/engine/engine.py @@ -929,9 +929,7 @@ async def model_engine(body: PlanTriggerRequest): # any panel performance, we ensure that we have a 3kWp and 4kWp option for the property logger.info("Identifying property recommendations") - recommendations = {} - recommendations_scoring_data = [] - representative_recommendations = {} + recommendations, recommendations_scoring_data, representative_recommendations = {}, [], {} for p in tqdm(input_properties): # We set the ECO package data, if we have it property_eco_package = eco_packages.get(p.id, (None, None, None)) @@ -965,17 +963,15 @@ async def model_engine(body: PlanTriggerRequest): recommendations_scoring_data.extend(p.recommendations_scoring_data) logger.info("Preparing data for scoring in sap change api") - recommendations_scoring_data = pd.DataFrame(recommendations_scoring_data) + recommendations_scoring_data = pd.DataFrame(recommendations_scoring_data).drop( + columns=[ + "rdsap_change", "heat_demand_change", "carbon_change", "sap_ending", "heat_demand_ending", + "carbon_ending" + ] + ) # Temp putting this here recommendations_scoring_data["is_post_sap10_ending"] = True - recommendations_scoring_data["sap_starting"] = 77 - - recommendations_scoring_data = recommendations_scoring_data.drop( - columns=["rdsap_change", "heat_demand_change", "carbon_change", "sap_ending", "heat_demand_ending", - "carbon_ending"] - ) - all_predictions = await model_api.async_paginated_predictions( data=recommendations_scoring_data, bucket=get_settings().DATA_BUCKET, @@ -1015,19 +1011,19 @@ async def model_engine(body: PlanTriggerRequest): # We now insert kwh estimates and costs into the recommendations logger.info("Calculating tenant savings - kwh and bills") - for property_id in tqdm([p.id for p in input_properties]): + for p in tqdm(input_properties): + property_id = p.id property_recommendations = recommendations.get(property_id, []) - property_instance = [p for p in input_properties if p.id == property_id][0] property_current_energy_bill = ( Recommendations.calculate_recommendation_tenant_savings( - property_instance=property_instance, + property_instance=p, kwh_simulation_predictions=kwh_simulation_predictions, property_recommendations=property_recommendations, ashp_cop=body.ashp_cop ) ) - property_instance.current_energy_bill = property_current_energy_bill + p.current_energy_bill = property_current_energy_bill # Insert the predictions into the recommendations and run the optimiser logger.info("Optimising measures") @@ -1195,23 +1191,40 @@ async def model_engine(body: PlanTriggerRequest): property_updates, property_epc_details, property_spatial_updates = [], [], [] plans_to_create, recommendations_to_create = [], [] - # TODO: Check the update to carbon - print("NEED TO CHECK THE UPDATE TO CARBON") # Prepare the data that will need to be uploaded in bulk for p in input_properties: recommendations_for_property = recommendations.get(p.id, []) default_recommendations = [r for r in recommendations_for_property if r["default"]] + + # We need to: + # Get already installed measures + already_installed_default = [r for r in default_recommendations if r["already_installed"]] + # Property should be have increased SAP + needs_rebaselining = bool(len(already_installed_default)) + rebaselining_sap = float(sum([r["sap_points"] for r in already_installed_default])) + rebaselining_carbon = float(sum([r["co2_equivalent_savings"] for r in already_installed_default])) + rebaselining_heat_demand = float(sum([r["heat_demand"] for r in already_installed_default])) + rebaselining_kwh = float(sum([r["kwh_savings"] for r in already_installed_default])) + rebaselining_bills = float(sum([r["energy_cost_savings"] for r in already_installed_default])) + # TODO - gotta apply the adjustments to the property table, and the property_details_epc table + + # This will include everything, including already installed total_sap_points = sum([r["sap_points"] for r in default_recommendations]) new_sap_points = float(p.data["current-energy-efficiency"]) + total_sap_points new_epc = sap_to_epc(new_sap_points) - total_cost = sum([r["total"] for r in default_recommendations]) + # Already installed measures do not have a cost but we remove anyway + total_cost = sum([r["total"] for r in default_recommendations if not r["already_installed"]]) valuations = PropertyValuation.estimate(property_instance=p, target_epc=new_epc, total_cost=total_cost) # --- property-level updates (always) --- property_updates.append({ "property_id": p.id, "portfolio_id": body.portfolio_id, - "data": p.get_full_property_data(current_valuation=valuations["current_value"]) + "data": p.get_full_property_data( + current_valuation=valuations["current_value"], + needs_rebaselining=needs_rebaselining, + rebaselining_sap=rebaselining_sap, + ) }) property_epc_details.append(p.get_property_details_epc(portfolio_id=body.portfolio_id)) diff --git a/backend/ml_models/api.py b/backend/ml_models/api.py index 7f3e5873..daf4b715 100644 --- a/backend/ml_models/api.py +++ b/backend/ml_models/api.py @@ -142,7 +142,8 @@ class ModelApi: @staticmethod def extract_phase(recommendation_id): if 'phase=' in recommendation_id: - return int(recommendation_id.split('phase=')[1][0]) + extracted = recommendation_id.split('phase=')[1] + return int(extracted.strip()) else: return None diff --git a/recommendations/HeatingRecommender.py b/recommendations/HeatingRecommender.py index fdc25bf9..15a7b0b0 100644 --- a/recommendations/HeatingRecommender.py +++ b/recommendations/HeatingRecommender.py @@ -1,5 +1,6 @@ import re import backend.app.assumptions as assumptions +from etl.customers.immo.pilot.asset_list import already_installed from recommendations.recommendation_utils import ( check_simulation_difference, override_costs, combine_recommendation_configs ) @@ -320,12 +321,6 @@ class HeatingRecommender: measures = MEASURE_MAP["heating"] if measures is None else measures - # TODO: We could have a system flush recommendation for an existing boiler, where there is no need to replace - # the boiler, but instead flushing the system will make it run more efficiently. There is a cost for this - # in the Costs class, stored as SYSTEM_FLUSH_COST - - # TODO: Right now, we don't have recommendations for electric boilers - we should probably have one - # if we have a non-invasive ashp recommendation, we get the configuration directly from the property instance non_invasive_ashp_recommendation = next( (r for r in self.property.non_invasive_recommendations if r["type"] == "air_source_heat_pump"), @@ -1115,6 +1110,7 @@ class HeatingRecommender: "hot-water-energy-eff": heating_simulation_config["hot_water_energy_eff_ending"] } + # TODO: Probably don't need to use this for HHRSH - simplify recommendations = self.combine_heating_and_controls( controls_recommendations=controls_recommender.recommendation, heating_simulation_config=heating_simulation_config, @@ -1128,6 +1124,12 @@ class HeatingRecommender: non_intrusive_recommendation=non_intrusive_recommendation, heating_product=hhrsh_product ) + + # Check if HHRSH are already installed + already_installed = "high_heat_retention_storage_heaters" in self.property.already_installed + for rec in recommendations: + rec["already_installed"] = already_installed + if _return: return recommendations @@ -1347,7 +1349,7 @@ class HeatingRecommender: n_rooms=self.property.number_of_rooms ) - already_installed = "heating" in self.property.already_installed + already_installed = "boiler_upgrade" in self.property.already_installed if already_installed: boiler_costs = override_costs(boiler_costs) description = "Heating system has already been upgraded, no further action needed." diff --git a/recommendations/Recommendations.py b/recommendations/Recommendations.py index 29ba267a..e1d63592 100644 --- a/recommendations/Recommendations.py +++ b/recommendations/Recommendations.py @@ -272,6 +272,36 @@ class Recommendations: property_recommendations.append(self.solar_recommender.recommendation) phase += 1 + if self.property_instance.already_installed: + # We need to re-shuffle our measures + property_recommendations_removed_installed = [] + already_installed_recs = [] + for recs in property_recommendations: + phase_recs = [] + phase_already_installed_recs = [] + for rec in recs: + if rec["already_installed"]: + phase_already_installed_recs.append(rec) + else: + phase_recs.append(rec) + if phase_recs: + property_recommendations_removed_installed.append(phase_recs) + if phase_already_installed_recs: + already_installed_recs.append(phase_already_installed_recs) + + # We re-set the phases + for i, recs in enumerate(property_recommendations_removed_installed): + for rec in recs: + rec["phase"] = i + # already installed recs get negative phasing + already_installed_phase = -len(already_installed_recs) + for recs in already_installed_recs: + for rec in recs: + rec["phase"] = already_installed_phase + already_installed_phase += 1 + + property_recommendations = already_installed_recs + property_recommendations_removed_installed + # We insert temporary ids into the recommendations which is important for the optimiser later property_recommendations = self.insert_temp_recommendation_id(property_recommendations) @@ -486,6 +516,11 @@ class Recommendations: mv_increasing_variables = ["carbon", "heat_demand"] mv_decreasing_variables = ["sap"] + # We allow for negative phase + starting_phase = min( + rec["phase"] for recs in property_recommendations for rec in recs + ) + impact_summary = [] for recommendations_by_type in property_recommendations: for rec in recommendations_by_type: @@ -526,7 +561,7 @@ class Recommendations: # We structure this so that depending on the phase, we capture the previous phase impacts and # then just have one piece of code to calculate the difference - if rec["phase"] == 0: + if rec["phase"] == starting_phase: # These are just the starting values, from the EPC. When we score the ML models, # heating_cost_starting and heating_cost_ending are just the values in the EPC. However, with # heating_cost_ending, we expect that the EPC will predict a heating cost based on what would happen @@ -954,6 +989,33 @@ class Recommendations: pd.isnull(kwh_impact_table["hotwater_fuel_type"]).sum()): raise Exception("Fuel type is missing") + # As one final adjustment, if we + # 1) have a boiler upgrade recommendation + # 2) Have an average efficiency boiler, we adjust the COP of the existing boiler down to 75% + heating_upgrades = [x for x in property_recommendations if x[0]["type"] == "heating"] + boiler_upgrade = [r for recs in heating_upgrades for r in recs if r["measure_type"] == "boiler_upgrade"] + existing_heating_efficiency = property_instance.data["mainheat-energy-eff"] + + if len(boiler_upgrade) and existing_heating_efficiency in ["Very Poor", "Poor", "Average"]: + efficiency_map = {"Very Poor": 0.6, "Poor": 0.65, "Average": 0.7} + adjusted_cop = efficiency_map[existing_heating_efficiency] + boiler_phase = boiler_upgrade[0]["phase"] + heating_measure_types_to_id = [ + {"recommendation_id": r["recommendation_id"], "measure_type": r["measure_type"]} + for r in heating_upgrades[0] + ] + kwh_impact_table = kwh_impact_table.merge( + pd.DataFrame(heating_measure_types_to_id), how="left", on="recommendation_id" + ) + for col in ["heating_cop", "hotwater_cop"]: + kwh_impact_table[col] = np.where( + (kwh_impact_table["phase"] <= boiler_phase) & + (kwh_impact_table["heating_fuel_type"] == "Natural Gas") & + (kwh_impact_table["measure_type"] != "boiler_upgrade"), + adjusted_cop, kwh_impact_table[col] + ) + kwh_impact_table = kwh_impact_table.drop(columns=["measure_type"]) + # We now calculate the fuel cost for k in ["heating", "hotwater"]: kwh_impact_table[f"{k}_cost"] = kwh_impact_table.apply(