From 2efb2a4f3ed248ec619b099b91f06227fd5722ad Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 9 Aug 2024 18:21:23 +0100 Subject: [PATCH] implemented recommendation level kwh and cost savings --- backend/Property.py | 4 + backend/app/plan/router.py | 178 ++++++++++++++++++++++++++++++++----- 2 files changed, 160 insertions(+), 22 deletions(-) diff --git a/backend/Property.py b/backend/Property.py index e65ba272..bcb24325 100644 --- a/backend/Property.py +++ b/backend/Property.py @@ -183,6 +183,7 @@ class Property: self.recommendations_scoring_data = [] self.simulation_epcs = {} + self.updated_simulation_epcs = [] # This additional condition data should change how we pass kwargs to this. We should no longer need to pass # kwargs to this class, but instead, we should pass the energy assessment condition data @@ -454,6 +455,9 @@ class Property: ) updated_simulation_epcs.append(sim_epc) + # Now we havet this data inthe + self.updated_simulation_epcs = updated_simulation_epcs + return updated_simulation_epcs @staticmethod diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py index 985588e9..b3a385be 100644 --- a/backend/app/plan/router.py +++ b/backend/app/plan/router.py @@ -671,29 +671,14 @@ async def trigger_plan(body: PlanTriggerRequest): # We use the impact_summary to update the simulation_epcs with the new SAP, heat demand, carbon, cost etc # at each phase - property_scoring_epcs = property_instance.update_simulation_epcs(impact_summary) - scoring_epcs.extend(property_scoring_epcs) + property_instance.update_simulation_epcs(impact_summary) + scoring_epcs.extend(property_instance.updated_simulation_epcs) recommendations[property_id] = recommendations_with_impact # We call the API with the scoring epcs scoring_epcs = pd.DataFrame(scoring_epcs) scoring_epcs = kwh_client.transform(data=scoring_epcs, cleaned=cleaned) - # There should be no difference between index 9 and index 8, apart from photo-supply (other that sap, etc) - a = scoring_epcs[scoring_epcs.index == 6] - b = scoring_epcs[scoring_epcs.index == 11] - difference = [] - for col in a.columns: - if a[col].values[0] != b[col].values[0]: - difference.append( - { - "col": col, - "without_solar": a[col].values[0], - "with_solar": b[col].values[0] - } - ) - difference = pd.DataFrame(difference) - kwh_simulation_predictions = model_api.paginated_predictions( data=scoring_epcs, bucket=get_settings().DATA_BUCKET, @@ -715,16 +700,42 @@ async def trigger_plan(body: PlanTriggerRequest): how="inner", on="id", suffixes=("_heating", "_hotwater") + ).reset_index(drop=True) + + # We adjust this table with the kwh estimates for low energy lighting kwh values, and solar kwh estimates + led_recommendation = pd.DataFrame([ + { + "phase": r["phase"], + "recommendation_id": r["recommendation_id"], + "lighting_kwh_savings": r["kwh_savings"] * GoogleSolarApi.SOLAR_CONSUMPTION_PROPORTION, + } for recs in property_recommendations for r in recs if r["type"] == "low_energy_lighting" + ], columns=["phase", "recommendation_id", "lighting_kwh_savings"]) + solar_recommendations = pd.DataFrame([ + { + "phase": r["phase"], + "recommendation_id": r["recommendation_id"], + "solar_kwh_savings": r["initial_ac_kwh_per_year"] * GoogleSolarApi.SOLAR_CONSUMPTION_PROPORTION, + } for recs in property_recommendations for r in recs if r["type"] == "solar_pv" + ], columns=["phase", "recommendation_id", "solar_kwh_savings"]) + + # merge them on + kwh_impact_table = kwh_impact_table.merge( + led_recommendation, how="left", on=["phase", "recommendation_id"] + ).merge( + solar_recommendations, how="left", on=["phase", "recommendation_id"] ) property_kwh = property_instance.energy_consumption_estimates["unadjusted"] + starting_dummy_id_value = -9999 kwh_impact_table = pd.concat( [ pd.DataFrame( [ { - "id": None, + "id": starting_dummy_id_value, + "phase": starting_dummy_id_value, + "recommendation_id": starting_dummy_id_value, "predictions_heating": property_kwh["heating"], "predictions_hotwater": property_kwh["hot_water"], } @@ -732,7 +743,19 @@ async def trigger_plan(body: PlanTriggerRequest): ), kwh_impact_table ] - ) + ).sort_values(["phase", "recommendation_id"], ascending=True).reset_index(drop=True) + + for i in range(0, len(kwh_impact_table)): + current_phase = kwh_impact_table.loc[i, 'phase'] + previous_phase_id = (current_phase - 1) if (current_phase > 0) else -9999 + previous_phase = kwh_impact_table[kwh_impact_table['phase'] == previous_phase_id] + + if not previous_phase.empty: + for col in ["predictions_heating", "predictions_hotwater"]: + if kwh_impact_table.loc[i, col] > previous_phase[col].max(): + kwh_impact_table.loc[i, col] = previous_phase[col].max() + + from backend.ml_models.AnnualBillSavings import AnnualBillSavings # We adjust the predictions with the UCL model for k in ["heating", "hotwater"]: kwh_impact_table[f"adjusted_{k}"] = kwh_impact_table[f"predictions_{k}"].apply( @@ -741,13 +764,124 @@ async def trigger_plan(body: PlanTriggerRequest): ) ) - kwh_impact_table["heating_fuel"] = property_instance.heating_energy_source - kwh_impact_table["hotwater_fuel"] = property_instance.hot_water_energy_source + ASHP_COP = 3 + descriptions_to_fuel_types = { + "Air source heat pump, radiators, electric": {"fuel": "Electricity", "cop": ASHP_COP}, + "Boiler and radiators, mains gas": {"fuel": 'Natural Gas', "cop": 0.9} + } + + def map_descriptions_to_fuel(heating_description, hotwater_description): + mapped = descriptions_to_fuel_types[heating_description] + heating_fuel = mapped["fuel"] + + if hotwater_description == "From main system": + return { + "heating_fuel_type": heating_fuel, "hotwater_fuel_type": heating_fuel, + "heating_cop": mapped["cop"], "hotwater_cop": mapped["cop"] + } + + raise NotImplementedError("Implement me") + + # For heating system recommendations, this could result in a fuel type change so we reflect that + fuel_mapping = pd.DataFrame([ + { + "id": epc["id"], + **map_descriptions_to_fuel(epc["mainheat-description"], epc["hotwater-description"]) + } for epc in property_instance.updated_simulation_epcs + ]) + + fuel_mapping = pd.concat( + [ + pd.DataFrame( + [ + { + "id": starting_dummy_id_value, + **map_descriptions_to_fuel( + property_instance.data["mainheat-description"], + property_instance.data["hotwater-description"] + ) + } + ] + ), + fuel_mapping + ] + ) + + kwh_impact_table = kwh_impact_table.merge( + fuel_mapping, how="left", on="id" + ).sort_values(["phase", "recommendation_id"], ascending=True).reset_index(drop=True) + + kwh_impact_table["heating_fuel_type"] = np.where( + kwh_impact_table["id"] == starting_dummy_id_value, + property_instance.heating_energy_source, + kwh_impact_table["heating_fuel_type"] + ) + + kwh_impact_table["hotwater_fuel_type"] = np.where( + kwh_impact_table["id"] == starting_dummy_id_value, + property_instance.hot_water_energy_source, + kwh_impact_table["hotwater_fuel_type"] + ) + + def calculate_recommendation_fuel_cost(kwh, fuel, cop): + if fuel == "Electricity": + return (kwh / cop) * AnnualBillSavings.ELECTRICITY_PRICE_CAP + + if fuel == "Natural Gas": + return (kwh / cop) * AnnualBillSavings.GAS_PRICE_CAP + + # We now calculate the fuel cost + for k in ["heating", "hotwater"]: + kwh_impact_table[f"{k}_cost"] = kwh_impact_table.apply( + lambda x: calculate_recommendation_fuel_cost( + x[f"adjusted_{k}"], x[f"{k}_fuel_type"], x[f"{k}_cop"] + ), axis=1 + ) + + # TODO: The impact of remapping EPC is huge! # We now deduce if any of the recommendations result in a change of fuel type for recs in property_recommendations: for rec in recs: - print(rec["description_simulation"]) + if rec["type"] == "mechanical_ventilation": + continue + + rec_impact = kwh_impact_table[kwh_impact_table["recommendation_id"] == rec["recommendation_id"]] + prevous_phase_id = (rec["phase"] - 1) if (rec["phase"] > 0) else starting_dummy_id_value + previous_phase_impact = kwh_impact_table[kwh_impact_table["phase"] == prevous_phase_id] + + if rec["type"] == "solar_pv": + rec["kwh_savings"] = rec_impact["solar_kwh_savings"].values[0] + rec["energy_cost_savings"] = ( + rec_impact["solar_kwh_savings"].values[0] * AnnualBillSavings.ELECTRICITY_PRICE_CAP + ) + continue + + heating_kwh_savings = ( + previous_phase_impact["adjusted_heating"].mean() - rec_impact["adjusted_heating"].values[0] + ) + heating_cost_savings = ( + previous_phase_impact["heating_cost"].mean() - rec_impact["heating_cost"].values[0] + ) + + hotwater_kwh_savings = ( + previous_phase_impact["adjusted_hotwater"].mean() - rec_impact["adjusted_hotwater"].values[0] + ) + hotwater_host = ( + previous_phase_impact["hotwater_cost"].mean() - rec_impact["hotwater_cost"].values[0] + ) + + total_kwh_savings = heating_kwh_savings + hotwater_kwh_savings + energy_cost_savings = heating_cost_savings + hotwater_host + + if rec["type"] == "lighting": + # In this case, we should probably just SKIP but check when we have one! + raise Exception("Implement me 3") + + rec["kwh_savings"] = total_kwh_savings + rec["energy_cost_savings"] = energy_cost_savings + + # TODO: Given the default recommendations, calcualte a total kwh and cost saving for the property!!! # Insert the predictions into the recommendations and run the optimiser # TODO: If a recommendation has a negative impact on SAP, we should remove it - this seems to have become a