From 6bd66d83f5f6964206ffe623f4096a749af3176e Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 19 Aug 2024 18:59:21 +0100 Subject: [PATCH] handling odd heating systems --- backend/Property.py | 27 ++++++++++++++----- backend/app/assumptions.py | 36 +++++++++++++++++++++++++ backend/app/plan/router.py | 8 +++++- backend/ml_models/AnnualBillSavings.py | 17 +++++++++++- recommendations/Recommendations.py | 37 +------------------------- recommendations/rdsap_tables.py | 8 +++--- 6 files changed, 85 insertions(+), 48 deletions(-) diff --git a/backend/Property.py b/backend/Property.py index f8b40872..5bca434f 100644 --- a/backend/Property.py +++ b/backend/Property.py @@ -9,7 +9,6 @@ from etl.epc.Dataset import TrainingDataset from etl.epc.Record import EPCRecord from etl.epc.settings import LATEST_FIELD, MANDATORY_FIXED_FEATURES from etl.epc_clean.epc_attributes.all_cleaners import all_cleaner_map -from etl.solar.SolarPhotoSupply import SolarPhotoSupply from utils.logger import setup_logger from utils.s3 import read_dataframe_from_s3_parquet from etl.epc.settings import DATA_ANOMALY_MATCHES @@ -18,11 +17,11 @@ from recommendations.recommendation_utils import ( estimate_perimeter, get_wall_type, estimate_external_wall_area, - esimtate_pitched_roof_area, estimate_windows, ) from backend.ml_models.AnnualBillSavings import AnnualBillSavings from backend.app.utils import sap_to_epc +import backend.app.assumptions as assumptions ENVIRONMENT = os.environ.get("ENVIRONMENT", "dev") DATA_BUCKET = os.environ.get( @@ -1184,11 +1183,20 @@ class Property: if set(self.heating_energy_source) == {'Electricity', 'Natural Gas'}: # It means they have mixed heating so we take the primary one, based on main fuel - if self.main_fuel["clean_description"] == "Mains gas not community": + # This will probably happen in the case of an extension + if self.main_fuel["clean_description"] in ["Mains gas not community", "Mains gas community"]: self.heating_energy_source = ['Natural Gas'] else: self.heating_energy_source = ['Electricity'] + if set(self.heating_energy_source) == {'Natural Gas', 'Wood Logs'}: + # It means they have mixed heating so we take the primary one, based on main fuel + # This will probably happen in the case of an extension + if self.main_fuel["clean_description"] in ["Mains gas not community", "Mains gas community"]: + self.heating_energy_source = ['Natural Gas'] + else: + self.heating_energy_source = ['Wood Logs'] + if len(self.heating_energy_source) == 0 or len(self.heating_energy_source) > 1: raise Exception("Investigate me") @@ -1216,6 +1224,10 @@ class Property: if fuel in ['Main System', "Community Scheme"]: self.hot_water_energy_source = self.heating_energy_source + elif fuel in ['Secondary System']: + # Check the secondary heating system + secondary_heating = self.data["secondheat-description"] + self.hot_water_energy_source = assumptions.DESCRIPTIONS_TO_FUEL_TYPES[secondary_heating]["fuel"] else: raise Exception("Investiage me") @@ -1273,7 +1285,10 @@ class Property: return self.current_energy_consumption # If the property currently has an electric boiler, it will still benefit from the ASHP efficiency gain - remap_fuel_sources = ["Natural Gas", "LPG", "Wood Logs", "Oil", "Electricity"] + remap_fuel_sources = [ + "Natural Gas", "LPG", "Wood Logs", "Oil", "Electricity", "Coal", "Smokeless Fuel", + "Natural Gas + Solar Thermal", "Anthracite", "Wood Pellets", + ] heating_energy_source = self.heating_energy_source hot_water_energy_source = self.hot_water_energy_source @@ -1281,11 +1296,11 @@ class Property: hotwater_consumption = self.energy_consumption_estimates["unadjusted"]["hot_water"] if (heating_energy_source not in remap_fuel_sources) or ( - hot_water_energy_source not in remap_fuel_sources + hot_water_energy_source not in remap_fuel_sources + ["Electricity + Solar Thermal"] ): raise NotImplementedError("Have not implemented estimating electrical consumption for this fuel type") - if heating_energy_source in ["Natural Gas", "LPG", "Wood Logs"]: + if heating_energy_source in remap_fuel_sources: # Adjust the heating consumption to reflect the expected efficiency of an ASHP heating_consumption = heating_consumption / (assumed_ashp_efficiency / 100) diff --git a/backend/app/assumptions.py b/backend/app/assumptions.py index f0ddf868..5f8cb85c 100644 --- a/backend/app/assumptions.py +++ b/backend/app/assumptions.py @@ -6,3 +6,39 @@ AVERAGE_ASHP_EFFICIENCY = 300 # Conservative estimate of the proportion of electricity that will be consumed, whereas the rest will # be exported SOLAR_CONSUMPTION_PROPORTION = 0.5 + +DESCRIPTIONS_TO_FUEL_TYPES = { + "Air source heat pump, radiators, electric": { + "fuel": "Electricity", "cop": AVERAGE_ASHP_EFFICIENCY / 100 + }, + "Boiler and radiators, mains gas": {"fuel": 'Natural Gas', "cop": 0.9}, + 'Electric storage heaters': {"fuel": 'Electricity', "cop": 1}, + "Electric immersion, off-peak": {"fuel": 'Electricity', "cop": 1}, + "Electric storage heaters, radiators": {"fuel": 'Electricity', "cop": 1}, + "Room heaters, electric": {"fuel": 'Electricity', "cop": 1}, + "Electric immersion, standard tariff": {"fuel": 'Electricity', "cop": 1}, + "Portable electric heaters assumed for most rooms": {"fuel": 'Electricity', "cop": 1}, + "Boiler and radiators, LPG": {"fuel": 'LPG', "cop": 0.9}, + "Room heaters, dual fuel (mineral and wood)": {"fuel": 'Wood Logs', "cop": 1}, + "Room heaters, mains gas": {"fuel": 'Natural Gas', "cop": 0.9}, + "Warm air, mains gas": {"fuel": 'Natural Gas', "cop": 0.9}, + "Boiler, mains gas": {"fuel": 'Natural Gas', "cop": 0.9}, + "Gas multipoint": {"fuel": "Natural Gas", "cop": 0.9}, + "Warm air, Electricaire": {"fuel": "Electricity", "cop": 1}, + "Gas boiler/circulator": {"fuel": "Natural Gas", "cop": 0.9}, + "Boiler and underfloor heating, mains gas": {"fuel": "Natural Gas", "cop": 0.9}, + "No system present: electric heaters assumed": {"fuel": "Electricity", "cop": 1}, + "Electric instantaneous at point of use": {"fuel": "Electricity", "cop": 1}, + "Boiler and radiators, oil": {"fuel": "Oil", "cop": 0.9}, + "Electric storage heaters, Electric storage heaters": {"fuel": "Electricity", "cop": 1}, + "Boiler and radiators, electric": {"fuel": "Electricity", "cop": 0.9}, + "Gas boiler/circulator, no cylinder thermostat": {"fuel": "Natural Gas", "cop": 0.9}, + "Boiler and radiators, dual fuel (mineral and wood)": {"fuel": "Wood Logs", "cop": 0.9}, + "Electric immersion, standard tariff, plus solar": {"fuel": "Electricity + Solar Thermal", "cop": 1}, + "From main system, flue gas heat recovery": {"fuel": "Natural Gas", "cop": 0.9}, + "Electric underfloor heating": {"fuel": "Electricity", "cop": 1}, + "No system present: electric immersion assumed": {"fuel": "Electricity", "cop": 1}, + "Air source heat pump, underfloor, electric": { + "fuel": "Electricity", "cop": AVERAGE_ASHP_EFFICIENCY / 100 + }, +} diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py index 23d3f5d2..f6e98918 100644 --- a/backend/app/plan/router.py +++ b/backend/app/plan/router.py @@ -508,7 +508,6 @@ async def trigger_plan(body: PlanTriggerRequest): logger.info("Getting spatial data") input_properties = OpenUprnClient.set_spatial_data(input_properties, bucket_name=get_settings().DATA_BUCKET) - logger.info("Setting property features") [p.set_features(cleaned=cleaned, kwh_client=kwh_client, kwh_predictions=kwh_preds) for p in input_properties] logger.info("Performing solar analysis") @@ -520,6 +519,13 @@ async def trigger_plan(body: PlanTriggerRequest): # basic estimate of roof area # TODO: Debug this + for p in input_properties: + if p.uprn in [10002634631, 100031601798, 10009574286, 10007366417]: + continue + p.estimate_electrical_consumption( + assumed_ashp_efficiency=assumptions.AVERAGE_ASHP_EFFICIENCY, exclusions=body.exclusions + ) + building_ids = [ { "building_id": p.building_id, diff --git a/backend/ml_models/AnnualBillSavings.py b/backend/ml_models/AnnualBillSavings.py index 13c9e0a5..d72feed7 100644 --- a/backend/ml_models/AnnualBillSavings.py +++ b/backend/ml_models/AnnualBillSavings.py @@ -274,7 +274,7 @@ class AnnualBillSavings: ) return (kwh / cop) * cost_per_kwh - if fuel == "Wood Logs": + if fuel in ["Wood Logs", "Wood Pellets"]: price_data = cls.FUEL_DATA[cls.FUEL_DATA["Fuel"] == "Pellets (Bagged)"].squeeze() cost_per_kwh = cls.cost_per_kwh( price_data["Price (p)"], price_data["Energy Content, Net Calorific value (kWh/unit)"] @@ -296,4 +296,19 @@ class AnnualBillSavings: ) return (kwh / cop) * cost_per_kwh + if fuel in ["Smokeless Fuel", "Anthracite"]: + price_data = cls.FUEL_DATA[cls.FUEL_DATA["Fuel"] == "Smokeless fuel"].squeeze() + cost_per_kwh = cls.cost_per_kwh( + price_data["Price (p)"], price_data["Energy Content, Net Calorific value (kWh/unit)"] + ) + return (kwh / cop) * cost_per_kwh + + # We use coal's values for + if fuel == "Coal": + price_data = cls.FUEL_DATA[cls.FUEL_DATA["Fuel"] == "Coal"].squeeze() + cost_per_kwh = cls.cost_per_kwh( + price_data["Price (p)"], price_data["Energy Content, Net Calorific value (kWh/unit)"] + ) + return (kwh / cop) * cost_per_kwh + raise Exception("Fuel not recognised") diff --git a/recommendations/Recommendations.py b/recommendations/Recommendations.py index fef7472c..4f75b30b 100644 --- a/recommendations/Recommendations.py +++ b/recommendations/Recommendations.py @@ -19,41 +19,6 @@ from backend.apis.GoogleSolarApi import GoogleSolarApi import backend.app.assumptions as assumptions ASHP_COP = 3 -DESCRIPTIONS_TO_FUEL_TYPES = { - "Air source heat pump, radiators, electric": { - "fuel": "Electricity", "cop": assumptions.AVERAGE_ASHP_EFFICIENCY / 100 - }, - "Boiler and radiators, mains gas": {"fuel": 'Natural Gas', "cop": 0.9}, - 'Electric storage heaters': {"fuel": 'Electricity', "cop": 1}, - "Electric immersion, off-peak": {"fuel": 'Electricity', "cop": 1}, - "Electric storage heaters, radiators": {"fuel": 'Electricity', "cop": 1}, - "Room heaters, electric": {"fuel": 'Electricity', "cop": 1}, - "Electric immersion, standard tariff": {"fuel": 'Electricity', "cop": 1}, - "Portable electric heaters assumed for most rooms": {"fuel": 'Electricity', "cop": 1}, - "Boiler and radiators, LPG": {"fuel": 'LPG', "cop": 0.9}, - "Room heaters, dual fuel (mineral and wood)": {"fuel": 'Wood Logs', "cop": 1}, - "Room heaters, mains gas": {"fuel": 'Natural Gas', "cop": 0.9}, - "Warm air, mains gas": {"fuel": 'Natural Gas', "cop": 0.9}, - "Boiler, mains gas": {"fuel": 'Natural Gas', "cop": 0.9}, - "Gas multipoint": {"fuel": "Natural Gas", "cop": 0.9}, - "Warm air, Electricaire": {"fuel": "Electricity", "cop": 1}, - "Gas boiler/circulator": {"fuel": "Natural Gas", "cop": 0.9}, - "Boiler and underfloor heating, mains gas": {"fuel": "Natural Gas", "cop": 0.9}, - "No system present: electric heaters assumed": {"fuel": "Electricity", "cop": 1}, - "Electric instantaneous at point of use": {"fuel": "Electricity", "cop": 1}, - "Boiler and radiators, oil": {"fuel": "Oil", "cop": 0.9}, - "Electric storage heaters, Electric storage heaters": {"fuel": "Electricity", "cop": 1}, - "Boiler and radiators, electric": {"fuel": "Electricity", "cop": 0.9}, - "Gas boiler/circulator, no cylinder thermostat": {"fuel": "Natural Gas", "cop": 0.9}, - "Boiler and radiators, dual fuel (mineral and wood)": {"fuel": "Wood Logs", "cop": 0.9}, - "Electric immersion, standard tariff, plus solar": {"fuel": "Electricity + Solar Thermal", "cop": 1}, - "From main system, flue gas heat recovery": {"fuel": "Natural Gas", "cop": 0.9}, - "Electric underfloor heating": {"fuel": "Electricity", "cop": 1}, - "No system present: electric immersion assumed": {"fuel": "Electricity", "cop": 1}, - "Air source heat pump, underfloor, electric": { - "fuel": "Electricity", "cop": assumptions.AVERAGE_ASHP_EFFICIENCY / 100 - }, -} STARTING_DUMMY_ID_VALUE = -9999 @@ -551,7 +516,7 @@ class Recommendations: } raise NotImplementedError("Handle this case") - mapped = DESCRIPTIONS_TO_FUEL_TYPES[heating_description] + mapped = assumptions.DESCRIPTIONS_TO_FUEL_TYPES[heating_description] heating_fuel = mapped["fuel"] if hotwater_description in [ diff --git a/recommendations/rdsap_tables.py b/recommendations/rdsap_tables.py index 98cda9ab..5110764b 100644 --- a/recommendations/rdsap_tables.py +++ b/recommendations/rdsap_tables.py @@ -514,8 +514,8 @@ FLOOR_LEVEL_MAP = { "top floor": 5, "20+": 20, "21st or above": 21, - **{str(i).zfill(2): i for i in range(0, 21)}, - **{ordinal(i): i for i in range(-1, 21)}, - **{str(i): i for i in range(-1, 21)}, - **{i: i for i in range(-1, 21)}, + **{str(i).zfill(2): i for i in range(0, 51)}, + **{ordinal(i): i for i in range(-1, 51)}, + **{str(i): i for i in range(-1, 51)}, + **{i: i for i in range(-1, 51)}, }