diff --git a/backend/Property.py b/backend/Property.py index 23e885d1..609a9d75 100644 --- a/backend/Property.py +++ b/backend/Property.py @@ -1302,7 +1302,8 @@ class Property: # If there is no existing solar PV, the photo-supply field will be None or a missing value # We use inspections data to tell us this - if self.inspections: + + if getattr(self.inspections, "roof_orientation", None): has_no_existing_solar_pv = self.inspections.roof_orientation.value not in [ "already has solar pv", "roof too small", "no roof" ] diff --git a/backend/app/assumptions.py b/backend/app/assumptions.py index a0234f75..37d9164e 100644 --- a/backend/app/assumptions.py +++ b/backend/app/assumptions.py @@ -77,7 +77,8 @@ DESCRIPTIONS_TO_FUEL_TYPES = { "Electric ceiling heating, electric": {"fuel": "Electricity", "cop": 1}, "Air source heat pump, warm air, electric": { "fuel": "Electricity", "cop": AVERAGE_ASHP_EFFICIENCY / 100 - } + }, + "Electric heat pump for water heating only": {"fuel": "Electricity", "cop": 1}, } # These are the measure types where if there is a ventilation recommendation, we force the inclusion of it diff --git a/backend/engine/engine.py b/backend/engine/engine.py index 3b90f623..fc620388 100644 --- a/backend/engine/engine.py +++ b/backend/engine/engine.py @@ -911,7 +911,8 @@ async def model_engine(body: PlanTriggerRequest): housing_type=body.housing_type, budget=body.budget, target_gain=gain, - funding=funding + funding=funding, + work_package=eco_packages[p.id][2] ) # Given the solutions we select the optimal one @@ -944,8 +945,19 @@ async def model_engine(body: PlanTriggerRequest): # This is the list of measures that we will recommend scheme = optimal_solution["scheme"] - funded_measures = optimal_solution["items"] if scheme != "none" else [] - solution = optimal_solution["items"] + optimal_solution["unfunded_items"] + + # We create this full list of selected measures, which is used in the next section for setting + # default measures + solution = deepcopy(optimal_solution["items"]) + deepcopy(optimal_solution["unfunded_items"]) + funded_measures = deepcopy(optimal_solution["items"]) if scheme != "none" else [] + unfunded_measures = deepcopy(optimal_solution["unfunded_items"]) + # If we have an EPC D + HHRSH project, we move HHRSH out of funded measures + if eco_packages.get(p.id)[2] == "solar_hhrsh_eco4" and p.data["current-energy-rating"] == "D": + unfunded_measures.extend( + [x for x in funded_measures if x["type"] == "high_heat_retention_storage_heaters"] + ) + funded_measures = [x for x in funded_measures if x["type"] != "high_heat_retention_storage_heaters"] + # This is the total amount of funding that the project will produce (EXCLUDING uplifts) (£) project_funding = optimal_solution["full_project_funding"] if scheme == "eco4" else \ optimal_solution["partial_project_funding"] diff --git a/recommendations/optimiser/funding_optimiser.py b/recommendations/optimiser/funding_optimiser.py index 6a0b1d0c..f9fbbfd6 100644 --- a/recommendations/optimiser/funding_optimiser.py +++ b/recommendations/optimiser/funding_optimiser.py @@ -198,7 +198,25 @@ def _ensure_unfunded_costs(groups): return groups -def optimise_with_funding_paths(p, input_measures, housing_type, funding: Funding, budget=None, target_gain=None): +def _get_already_installed_gain(selected_measures, needs_pre_eco_hhrsh_upgrade): + """ + Calculate already installed gain, with special case for pre-ECO4 HHRSH upgrade. + :param selected_measures: List of selected measures + :param needs_pre_eco_hhrsh_upgrade: Boolean indicating if pre-ECO4 HHRSH upgrade is needed + :return: + """ + if needs_pre_eco_hhrsh_upgrade: + return sum( + [x["gain"] for x in selected_measures if + x["already_installed"] or x["type"] == "high_heat_retention_storage_heaters"] + ) + + return sum([x["gain"] for x in selected_measures if x["already_installed"]]) + + +def optimise_with_funding_paths( + p, input_measures, housing_type, funding: Funding, budget=None, target_gain=None, work_package=None +): """ run_optimizer(sub_measures, budget, target_gain) -> (picked_options, sub_cost, sub_gain) """ @@ -227,7 +245,9 @@ def optimise_with_funding_paths(p, input_measures, housing_type, funding: Fundin }) # This function will filter down on innovation measures if we are social EPC D - funding_paths, optimisation_input_measures = make_funding_paths(p, input_measures, housing_type, funding) + funding_paths, optimisation_input_measures = make_funding_paths( + p, input_measures, housing_type, funding, work_package + ) # We now produce a fabric only path for ECO4 # We add in generic insulation funding paths (where there is no fixed measure) @@ -244,6 +264,10 @@ def optimise_with_funding_paths(p, input_measures, housing_type, funding: Fundin ] + funding_paths ) + needs_pre_eco_hhrsh_upgrade = ( + (p.data["current-energy-rating"] == "D") and work_package == "solar_hhrsh_eco4" + ) + for path_spec in funding_paths: # ECO4 fabric only path = special case @@ -281,8 +305,11 @@ def optimise_with_funding_paths(p, input_measures, housing_type, funding: Fundin scheme = _path_scheme([path_spec]) - # We sum of gain, for already installed measures - already_installed_gain = sum([x["gain"] for x in picked if x["already_installed"]]) + # We sum of gain, for already installed measures. In this, we also include HHRSH, when we have + # an EPC D property that needs HHRSH but HHRSH isn't an eligible measure + already_installed_gain = _get_already_installed_gain( + picked, needs_pre_eco_hhrsh_upgrade + ) solutions.append( { @@ -422,7 +449,11 @@ def optimise_with_funding_paths(p, input_measures, housing_type, funding: Fundin total_gain += unfunded_gain # We now grab the "already installed gain" - already_installed_gain = sum([x["gain"] for x in total_picks if x["already_installed"]]) + # We sum of gain, for already installed measures. In this, we also include HHRSH, when we have + # an EPC D property that needs HHRSH but HHRSH isn't an eligible measure + already_installed_gain = _get_already_installed_gain( + total_picks, needs_pre_eco_hhrsh_upgrade + ) solutions.append({ "fixed_ids": fixed_ids, @@ -837,7 +868,7 @@ def _make_generic_gbis_funding_paths(input_gbis_measures, funding_paths): return funding_paths + gbis_funding_paths -def make_funding_paths(p, input_measures, housing_type, funding: Funding): +def make_funding_paths(p, input_measures, housing_type, funding: Funding, work_package=None): """ This function generates funding paths based on the input measures and the tenure of the property. It checks for the presence of specific measures and creates paths that include necessary insulation measures @@ -848,6 +879,8 @@ def make_funding_paths(p, input_measures, housing_type, funding: Funding): :param input_measures: :param housing_type: :param funding: The funding object that provides methods to check eligibility and calculate funding. + :param work_package: Optional work package information. We handle the case of an EPC D property needing a heating + upgrade, where the heating upgrade needs to be conducted before the solar PV work :return: """ @@ -890,6 +923,12 @@ def make_funding_paths(p, input_measures, housing_type, funding: Funding): group_of_innovation_measures = [] group_of_gbis_innovation_measures = [] for measure in measures: + + if measure["type"] == "high_heat_retention_storage_heaters" and work_package == "solar_hhrsh_eco4": + # With this work type, if the property is EPC D and doesn't have an eligible heating system + # we install HHRSH as a pre-requisite measure, before the ECO4 project if complete. + group_of_innovation_measures.append(measure) + if measure["innovation_uplift"] or measure["type"] in remaining_insulation_type or measure[ "already_installed"]: group_of_innovation_measures.append(measure) @@ -906,7 +945,7 @@ def make_funding_paths(p, input_measures, housing_type, funding: Funding): input_gbis_measures_innovation.extend(group_of_gbis_innovation_measures) funding_paths = _make_solar_heating_funding_paths( - p, input_measures_innovation, funding_paths, remaining_insulation_type, housing_type, funding + p, input_measures_innovation, funding_paths, remaining_insulation_type, housing_type, funding, ) # Can only be innovation GBIS measures