diff --git a/.idea/Model.iml b/.idea/Model.iml
index b0f9c00d..4413bb06 100644
--- a/.idea/Model.iml
+++ b/.idea/Model.iml
@@ -7,7 +7,7 @@
-
+
diff --git a/.idea/misc.xml b/.idea/misc.xml
index ca0e1cd9..3b05c6ac 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/backend/app/db/models/materials.py b/backend/app/db/models/materials.py
index 09d7369d..cf6dd971 100644
--- a/backend/app/db/models/materials.py
+++ b/backend/app/db/models/materials.py
@@ -13,6 +13,7 @@ class MaterialType(enum.Enum):
external_wall_insulation = "external_wall_insulation"
internal_wall_insulation = "internal_wall_insulation"
cavity_wall_insulation = "cavity_wall_insulation"
+ mechanical_ventilation = "mechanical_ventilation"
class DepthUnit(enum.Enum):
@@ -21,6 +22,7 @@ class DepthUnit(enum.Enum):
class CostUnit(enum.Enum):
gbp_sq_meter = "gbp_sq_meter"
+ gbp_per_unit = "gbp_per_unit"
class RValueUnit(enum.Enum):
diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py
index 400b2dee..135b877d 100644
--- a/backend/app/plan/router.py
+++ b/backend/app/plan/router.py
@@ -30,6 +30,7 @@ from backend.Property import Property
from etl.epc.DataProcessor import DataProcessor
from etl.epc.settings import COLUMNS_TO_MERGE_ON
from recommendations.FloorRecommendations import FloorRecommendations
+from recommendations.VentilationRecommendations import VentilationRecommendations
from recommendations.optimiser.CostOptimiser import CostOptimiser
from recommendations.optimiser.GainOptimiser import GainOptimiser
from recommendations.optimiser.optimiser_functions import prepare_input_measures
@@ -125,9 +126,6 @@ async def trigger_plan(body: PlanTriggerRequest):
#
# with open("cleaned.pickle", "rb") as f:
# cleaned = pickle.load(f)
- #
- # with open("materials_by_type.pickle", "wb") as f:
- # materials_by_type = pickle.load(f)
recommendations = {}
recommendations_scoring_data = []
@@ -160,6 +158,16 @@ async def trigger_plan(body: PlanTriggerRequest):
if wall_recomender.recommendations:
property_recommendations.append(wall_recomender.recommendations)
+ # Ventilation recommendations
+ ventilation_recomender = VentilationRecommendations(
+ property_instance=p,
+ materials=materials_by_type["ventilation"]
+ )
+ ventilation_recomender.recommend()
+
+ if ventilation_recomender.recommendation:
+ property_recommendations.append(ventilation_recomender.recommendation)
+
# We insert temporary ids into the recommendations which is important for the optimiser later
property_recommendations = insert_temp_recommendation_id(property_recommendations)
diff --git a/backend/app/plan/utils.py b/backend/app/plan/utils.py
index 85a02b61..0a1eaa72 100644
--- a/backend/app/plan/utils.py
+++ b/backend/app/plan/utils.py
@@ -15,7 +15,8 @@ def filter_materials(materials):
mapping = {
"walls": ["internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation"],
- "floor": ["suspended_floor_insulation", "solid_floor_insulation"]
+ "floor": ["suspended_floor_insulation", "solid_floor_insulation"],
+ "ventilation": ["mechanical_ventilation"],
}
materials = [row2dict(material) for material in materials]
@@ -164,9 +165,13 @@ def create_recommendation_scoring_data(
if scoring_dict["floor_insulation_thickness_ENDING"] is None:
scoring_dict["floor_insulation_thickness_ENDING"] = "none"
- if recommendation["type"] not in ["wall_insulation", "floor_insulation"]:
+ if recommendation["type"] == "mechanical_ventilation":
+ scoring_dict["MECHANICAL_VENTILATION_ENDING"] = 'mechanical, extract only'
+
+ if recommendation["type"] not in ["wall_insulation", "floor_insulation", "mechanical_ventilation"]:
raise NotImplementedError("Implement me")
+ # Fill missing roof u-values - this fill is not based on recommended upgrades
if scoring_dict["roof_thermal_transmittance_ENDING"] is None:
scoring_dict["roof_thermal_transmittance_ENDING"] = get_roof_u_value(
insulation_thickness=property.roof["insulation_thickness"],
diff --git a/backend/tests/test_sap_model_prep.py b/backend/tests/test_sap_model_prep.py
index c11a0fae..1ba59fa0 100644
--- a/backend/tests/test_sap_model_prep.py
+++ b/backend/tests/test_sap_model_prep.py
@@ -10,18 +10,19 @@ from utils.s3 import read_dataframe_from_s3_parquet
from tqdm import tqdm
+# Handy code for selecting testin data
# import pickle
+#
# with open("sap_change_dataset.pickle", "rb") as f:
# sap_change_dataset = pickle.load(f)
#
# search_from = sap_change_dataset[
-# (sap_change_dataset["walls_thermal_transmittance_ENDING"] != sap_change_dataset["walls_thermal_transmittance"]) &
-# (sap_change_dataset["is_solid_brick"])
-# ]
+# (sap_change_dataset["walls_thermal_transmittance_ENDING"] == sap_change_dataset["walls_thermal_transmittance"])
+# ]
# search_from = search_from[
# (search_from["roof_thermal_transmittance_ENDING"] == search_from["roof_thermal_transmittance"]) &
# (search_from["floor_thermal_transmittance_ENDING"] == search_from["floor_thermal_transmittance"]) &
-# (search_from["MECHANICAL_VENTILATION_ENDING"] == search_from["MECHANICAL_VENTILATION_STARTING"]) &
+# (search_from["MECHANICAL_VENTILATION_ENDING"] != search_from["MECHANICAL_VENTILATION_STARTING"]) &
# (search_from["SECONDHEAT_DESCRIPTION_ENDING"] == search_from["SECONDHEAT_DESCRIPTION_STARTING"]) &
# (search_from["GLAZED_TYPE_ENDING"] == search_from["GLAZED_TYPE_STARTING"])
# ]
@@ -51,16 +52,8 @@ from tqdm import tqdm
# starting_cols.append(starting_col)
#
# # We want them to be different
-# if c == "walls_thermal_transmittance_ENDING":
-# if row[c] == row[starting_col]:
-# same = False
-# break
-# else:
-# continue
-#
-# # We want them to be different
-# if c == "walls_insulation_thickness_ENDING":
-# if row[c] == row[starting_col]:
+# if c == "MECHANICAL_VENTILATION_ENDING":
+# if (row[c] == row[starting_col]) | (row[starting_col] != "natural"):
# same = False
# break
# else:
@@ -78,20 +71,20 @@ from tqdm import tqdm
#
# import pandas as pd
#
-# start = row[starting_cols]
+# start = row[["SAP_STARTING"] + starting_cols]
# start.index = [c.replace("_STARTING", "") for c in start.index]
-# end = row[ending_cols]
+# end = row[["SAP_ENDING"] + ending_cols]
# end.index = [c.replace("_ENDING", "") for c in end.index]
# start["type"] = "starting"
# end["type"] = "ending"
#
# compare = pd.concat([start, end], axis=1)
#
-# ending_lmk = "d0fc64d6b80db04c32998c9b846dd04c8f0b486231a11e4c062020b35af1312d"
-# starting_lmk = "b0d82f468273bec55ec5676a809b8e36b55db940ffa92f482a482f6aaa38eb1d"
+# ending_lmk = "96b34dbe6cebdd7e648151e070047c8cd605c539851fc5a37e325903440081ab"
+# starting_lmk = "dc1a4da246562656132b8e36e0534cd90b09fa40fc584e25e644e2d9ab86a247"
#
# client = EpcClient(auth_token=EPC_AUTH_TOKEN)
-# result = client.domestic.search(params={"address": "FLAT 12, WAREHOUSE W, 3 WESTERN GATEWAY", "postcode": "E16 1BD"})
+# result = client.domestic.search(params={"address": "45 Shepperson Road", "postcode": "S6 4FG"})
# starting_epc = [x for x in result["rows"] if x["lmk-key"] == starting_lmk][0]
# ending_epc = [x for x in result["rows"] if x["lmk-key"] == ending_lmk][0]
@@ -106,6 +99,8 @@ from tqdm import tqdm
# ) as f:
# cleaning_data = pickle.load(f)
+# TODO: Need to do floors, both suspended and solid
+
class TestSapModelPrep:
@@ -530,3 +525,184 @@ class TestSapModelPrep:
continue
assert test_record2[c].values[0] == row2[c]
+
+ def test_ventilation(self, cleaned, cleaning_data):
+
+ starting_epc3 = {
+ 'low-energy-fixed-light-count': '', 'address': '45 Shepperson Road', 'uprn-source': 'Energy Assessor',
+ 'floor-height': '1.87', 'heating-cost-potential': '645', 'unheated-corridor-length': '',
+ 'hot-water-cost-potential': '69', 'construction-age-band': 'England and Wales: 1900-1929',
+ 'potential-energy-rating': 'C', 'mainheat-energy-eff': 'Good', 'windows-env-eff': 'Average',
+ 'lighting-energy-eff': 'Average', 'environment-impact-potential': '75',
+ 'glazed-type': 'double glazing, unknown install date', 'heating-cost-current': '1028', 'address3': '',
+ 'mainheatcont-description': 'Programmer, TRVs and bypass', 'sheating-energy-eff': 'N/A',
+ 'property-type': 'House', 'local-authority-label': 'Sheffield', 'fixed-lighting-outlets-count': '21',
+ 'energy-tariff': 'Single', 'mechanical-ventilation': 'natural', 'hot-water-cost-current': '96',
+ 'county': '', 'postcode': 'S6 4FG', 'solar-water-heating-flag': 'N', 'constituency': 'E14000921',
+ 'co2-emissions-potential': '2.9', 'number-heated-rooms': '5',
+ 'floor-description': 'Suspended, no insulation (assumed)', 'energy-consumption-potential': '152',
+ 'local-authority': 'E08000019', 'built-form': 'Enclosed Mid-Terrace', 'number-open-fireplaces': '0',
+ 'windows-description': 'Fully double glazed', 'glazed-area': 'Normal', 'inspection-date': '2022-06-13',
+ 'mains-gas-flag': 'Y', 'co2-emiss-curr-per-floor-area': '59', 'address1': '45 Shepperson Road',
+ 'heat-loss-corridor': '', 'flat-storey-count': '',
+ 'constituency-label': 'Sheffield, Brightside and Hillsborough', 'roof-energy-eff': 'Very Poor',
+ 'total-floor-area': '107.0', 'building-reference-number': '10002892085', 'environment-impact-current': '46',
+ 'co2-emissions-current': '6.3', 'roof-description': 'Pitched, no insulation (assumed)',
+ 'floor-energy-eff': 'N/A', 'number-habitable-rooms': '5', 'address2': '', 'hot-water-env-eff': 'Good',
+ 'posttown': 'SHEFFIELD', 'mainheatc-energy-eff': 'Average', 'main-fuel': 'mains gas (not community)',
+ 'lighting-env-eff': 'Average', 'windows-energy-eff': 'Average', 'floor-env-eff': 'N/A',
+ 'sheating-env-eff': 'N/A', 'lighting-description': 'Low energy lighting in 43% of fixed outlets',
+ 'roof-env-eff': 'Very Poor', 'walls-energy-eff': 'Very Poor', 'photo-supply': '0.0',
+ 'lighting-cost-potential': '83', 'mainheat-env-eff': 'Good', 'multi-glaze-proportion': '100',
+ 'main-heating-controls': '', 'lodgement-datetime': '2023-05-27 12:15:21', 'flat-top-storey': '',
+ 'current-energy-rating': 'E', 'secondheat-description': 'None', 'walls-env-eff': 'Very Poor',
+ 'transaction-type': 'marketed sale', 'uprn': '100051073214', 'current-energy-efficiency': '54',
+ 'energy-consumption-current': '335', 'mainheat-description': 'Boiler and radiators, mains gas',
+ 'lighting-cost-current': '131', 'lodgement-date': '2023-05-27', 'extension-count': '1',
+ 'mainheatc-env-eff': 'Average',
+ 'lmk-key': 'dc1a4da246562656132b8e36e0534cd90b09fa40fc584e25e644e2d9ab86a247', 'wind-turbine-count': '0',
+ 'tenure': 'Not defined - use in the case of a new dwelling for which the intended tenure in not known. It '
+ 'is not to be used for an existing dwelling',
+ 'floor-level': '', 'potential-energy-efficiency': '80', 'hot-water-energy-eff': 'Good',
+ 'low-energy-lighting': '43',
+ 'walls-description': 'Sandstone or limestone, as built, no insulation (assumed)',
+ 'hotwater-description': 'From main system'
+ }
+
+ row3 = {
+ 'UPRN': '100051073214', 'RDSAP_CHANGE': 2, 'HEAT_DEMAND_CHANGE': -22, 'CARBON_CHANGE': -0.39999999999999947,
+ 'SAP_STARTING': 54, 'SAP_ENDING': 56, 'HEAT_DEMAND_STARTING': 335, 'HEAT_DEMAND_ENDING': 313,
+ 'CARBON_STARTING': 6.3, 'CARBON_ENDING': 5.9, 'PROPERTY_TYPE': 'House', 'BUILT_FORM': 'Mid-Terrace',
+ 'CONSTITUENCY': 'E14000921', 'NUMBER_HABITABLE_ROOMS': 5.0, 'NUMBER_HEATED_ROOMS': 5.0,
+ 'FIXED_LIGHTING_OUTLETS_COUNT': 21.0, 'CONSTRUCTION_AGE_BAND': 'England and Wales: 1900-1929',
+ 'TRANSACTION_TYPE_STARTING': 'marketed sale', 'MECHANICAL_VENTILATION_STARTING': 'natural',
+ 'SECONDHEAT_DESCRIPTION_STARTING': 'None', 'ENERGY_TARIFF_STARTING': 'Single',
+ 'SOLAR_WATER_HEATING_FLAG_STARTING': 'N', 'PHOTO_SUPPLY_STARTING': 0.0,
+ 'GLAZED_TYPE_STARTING': 'double glazing, unknown install date', 'MULTI_GLAZE_PROPORTION_STARTING': 100.0,
+ 'LOW_ENERGY_LIGHTING_STARTING': 43.0, 'NUMBER_OPEN_FIREPLACES_STARTING': 0.0,
+ 'EXTENSION_COUNT_STARTING': 1.0, 'TOTAL_FLOOR_AREA_STARTING': 107.0, 'FLOOR_HEIGHT_STARTING': 1.87,
+ 'TRANSACTION_TYPE_ENDING': 'marketed sale', 'MECHANICAL_VENTILATION_ENDING': 'mechanical, extract only',
+ 'SECONDHEAT_DESCRIPTION_ENDING': 'None', 'ENERGY_TARIFF_ENDING': 'Single',
+ 'SOLAR_WATER_HEATING_FLAG_ENDING': 'N', 'PHOTO_SUPPLY_ENDING': 0.0,
+ 'GLAZED_TYPE_ENDING': 'double glazing, unknown install date', 'MULTI_GLAZE_PROPORTION_ENDING': 100.0,
+ 'LOW_ENERGY_LIGHTING_ENDING': 43.0, 'NUMBER_OPEN_FIREPLACES_ENDING': 0.0, 'EXTENSION_COUNT_ENDING': 1.0,
+ 'TOTAL_FLOOR_AREA_ENDING': 107.0, 'FLOOR_HEIGHT_ENDING': 1.87, 'DAYS_TO_STARTING': 3221,
+ 'DAYS_TO_ENDING': 2874, 'walls_thermal_transmittance': 2.0, 'is_cavity_wall': False,
+ 'is_filled_cavity': False, 'is_solid_brick': False, 'is_system_built': False, 'is_timber_frame': False,
+ 'is_granite_or_whinstone': False, 'is_as_built': True, 'is_cob': False, 'is_sandstone_or_limestone': True,
+ 'is_park_home': False, 'walls_insulation_thickness': 'none', 'external_insulation': False,
+ 'internal_insulation': False, 'walls_thermal_transmittance_ENDING': 2.0, 'is_park_home_ENDING': False,
+ 'walls_insulation_thickness_ENDING': 'none', 'external_insulation_ENDING': False,
+ 'internal_insulation_ENDING': False, 'floor_thermal_transmittance': 0.62, 'is_to_unheated_space': False,
+ 'is_to_external_air': False, 'is_suspended': True, 'is_solid': False, 'another_property_below': False,
+ 'floor_insulation_thickness': 'none', 'floor_thermal_transmittance_ENDING': 0.62,
+ 'floor_insulation_thickness_ENDING': 'none', 'roof_thermal_transmittance': 2.3, 'is_pitched': True,
+ 'is_roof_room': False, 'is_loft': False, 'is_flat': False, 'is_thatched': False, 'is_at_rafters': False,
+ 'has_dwelling_above': False, 'roof_insulation_thickness': 'none', 'roof_thermal_transmittance_ENDING': 2.3,
+ 'roof_insulation_thickness_ENDING': 'none', 'heater_type': 'Unknown', 'system_type': 'from main system',
+ 'thermostat_characteristics': 'Unknown', 'heating_scope': 'Unknown', 'energy_recovery': 'Unknown',
+ 'hotwater_tariff_type': 'Unknown', 'extra_features': 'Unknown', 'chp_systems': 'Unknown',
+ 'distribution_system': 'Unknown', 'no_system_present': 'Unknown', 'appliance': 'Unknown',
+ 'heater_type_ENDING': 'Unknown', 'system_type_ENDING': 'from main system',
+ 'thermostat_characteristics_ENDING': 'Unknown', 'heating_scope_ENDING': 'Unknown',
+ 'energy_recovery_ENDING': 'Unknown', 'hotwater_tariff_type_ENDING': 'Unknown',
+ 'extra_features_ENDING': 'Unknown', 'chp_systems_ENDING': 'Unknown',
+ 'distribution_system_ENDING': 'Unknown', 'no_system_present_ENDING': 'Unknown',
+ 'appliance_ENDING': 'Unknown', 'has_radiators': True, 'has_fan_coil_units': False,
+ 'has_pipes_in_screed_above_insulation': False, 'has_pipes_in_insulated_timber_floor': False,
+ 'has_pipes_in_concrete_slab': False, 'has_boiler': True, 'has_air_source_heat_pump': False,
+ 'has_room_heaters': False, 'has_electric_storage_heaters': False, 'has_warm_air': False,
+ 'has_electric_underfloor_heating': False, 'has_electric_ceiling_heating': False,
+ 'has_community_scheme': False, 'has_ground_source_heat_pump': False, 'has_no_system_present': False,
+ 'has_portable_electric_heaters': False, 'has_water_source_heat_pump': False,
+ 'has_electric_heat_pump': False, 'has_micro-cogeneration': False, 'has_solar_assisted_heat_pump': False,
+ 'has_exhaust_source_heat_pump': False, 'has_community_heat_pump': False, 'has_electric': False,
+ 'has_mains_gas': True, 'has_wood_logs': False, 'has_coal': False, 'has_oil': False,
+ 'has_wood_pellets': False, 'has_anthracite': False, 'has_dual_fuel_mineral_and_wood': False,
+ 'has_smokeless_fuel': False, 'has_lpg': False, 'has_b30k': False, 'has_electricaire': False,
+ 'has_assumed_for_most_rooms': False, 'has_underfloor_heating': False, 'has_radiators_ENDING': True,
+ 'has_fan_coil_units_ENDING': False, 'has_pipes_in_screed_above_insulation_ENDING': False,
+ 'has_pipes_in_insulated_timber_floor_ENDING': False, 'has_pipes_in_concrete_slab_ENDING': False,
+ 'has_boiler_ENDING': True, 'has_air_source_heat_pump_ENDING': False, 'has_room_heaters_ENDING': False,
+ 'has_electric_storage_heaters_ENDING': False, 'has_warm_air_ENDING': False,
+ 'has_electric_underfloor_heating_ENDING': False, 'has_electric_ceiling_heating_ENDING': False,
+ 'has_community_scheme_ENDING': False, 'has_ground_source_heat_pump_ENDING': False,
+ 'has_no_system_present_ENDING': False, 'has_portable_electric_heaters_ENDING': False,
+ 'has_water_source_heat_pump_ENDING': False, 'has_electric_heat_pump_ENDING': False,
+ 'has_micro-cogeneration_ENDING': False, 'has_solar_assisted_heat_pump_ENDING': False,
+ 'has_exhaust_source_heat_pump_ENDING': False, 'has_community_heat_pump_ENDING': False,
+ 'has_electric_ENDING': False, 'has_mains_gas_ENDING': True, 'has_wood_logs_ENDING': False,
+ 'has_coal_ENDING': False, 'has_oil_ENDING': False, 'has_wood_pellets_ENDING': False,
+ 'has_anthracite_ENDING': False, 'has_dual_fuel_mineral_and_wood_ENDING': False,
+ 'has_smokeless_fuel_ENDING': False, 'has_lpg_ENDING': False, 'has_b30k_ENDING': False,
+ 'has_electricaire_ENDING': False, 'has_assumed_for_most_rooms_ENDING': False,
+ 'has_underfloor_heating_ENDING': False, 'thermostatic_control': 'Unknown', 'charging_system': 'Unknown',
+ 'switch_system': 'programmer', 'no_control': 'Unknown', 'dhw_control': 'Unknown',
+ 'community_heating': 'Unknown', 'multiple_room_thermostats': False, 'auxiliary_systems': 'bypass',
+ 'trvs': 'trvs', 'rate_control': 'Unknown', 'thermostatic_control_ENDING': 'Unknown',
+ 'charging_system_ENDING': 'Unknown', 'switch_system_ENDING': 'programmer', 'no_control_ENDING': 'Unknown',
+ 'dhw_control_ENDING': 'Unknown', 'community_heating_ENDING': 'Unknown',
+ 'multiple_room_thermostats_ENDING': False, 'auxiliary_systems_ENDING': 'bypass', 'trvs_ENDING': 'trvs',
+ 'rate_control_ENDING': 'Unknown', 'glazing_type': 'double', 'glazing_type_ENDING': 'double',
+ 'fuel_type': 'mains gas', 'main-fuel_tariff_type': 'Unknown', 'is_community': False,
+ 'no_individual_heating_or_community_network': False, 'complex_fuel_type': 'Unknown',
+ 'fuel_type_ENDING': 'mains gas', 'main-fuel_tariff_type_ENDING': 'Unknown', 'is_community_ENDING': False,
+ 'no_individual_heating_or_community_network_ENDING': False, 'complex_fuel_type_ENDING': 'Unknown',
+ 'estimated_perimeter_STARTING': 41.634120622393354, 'estimated_perimeter_ENDING': 41.634120622393354
+ }
+
+ home3 = Property(
+ id=0,
+ postcode=starting_epc3["postcode"],
+ address1=starting_epc3["address1"],
+ epc_client=EpcClient(auth_token="notoken"),
+ data=starting_epc3
+ )
+ home3.get_components(cleaned)
+
+ data_processor3 = DataProcessor(None, newdata=True)
+ data_processor3.insert_data(pd.DataFrame([home3.get_model_data()]))
+
+ data_processor3.pre_process()
+
+ starting_epc_data3 = data_processor3.get_component_features(suffix="_STARTING")
+ ending_epc_data3 = data_processor3.get_component_features(suffix="_ENDING")
+ fixed_data3 = data_processor3.get_fixed_features()
+
+ ending_lodgement_date3 = '2022-06-14'
+
+ ending_epc_data3["DAYS_TO_ENDING"] = data_processor3.calculate_days_to(ending_lodgement_date3)
+
+ recommendation3 = {
+ "recommendation_id": 0,
+ "type": "mechanical_ventilation"
+ }
+
+ test_record3 = create_recommendation_scoring_data(
+ property=home3,
+ recommendation=recommendation3,
+ starting_epc_data=starting_epc_data3,
+ ending_epc_data=ending_epc_data3,
+ fixed_data=fixed_data3,
+ )
+ test_record3 = pd.DataFrame([test_record3])
+
+ # Test the final cleaning:
+ test_record3 = DataProcessor.apply_averages_cleaning(
+ data_to_clean=test_record3,
+ cleaning_data=cleaning_data,
+ cols_to_merge_on=COLUMNS_TO_MERGE_ON + ["LOCAL_AUTHORITY"]
+ ).drop(columns=["LOCAL_AUTHORITY"])
+
+ test_record3 = DataProcessor.clean_missings_after_description_process(
+ test_record3, [
+ c for c in test_record3.columns if
+ ("thermal_transmittance" in c) or ("insulation_thickness" in c)
+ ]
+ )
+
+ for c in test_record3.columns:
+ if c in ["id", "SAP_ENDING", "HEAT_DEMAND_ENDING", "CARBON_ENDING"]:
+ continue
+
+ assert test_record3[c].values[0] == row3[c]
diff --git a/recommendations/VentilationRecommendations.py b/recommendations/VentilationRecommendations.py
new file mode 100644
index 00000000..35de9b3b
--- /dev/null
+++ b/recommendations/VentilationRecommendations.py
@@ -0,0 +1,70 @@
+import pandas as pd
+from BaseUtility import Definitions
+from backend.Property import Property
+
+
+class VentilationRecommendations(Definitions):
+ """
+ For properties that do not have ventilation, we recommend installing ventilaion
+ This is particularly important for properties that have insulated walls and is also
+ crucial for prevent overheating risks in warmer months
+ """
+
+ VENTILATION_DESCRIPTIONS = [
+ 'mechanical, extract only',
+ 'mechanical, supply and extract'
+ ]
+
+ def __init__(
+ self,
+ property_instance: Property,
+ materials
+ ):
+ self.property = property_instance
+
+ self.has_ventilaion = None
+ self.recommendation = None
+ self.materials = materials
+
+ def identify_ventilation(self):
+ self.has_ventilaion = self.property.data["mechanical-ventilation"] in self.VENTILATION_DESCRIPTIONS
+
+ def recommend(self):
+ """
+ If there is no ventilation, we recommend installing ventilation
+
+ Generally, best practice is to install controlled ventilation for insulated walls so we still recommend
+ ventilation if there is natural ventilation
+ :return:
+ """
+
+ self.identify_ventilation()
+ if self.has_ventilaion:
+ return
+
+ if len(self.materials) != 1:
+ raise NotImplementedError("Only handled the case of having one venilation option")
+
+ # We recommend installing 2 units
+ n_units = 2
+
+ part = self.materials.copy()
+
+ estimated_cost = n_units * part[0]["cost"]
+
+ part[0]["estimated_cost"] = estimated_cost
+ part[0]["quantity"] = n_units
+ part[0]["quantity_unit"] = None
+
+ # We recommend installing two mechanical ventilation systems
+ self.recommendation = [
+ {
+ "parts": part,
+ "type": part[0]["type"],
+ "description": "Install %s" % part[0]["description"],
+ "starting_u_value": None,
+ "new_u_value": None,
+ "sap_points": None,
+ "cost": estimated_cost,
+ }
+ ]
diff --git a/recommendations/WallRecommendations.py b/recommendations/WallRecommendations.py
index b3eea7e0..ad2ca861 100644
--- a/recommendations/WallRecommendations.py
+++ b/recommendations/WallRecommendations.py
@@ -1,4 +1,3 @@
-import itertools
import math
from typing import List
diff --git a/recommendations/tests/test_ventilation_recommendations.py b/recommendations/tests/test_ventilation_recommendations.py
new file mode 100644
index 00000000..2dcaba57
--- /dev/null
+++ b/recommendations/tests/test_ventilation_recommendations.py
@@ -0,0 +1,110 @@
+from backend.Property import Property
+from unittest.mock import Mock
+from recommendations.VentilationRecommendations import VentilationRecommendations
+
+ventilation_materials = [
+ {
+ 'id': 17, 'type': 'mechanical_ventilation', 'description': 'Mechanical Extract Ventilation',
+ 'depths': None, 'depth_unit': None, 'cost': 500, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': None,
+ 'r_value_unit': None, 'thermal_conductivity': None, 'thermal_conductivity_unit': None,
+ 'link': None, 'is_active': True, 'estimated_cost': 1000, 'quantity': 2, 'quantity_unit': None
+ }
+]
+
+
+class TestVentilationRecommendations:
+
+ def test_natural_ventilation(self):
+ input_property1 = Property(id=1, postcode="F4k3 6", address1="623 fake street", epc_client=Mock())
+ input_property1.data = {"mechanical-ventilation": "natural"}
+
+ recommender = VentilationRecommendations(
+ property_instance=input_property1,
+ materials=ventilation_materials
+ )
+
+ assert not recommender.recommendation
+
+ recommender.recommend()
+
+ assert len(recommender.recommendation) == 1
+
+ assert recommender.recommendation[0]["cost"] == 1000
+ assert recommender.recommendation[0]["type"] == "mechanical_ventilation"
+ assert len(recommender.recommendation[0]["parts"]) == 1
+ assert recommender.recommendation[0]["parts"][0]["description"] == 'Mechanical Extract Ventilation'
+ assert recommender.recommendation[0]["parts"][0]["quantity"] == 2
+
+ def test_missing_ventilation(self):
+ input_property2 = Property(id=1, postcode="F4k3 6", address1="623 fake street", epc_client=Mock())
+ input_property2.data = {"mechanical-ventilation": None}
+
+ recommender2 = VentilationRecommendations(
+ property_instance=input_property2,
+ materials=ventilation_materials
+ )
+
+ assert not recommender2.recommendation
+
+ recommender2.recommend()
+
+ assert len(recommender2.recommendation) == 1
+
+ assert recommender2.recommendation[0]["cost"] == 1000
+ assert recommender2.recommendation[0]["type"] == "mechanical_ventilation"
+ assert len(recommender2.recommendation[0]["parts"]) == 1
+ assert recommender2.recommendation[0]["parts"][0]["description"] == 'Mechanical Extract Ventilation'
+ assert recommender2.recommendation[0]["parts"][0]["quantity"] == 2
+
+ def test_nodata_ventilation(self):
+ input_property3 = Property(id=1, postcode="F4k3 6", address1="623 fake street", epc_client=Mock())
+ input_property3.data = {"mechanical-ventilation": "NO DATA!!"}
+
+ recommender3 = VentilationRecommendations(
+ property_instance=input_property3,
+ materials=ventilation_materials
+ )
+
+ assert not recommender3.recommendation
+
+ recommender3.recommend()
+
+ assert len(recommender3.recommendation) == 1
+
+ assert recommender3.recommendation[0]["cost"] == 1000
+ assert recommender3.recommendation[0]["type"] == "mechanical_ventilation"
+ assert len(recommender3.recommendation[0]["parts"]) == 1
+ assert recommender3.recommendation[0]["parts"][0]["description"] == 'Mechanical Extract Ventilation'
+ assert recommender3.recommendation[0]["parts"][0]["quantity"] == 2
+
+ def test_existing_ventilation_1(self):
+ input_property4 = Property(id=1, postcode="F4k3 6", address1="623 fake street", epc_client=Mock())
+ input_property4.data = {"mechanical-ventilation": 'mechanical, extract only'}
+
+ recommender4 = VentilationRecommendations(
+ property_instance=input_property4,
+ materials=ventilation_materials
+ )
+
+ assert not recommender4.recommendation
+
+ recommender4.recommend()
+
+ assert not recommender4.recommendation
+ assert recommender4.has_ventilaion
+
+ def test_existing_ventilation_2(self):
+ input_property5 = Property(id=1, postcode="F4k3 6", address1="623 fake street", epc_client=Mock())
+ input_property5.data = {"mechanical-ventilation": 'mechanical, supply and extract'}
+
+ recommender5 = VentilationRecommendations(
+ property_instance=input_property5,
+ materials=ventilation_materials
+ )
+
+ assert not recommender5.recommendation
+
+ recommender5.recommend()
+
+ assert not recommender5.recommendation
+ assert recommender5.has_ventilaion