diff --git a/recommendations/Costs.py b/recommendations/Costs.py index b005ab69..86062433 100644 --- a/recommendations/Costs.py +++ b/recommendations/Costs.py @@ -1,6 +1,7 @@ import numpy as np from recommendations.county_to_region import county_to_region_map from utils.logger import setup_logger +from backend.ml_models.AnnualBillSavings import AnnualBillSavings logger = setup_logger() @@ -21,25 +22,6 @@ regional_labour_variations = [ {"Region": "Northern Ireland", "Adjustment_Factor": 0.76} ] -# This data is based on the MCS database - taken the figures for June 2024 -MCS_SOLAR_PV_COST_DATA = { - "last_updated": "2024-07-10", - "average_cost_per_kwh": 1825, - "average_cost_per_kwh-Outer London": 1950, - "average_cost_per_kwh-Inner London": 1950, - "average_cost_per_kwh-South East England": 1966, - "average_cost_per_kwh-South West England": 1864, - "average_cost_per_kwh-East of England": 1719, - "average_cost_per_kwh-East Midlands": 1730, - "average_cost_per_kwh-West Midlands": 1789, - "average_cost_per_kwh-North East England": 1872, - "average_cost_per_kwh-North West England": 1860, - "average_cost_per_kwh-Yorkshire and the Humber": 1789, - "average_cost_per_kwh-Wales": 1676, - "average_cost_per_kwh-Scotland": 1781, - "average_cost_per_kwh-Northern Ireland": 1347, -} - # Installers are now working with 435 watt panels PANEL_SIZE = 0.435 @@ -61,47 +43,40 @@ INSTALLER_SOLAR_COSTS = [ {'n_panels': 18, 'array_kwp': 18 * PANEL_SIZE, 'cost': 6792.57, 'installer': 'CEG'} ] +# These are costs we received from CRG, for pricing up air source heat pumps +# These are costs that we have been provided from CRG specifically for air source heat pumps +ASHP_SMALL_SYSTEM_COST = 8812.92 # 4.8 to 8.5, based on their pricing +ASHP_LARGE_SYSTEM_COST = 11053.25 +ASHP_SECURITY = 455.00 +ASHP_WALL_BRACKET = 574.17 +ASHP_DISTRIBUTION_SYSTEM_COSTS = [ + {"n_radiators": 4, "cost": 3380.00}, + {"n_radiators": 5, "cost": 3607.50}, + {"n_radiators": 6, "cost": 4116.67}, + {"n_radiators": 7, "cost": 4647.50}, + {"n_radiators": 8, "cost": 5200.00}, + {"n_radiators": 9, "cost": 5730.83}, + {"n_radiators": 10, "cost": 6283.33}, + {"n_radiators": 11, "cost": 6857.50}, + {"n_radiators": 12, "cost": 7431.67}, + {"n_radiators": 13, "cost": 8016.67}, + {"n_radiators": 14, "cost": 8612.50}, + {"n_radiators": 15, "cost": 9219.17}, + {"n_radiators": 16, "cost": 9804.17}, + {"n_radiators": 17, "cost": 10389.17}, +] +ASHP_CYLINDER_COSTS = [ + {"capacity_l": 120, "cost": 3318.25}, + {"capacity_l": 180, "cost": 3480.75}, + {"capacity_l": 200, "cost": 3853.42}, + {"capacity_l": 250, "cost": 3961.75}, +] + # CEG uses use Solshare as an inverter to provide solar PV to multiple flats. This costs £7500 for the inverter alone # https://midsummerwholesale.co.uk/buy/solshare INSTALLER_SOLAR_PV_INVERTER_COST = 7500 INSTALLER_SOLAR_PV_INVERTER_LABOUR_COST = 500 # Just a rough guess to labour costs -# INSTALLER_SCAFFOLDING_COSTS = [ -# {'stories': 1, 'description': '1 Story Scaffold', 'cost': 531.00, 'installer': 'CEG'}, -# {'stories': 2, 'description': '2 Story Scaffold', 'cost': 841.00, 'installer': 'CEG'}, -# {'stories': 3, 'description': '3 Story Scaffold', 'cost': 1077.00, 'installer': 'CEG'} -# ] - -# This data is based on the MCS database, We use the larger figure between the 2023 and 2024 average, -# to be conservative -MCS_AIR_SOURCE_HEAT_PUMP_COST_DATA = { - "Outer London": 13220, - "Inner London": 13220, - "South East England": 13547, - "South West England": 12776, - "East of England": 12585, - "East Midlands": 12239, - "West Midlands": 13182, - "North East England": 11829, - "North West England": 11714, - "Yorkshire and the Humber": 11919, - "Wales": 13701, - "Scotland": 12586, - "Northern Ireland": 12000, # There are hardly any air source heat pump installs going on in Northern Ireland -} - -INSTALLER_ASHP_COSTS = [ - {'capacity_kw': 5.0, 'brand': 'Mitsubishi', 'tank_size_liters': 150, 'cost': 10149.53, 'installer': 'CEG'}, - {'capacity_kw': 6.0, 'brand': 'Mitsubishi', 'tank_size_liters': 170, 'cost': 10823.48, 'installer': 'CEG'}, - {'capacity_kw': 8.5, 'brand': 'Mitsubishi', 'tank_size_liters': 200, 'cost': 11312.43, 'installer': 'CEG'}, - {'capacity_kw': 11.2, 'brand': 'Mitsubishi', 'tank_size_liters': 250, 'cost': 12156.75, 'installer': 'CEG'}, - {'capacity_kw': 14.0, 'brand': 'Mitsubishi', 'tank_size_liters': 300, 'cost': 14405.54, 'installer': 'CEG'}, - {'capacity_kw': 14.0, 'brand': 'Mitsubishi', 'tank_size_liters': 300, 'cost': 14405.54, 'installer': 'CEG'}, - {'capacity_kw': 17.0, 'brand': 'Grant', 'tank_size_liters': 300, 'cost': 14445.00, 'installer': 'CEG'}, - {'capacity_kw': 20.0, 'brand': 'Ecoforest', 'tank_size_liters': 400, 'cost': 21189.41, 'installer': 'CEG'}, - {'capacity_kw': None, 'brand': '2 x cascaded ASHPs', 'tank_size_liters': 500, 'cost': 22950.00, 'installer': 'CEG'} -] - INSTALLER_SOLAR_BATTERY_COSTS = [ {'capacity_kwh': 5, 'description': 'Battery Add on', 'cost': 3769.89, 'installer': 'JJC'}, # {'capacity_kwh': 10, 'description': 'Battery Add on', 'cost': 4300.00, 'installer': 'CEG'}, @@ -368,7 +343,7 @@ class Costs: total_cost = total_cost + labour_cost - total_cost = round(total_cost, 2) + total_cost = round(total_cost) return { "total": total_cost, @@ -853,32 +828,55 @@ class Costs: "labour_days": labour_days, } - def air_source_heat_pump(self, ashp_size): - """ - Based on the region and type of property, this function will produce a cost estimation for an air source heat - pump. This cost will include the boiler upgrade scheme grant - - """ - # This is the average cost of a project, we'll add some additional contingency - - if ashp_size is None: - cost = [x for x in INSTALLER_ASHP_COSTS if x["capacity_kw"] is None][0]["cost"] + @staticmethod + def _select_cylinder_capacity(occupants: float): + if occupants <= 2: + return 120 + elif occupants <= 3: + return 180 + elif occupants <= 4: + return 200 else: - cost = [x for x in INSTALLER_ASHP_COSTS if x][0]["cost"] + return 250 - # The costs from installers exclude VAT - vat = cost * self.VAT_RATE - cost = cost + vat + def air_source_heat_pump(self, ashp_size: float, number_heated_rooms: int, total_floor_area: float) -> dict: + """ + We produce a cost estimation for an air source heat pump, based on costs we have received from installers. - # We assume 5 days installation - labour_days = 5 - labour_hours = labour_days * 8 + """ + + system_cost = ( + (ASHP_SMALL_SYSTEM_COST if ashp_size <= 8.5 else ASHP_LARGE_SYSTEM_COST) + ASHP_SECURITY + ASHP_WALL_BRACKET + ) + + available_n_rads = [x["n_radiators"] for x in ASHP_DISTRIBUTION_SYSTEM_COSTS] + if number_heated_rooms < min(available_n_rads): + # We use the smallest value + rads_to_use = min(available_n_rads) + elif number_heated_rooms > max(available_n_rads): + # We use the largest value + rads_to_use = max(available_n_rads) + else: + rads_to_use = int(number_heated_rooms) + + distribution_system_cost = [ + x for x in ASHP_DISTRIBUTION_SYSTEM_COSTS if x["n_radiators"] == rads_to_use + ][0]["cost"] + + # Cylinder cost + est_n_occupants = AnnualBillSavings.calculate_occupants(total_floor_area) + cylinder_capacity = self._select_cylinder_capacity(est_n_occupants) + cylinder_cost = [ + x for x in ASHP_CYLINDER_COSTS if x["capacity_l"] == cylinder_capacity + ][0]["cost"] + + total = system_cost + distribution_system_cost + cylinder_cost return { - "total": cost, - "contingency": cost * self.CONTINGENCIES["air_source_heat_pump"], + "total": total, + "contingency": total * self.CONTINGENCIES["air_source_heat_pump"], "contingency_rate": self.CONTINGENCIES["air_source_heat_pump"], - "vat": vat, - "labour_hours": labour_hours, - "labour_days": labour_days, + "vat": 0, + "labour_hours": 80, + "labour_days": 10, } diff --git a/recommendations/HeatingRecommender.py b/recommendations/HeatingRecommender.py index c5aa8b38..409f9ec6 100644 --- a/recommendations/HeatingRecommender.py +++ b/recommendations/HeatingRecommender.py @@ -526,14 +526,14 @@ class HeatingRecommender: # 1) Best available path: HLP → direct peak if heat_loss_parameter_W_per_m2K is not None: peak_kw = heat_loss_parameter_W_per_m2K * floor_area_m2 * ΔT / 1000.0 - return (peak_kw, peak_kw) # no range needed + return peak_kw, peak_kw # no range needed # 2) Second-best: space-heating demand → HDD method if space_heat_kwh_per_m2_yr is not None: annual_space_kwh = space_heat_kwh_per_m2_yr * floor_area_m2 Htot = annual_space_kwh * 1000.0 / (hdd_base_dd * 24.0) # W/K peak_kw = Htot * ΔT / 1000.0 - return (peak_kw, peak_kw) + return peak_kw, peak_kw # 3) Minimal inputs: primary energy + assumed fraction → range assert epc_primary_kwh_per_m2_yr is not None @@ -547,7 +547,7 @@ class HeatingRecommender: low = to_peak(space_heat_fraction_range[0]) high = to_peak(space_heat_fraction_range[1]) - return (low, high) + return low, high @staticmethod def pick_model(peak_kw_range, models_kw=(5, 6, 8.5, 11.2, 14, 17, 20)):