added new pricing for air source heat pump

This commit is contained in:
Khalim Conn-Kowlessar 2025-12-24 12:58:24 +08:00
parent b881c8358e
commit aa515a9797
2 changed files with 79 additions and 81 deletions

View file

@ -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,
}

View file

@ -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)):