diff --git a/backend/Property.py b/backend/Property.py index d32feebf..5976d8ec 100644 --- a/backend/Property.py +++ b/backend/Property.py @@ -378,10 +378,7 @@ class Property: self.recommendations_scoring_data.append(scoring_dict) - simulation_epc = vars(self.epc_record).copy() - # Insert static values - simulation_epc["lodgement_date"] = simulation_lodgment_date - simulation_epc = {k.replace("_", "-"): v for k, v in simulation_epc.items()} + simulation_epc = self.epc_record.to_dict(case="kebab", source="prepared") types = [x["type"] for x in previous_phase_representatives] if "external_wall_insulation" in types and "internal_wall_insulation" in types: diff --git a/backend/app/utils.py b/backend/app/utils.py index c1ad54f6..eb727f81 100644 --- a/backend/app/utils.py +++ b/backend/app/utils.py @@ -3,6 +3,7 @@ import string import secrets import logging from io import BytesIO +from typing import Optional def setup_logger(log_file=None, level=logging.INFO, overwrite_handler=False): @@ -73,7 +74,7 @@ def sap_to_epc(sap_points: int | float): return "G" -def epc_to_sap_lower_bound(epc: str): +def epc_to_sap_lower_bound(epc: Optional[str]): """ Given an EPC rating, returns the lower bound SAP score required to hit that EPC rating diff --git a/backend/engine/engine.py b/backend/engine/engine.py index e1e45b47..3014f2b3 100644 --- a/backend/engine/engine.py +++ b/backend/engine/engine.py @@ -1076,8 +1076,7 @@ async def model_engine(body: PlanTriggerRequest): property_required_measures, recommendations, p, needs_ventilation ) gain = optimiser_functions.calculate_gain( - body=body, p=p, fixed_gain=fixed_gain, eco_packages=eco_packages, - already_installed_gain=already_installed_sap + body=body, p=p, fixed_gain=fixed_gain, already_installed_gain=already_installed_sap ) # We insert the innovation uplift diff --git a/recommendations/Costs.py b/recommendations/Costs.py index 2bcc67df..f2d43339 100644 --- a/recommendations/Costs.py +++ b/recommendations/Costs.py @@ -858,7 +858,7 @@ class Costs: n_radiators = self._estimate_n_radiators( number_habitable_rooms=n_rooms, total_floor_area=self.property.floor_area, - property_type=self.property.epc_record.property - type, + property_type=self.property.epc_record.property_type, built_form=self.property.epc_record.built_form ) diff --git a/recommendations/Recommendations.py b/recommendations/Recommendations.py index 2d56eda9..77bf78ed 100644 --- a/recommendations/Recommendations.py +++ b/recommendations/Recommendations.py @@ -1,7 +1,7 @@ import pandas as pd import numpy as np from backend.Property import Property -from typing import List, Mapping, Any +from typing import List, Mapping, Any, Optional from itertools import groupby from recommendations.FloorRecommendations import FloorRecommendations from recommendations.WallRecommendations import WallRecommendations @@ -49,7 +49,7 @@ class Recommendations: materials: List, exclusions: List[str] = None, inclusions: List[str] = None, - default_u_values: bool = False, + default_u_values: Optional[bool] = False, ): """ :param property_instance: Instance of the Property class, for the home associated to property_id diff --git a/recommendations/SecondaryHeating.py b/recommendations/SecondaryHeating.py index ef0fc2d2..c2250e1e 100644 --- a/recommendations/SecondaryHeating.py +++ b/recommendations/SecondaryHeating.py @@ -22,10 +22,10 @@ class SecondaryHeating: # No secondary heating system, so no recommendation to remove it return - if self.property.data['number-habitable-rooms'] > self.property.data['number-heated-rooms']: - n_rooms = self.property.data['number-habitable-rooms'] - self.property.data['number-heated-rooms'] + if self.property.epc_record.number_habitable_rooms > self.property.epc_record.number_heated_rooms: + n_rooms = self.property.epc_record.number_habitable_rooms - self.property.epc_record.number_heated_rooms else: - n_rooms = self.property.data["number-heated-rooms"] + n_rooms = self.property.epc_record.number_heated_rooms costs = self.costs.heater_removal(n_rooms=n_rooms) diff --git a/recommendations/optimiser/funding_optimiser.py b/recommendations/optimiser/funding_optimiser.py index a91c05bd..a1040dca 100644 --- a/recommendations/optimiser/funding_optimiser.py +++ b/recommendations/optimiser/funding_optimiser.py @@ -285,7 +285,7 @@ def optimise_with_funding_paths( # We add in generic insulation funding paths (where there is no fixed measure) # Heating controls are only eligible if installed as part of a heating upgrade and so we do not include them # here. We don't have an option if the property is a C or above - if housing_type == "Social" and p.data["current-energy-rating"] not in ["C", "B", "A"]: + if housing_type == "Social" and p.epc_record.current_energy_rating not in ["C", "B", "A"]: funding_paths = ( [ { @@ -297,7 +297,7 @@ def optimise_with_funding_paths( ) needs_pre_eco_hhrsh_upgrade = ( - (p.data["current-energy-rating"] == "D") and work_package == "solar_hhrsh_eco4" + (p.epc_record.current_energy_rating == "D") and work_package == "solar_hhrsh_eco4" ) for path_spec in funding_paths: @@ -306,7 +306,7 @@ def optimise_with_funding_paths( if isinstance(path_spec, dict) and path_spec.get("reference") == "fabric-only:eco4": sub_measures = _filter_measures_by_types(optimisation_input_measures, path_spec["allowed_types"]) # If the property is EPC D and socil, we also include just innovation measures - if housing_type == "Social" and p.data["current-energy-rating"] == "D": + if housing_type == "Social" and p.epc_record.current_energy_rating == "D": # We add in a second option which is just innovation measures sub_measures_innovation = [] for measures in sub_measures: @@ -354,7 +354,7 @@ def optimise_with_funding_paths( "path": path_spec, "scheme": scheme, "is_eligible": _is_eligible_funding_package( - scheme, float(p.data["current-energy-efficiency"]), sub_gain + scheme, p.epc_record.current_energy_efficiency, sub_gain ), "unfunded_items": unfunded_picked, "already_installed_gain": already_installed_gain @@ -500,9 +500,7 @@ def optimise_with_funding_paths( "total_gain": total_gain, "path": path_spec, "scheme": scheme, - "is_eligible": _is_eligible_funding_package( - scheme, int(p.data["current-energy-efficiency"]), total_gain - ), + "is_eligible": _is_eligible_funding_package(scheme, p.epc_record.current_energy_efficiency, total_gain), "unfunded_items": unfunded_picked, "already_installed_gain": already_installed_gain }) @@ -523,7 +521,7 @@ def optimise_with_funding_paths( # logger.info("We have some packages that are fundable but do not meet the target gain") # We now can calculate the project ABS, which subtracts from the cost, but this is only relevant for ECO4 - solutions["starting_sap"] = int(p.data["current-energy-efficiency"]) + solutions["starting_sap"] = p.epc_record.current_energy_efficiency solutions["floor_area"] = p.floor_area solutions["ending_sap"] = solutions["starting_sap"] + solutions["total_gain"] # We flag projects that are including batteries @@ -677,7 +675,7 @@ def optimise_with_scenarios( for x in measures: if x["has_battery"]: x["battery_gain"] = BatterySAPScorer.score( - starting_sap=int(p.data["current-energy-efficiency"]) + target_gain + 1, + starting_sap=p.epc_record.current_energy_efficiency + target_gain + 1, pv_size=x["array_size"] ) x["gain"] += x["battery_gain"] @@ -893,7 +891,7 @@ def append_solution_metrics(solutions, target_gain, p, already_installed_sap=0): # We need the ending SAP, but we'll need to remove the battery SAP uplift first solutions_df["ending_sap_without_battery"] = solutions_df.apply( - lambda x: int(p.data["current-energy-efficiency"]) + already_installed_sap + _get_ending_sap_without_battery(x), + lambda x: p.epc_record.current_energy_efficiency + already_installed_sap + _get_ending_sap_without_battery(x), axis=1 ) @@ -1162,7 +1160,7 @@ def _make_solar_heating_funding_paths( p, input_measures, funding_paths, remaining_insulation_type, housing_type, funding: Funding ): # If a property is private and EPC D or above, it's not eligible - if housing_type == "Private" and p.data["current-energy-rating"] in ["D", "C", "B", "A"]: + if housing_type == "Private" and p.epc_record.current_energy_rating in ["D", "C", "B", "A"]: return funding_paths # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Solar PV with existing eligible heating system @@ -1288,7 +1286,7 @@ def make_funding_paths(p, input_measures, housing_type, funding: Funding, work_p """ # If the property is currently EPC C, there is no funding availability - if p.data["current-energy-rating"] in ["C", "B", "A"]: + if p.epc_record.current_energy_rating in ["C", "B", "A"]: return [], input_measures # We handle the case of minimum insulation requirements. Whenever we have a heating system recommendation, @@ -1316,7 +1314,7 @@ def make_funding_paths(p, input_measures, housing_type, funding: Funding, work_p funding_paths = [] - if housing_type == "Social" and p.data["current-energy-rating"] == "D": + if housing_type == "Social" and p.epc_record.current_energy_rating == "D": # If the property is currently EPC D, we can only include innovation measures or measures to meet the # minimum insulation requirements. We make an exception if we have a measure that is # already installed, specifically a heat pump @@ -1362,7 +1360,7 @@ def make_funding_paths(p, input_measures, housing_type, funding: Funding, work_p # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # 1) The package must include EWI or IWI if the property is private rental sector # We check if we have any EWI or IWI measures available - only for EPC E or below - if p.data["current-energy-rating"] in ["E", "F", "G"]: + if p.epc_record.current_energy_rating in ["E", "F", "G"]: ewi_or_iwi = [{"OR": []}] reference_measures = [] # If we have EWI we add it in diff --git a/recommendations/optimiser/optimiser_functions.py b/recommendations/optimiser/optimiser_functions.py index 6fd70c20..46069a95 100644 --- a/recommendations/optimiser/optimiser_functions.py +++ b/recommendations/optimiser/optimiser_functions.py @@ -207,7 +207,6 @@ def calculate_gain( body: PlanTriggerRequest, p: Property, fixed_gain: float, - eco_packages: None | dict = None, already_installed_gain: float = 0, ) -> float | None: """ @@ -226,7 +225,6 @@ def calculate_gain( Property object with EPC data (must have p.data["current-energy-efficiency"]). fixed_gain : float Total fixed gain from required measures (returned by calculate_fixed_gain). - eco_packages : dict, optional already_installed_gain: float, optional Returns @@ -235,15 +233,8 @@ def calculate_gain( Required SAP gain for EPC, or None for non-EPC goals. """ if body.goal == "Increasing EPC": - current_sap = int(p.data["current-energy-efficiency"]) + already_installed_gain - - if eco_packages is None: - target_sap = epc_to_sap_lower_bound(body.goal_value) - else: - target_sap = ( - eco_packages.get(p.id)[1] if eco_packages.get(p.id)[1] is not None - else epc_to_sap_lower_bound(body.goal_value) - ) + current_sap = p.epc_record.current_energy_efficiency + already_installed_gain + target_sap = epc_to_sap_lower_bound(body.goal_value) if target_sap <= current_sap: # We've already met or exceeded the target EPC