implementing mv

This commit is contained in:
Khalim Conn-Kowlessar 2025-03-15 17:34:55 +00:00
parent 792be8468b
commit c2062507ca
9 changed files with 120 additions and 81 deletions

View file

@ -380,7 +380,7 @@ class Property:
for rec in property_recommendations_by_phase:
# We simulate the impact of the recommendation at this current phase, and all of the prior phases
if rec["type"] in ["mechanical_ventilation", "trickle_vents", "draught_proofing"]:
if rec["type"] in ["trickle_vents", "draught_proofing"]:
continue
scoring_dict = self.create_recommendation_scoring_data(
@ -388,7 +388,6 @@ class Property:
recommendation_record=recommendation_record,
recommendations=previous_phase_representatives + [rec],
primary_recommendation_id=rec["recommendation_id"],
non_invasive_recommendations=self.non_invasive_recommendations,
)
self.recommendations_scoring_data.append(scoring_dict)
@ -494,7 +493,6 @@ class Property:
recommendation_record,
recommendations: list,
primary_recommendation_id: int,
non_invasive_recommendations: list = None,
):
"""
This function will iterate through a list of recommendations and apply a simulation for each recommendation
@ -503,7 +501,6 @@ class Property:
:param recommendation_record: The record of the property, which will be updated
:param recommendations: The list of recommendations to apply
:param primary_recommendation_id: The id of the primary recommendation, which is used to identify the record
:param non_invasive_recommendations: The list of non-invasive recommendations
:return: The updated recommendation record
"""
@ -532,7 +529,7 @@ class Property:
"internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation",
"cylinder_thermostat", "loft_insulation", "room_roof_insulation", "flat_roof_insulation",
"solid_floor_insulation", "suspended_floor_insulation", "mixed_glazing",
"windows_glazing"
"windows_glazing", "mechanical_ventilation"
]:
# We update the data, as defined in the recommendaton
for prefix in ["walls", "roof", "floor"]:
@ -558,7 +555,7 @@ class Property:
"solid_floor_insulation", "suspended_floor_insulation",
"windows_glazing", "solar_pv", "heating", "hot_water_tank_insulation",
"heating_control", "secondary_heating", "cylinder_thermostat", "mixed_glazing",
"extension_cavity_wall_insulation",
"extension_cavity_wall_insulation", "mechanical_ventilation",
]:
raise NotImplementedError(
"Implement me, given type %s" % recommendation["type"]

View file

@ -203,11 +203,11 @@ class TrainingDataset(BaseDataset):
common_cols = [[col + "_starting", col + "_ending"] for col in common_cols]
self.df = self.df.loc[
:,
no_suffix_cols
+ only_ending_cols
+ [col for cols in common_cols for col in cols],
]
:,
no_suffix_cols
+ only_ending_cols
+ [col for cols in common_cols for col in cols],
]
def _remove_abnormal_change_in_floor_area(self):
"""
@ -511,7 +511,7 @@ class TrainingDataset(BaseDataset):
expanded_df["is_sandstone_or_limestone"]
== expanded_df["is_sandstone_or_limestone_ending"]
)
]
]
elif component == "floor":
expanded_df = expanded_df[
(expanded_df["is_suspended"] == expanded_df["is_suspended_ending"])
@ -528,7 +528,7 @@ class TrainingDataset(BaseDataset):
expanded_df["is_to_external_air"]
== expanded_df["is_to_external_air_ending"]
)
]
]
elif component == "roof":
expanded_df = expanded_df[
(expanded_df["is_pitched"] == expanded_df["is_pitched_ending"])
@ -541,7 +541,7 @@ class TrainingDataset(BaseDataset):
expanded_df["has_dwelling_above"]
== expanded_df["has_dwelling_above_ending"]
)
]
]
return expanded_df

View file

@ -139,28 +139,22 @@ class EPCRecord:
self._clean_records_using_epc_records()
self._clean_with_data_processor()
self._expand_prepared_epc_to_attributes()
self._identify_delta_between_prepared_and_original_records()
# Process to create uvalues for the single epc record
# selff.df = self.epc_record_as_dataframe('prepared_epc')
# self.df = self.epc_record_as_dataframe('prepared_epc')
# self._feature_generation()
# self._drop_features()
return
self._expand_description_to_features()
self._expand_description_to_uvalues()
# self._expand_description_to_features()
# self._expand_description_to_uvalues()
#
# self._generate_uvalues()
# self._validate_expanded_description()
# self._validate_u_values()
# etc
pass
def _drop_features(self):
"""
@ -360,6 +354,7 @@ class EPCRecord:
self._clean_number_lighting_outlets()
self._clean_floor_level()
self._clean_floor_height()
self._clean_constituency()
# self._clean_potential_energy_efficiency()
# self._clean_environment_impact_potential()
@ -402,6 +397,17 @@ class EPCRecord:
if self.prepared_epc["floor-height"] <= 1.665:
self.prepared_epc["floor-height"] = average
def _clean_constituency(self):
"""
We handle the single case of finding a missing constituency by using the local authority
"""
if pd.isnull(self.prepared_epc["constituency"]) or (self.prepared_epc["constituency"] == ""):
if self.prepared_epc["local-authority"] != "E06000044":
raise NotImplementedError(
"This function is only implemented for Portsmouth, in the single edgecase seen"
)
self.prepared_epc["constituency"] = "E14000883"
def _clean_floor_level(self):
"""
This method will clean the floor level, if empty or invalid

View file

@ -234,6 +234,13 @@ class Costs:
if self.region is None:
# Try and grab using the local-authority-label
self.region = county_to_region_map.get(self.property.data["local-authority-label"], None)
if self.region is None:
# Try and get the region after converting the keys to lower
self.region = {
k.lower(): v for k, v in county_to_region_map.items()
}.get(self.property.data["local-authority-label"].lower(), None)
if self.region is None:
raise ValueError("Region not found in county map")

View file

@ -12,7 +12,7 @@ class HeatingControlRecommender:
self.recommendation = []
def recommend(self, heating_description, description_prefix="", description_suffix=""):
def recommend(self, heating_description, phase, description_prefix="", description_suffix=""):
# TODO: Many of these functions are quite similar. We can possibly create a single wrapper function that
# takes in the heating description and the description prefix/suffix, and then creates the appropriate
@ -23,32 +23,32 @@ class HeatingControlRecommender:
# This first iteration of the recommender will provide very basic recommendation
# We recommend heating controls based on the main heating system
if heating_description in ["Room heaters, electric"]:
self.recommend_room_heaters_electric_controls()
self.recommend_room_heaters_electric_controls(phase=phase)
return
if heating_description in ["Electric storage heaters", "Electric storage heaters, radiators"]:
self.recommend_high_heat_retention_controls(description_prefix=description_prefix)
self.recommend_high_heat_retention_controls(description_prefix=description_prefix, phase=phase)
return
if heating_description in ["Boiler and radiators, mains gas"]:
# We can recommend roomstat programmer trvs
self.recommend_roomstat_programmer_trvs(description_suffix=description_suffix)
self.recommend_roomstat_programmer_trvs(description_suffix=description_suffix, phase=phase)
# We can also recommend time and temperature zone controls
self.recommend_time_temperature_zone_controls(description_suffix=description_suffix)
self.recommend_time_temperature_zone_controls(description_suffix=description_suffix, phase=phase)
return
if heating_description in ["Boiler and radiators, electric"]:
self.recommend_roomstat_programmer_trvs()
self.recommend_roomstat_programmer_trvs(phase=phase)
return
if heating_description in ["Air source heat pump, radiators, electric"]:
# For an ASHP, we can recommend time and temperature zone controls, as well as programmer, trvs and a bypass
# which are common configurations for ASHPs
self.recommend_time_temperature_zone_controls()
self.recommend_time_temperature_zone_controls(phase=phase)
# self.recommend_programmer_trvs_bypass()
def recommend_room_heaters_electric_controls(self):
def recommend_room_heaters_electric_controls(self, phase):
"""
If the home has Room heaters, electric, we start by identifying potential heating controls that could
be upgraded, that would provide a practical impact. This will be the least invasive improvement.
@ -88,6 +88,9 @@ class HeatingControlRecommender:
self.recommendation.append(
{
"phase": phase,
"type": "heating",
"measure_type": "programmer_appliance_thermostat",
"description": "upgrade heating controls to Programmer and Appliance or Smart Thermostats",
**self.costs.programmer_and_appliance_thermostat(has_programmer=has_programmer),
"simulation_config": simulation_config
@ -97,7 +100,7 @@ class HeatingControlRecommender:
# We don't implement any other recommendations right now
return
def recommend_high_heat_retention_controls(self, description_prefix=""):
def recommend_high_heat_retention_controls(self, phase, description_prefix=""):
"""
When applicable, we recommend upgrading the heating controls to high heat retention controls. This is a
specific type of control system that is designed to work with electric storage heaters. It is a more
@ -133,6 +136,9 @@ class HeatingControlRecommender:
self.recommendation.append(
{
"phase": phase,
"type": "heating",
"measure_type": "celect_type_controls",
"description": "Upgrade heating controls to High Heat Retention Storage Heater Controls",
**self.costs.celect_type_controls(),
"simulation_config": simulation_config,
@ -143,7 +149,7 @@ class HeatingControlRecommender:
# We don't implement any other recommendations right now
return
def recommend_roomstat_programmer_trvs(self, description_suffix=""):
def recommend_roomstat_programmer_trvs(self, phase, description_suffix=""):
"""
If the home has a boiler and radiators, mains gas, we start by identifying potential heating controls that could
be upgraded, that would provide a practical impact.
@ -208,15 +214,16 @@ class HeatingControlRecommender:
description = "Upgrade heating controls to Room thermostat, programmer and TRVs"
already_installed = "heating_control" in self.property.already_installed
already_installed = "roomstat_programmer_trvs" in self.property.already_installed
if already_installed:
cost_result = override_costs(cost_result)
description = "Heating controls have already been upgraded, no further action needed."
self.recommendation.append(
{
"type": "heating_control",
"type": "heating",
"measure_type": "roomstat_programmer_trvs",
"phase": phase,
"parts": [],
"description": description,
**cost_result,
@ -231,7 +238,7 @@ class HeatingControlRecommender:
return
def recommend_time_temperature_zone_controls(self, description_suffix=""):
def recommend_time_temperature_zone_controls(self, phase, description_suffix=""):
"""
If the home has a boiler, we can recommend time and temperature zone controls. This is a more advanced
and more efficient control system than the standard controls that come with a boiler. However, it may come
@ -282,14 +289,15 @@ class HeatingControlRecommender:
"temperature zone control)"
)
already_installed = "heating_control" in self.property.already_installed
already_installed = "time_temperature_zone_control" in self.property.already_installed
if already_installed:
cost_result = override_costs(cost_result)
description = "Heating controls have already been upgraded, no further action needed."
self.recommendation.append(
{
"type": "heating_control",
"type": "heating",
"phase": phase,
"measure_type": "time_temperature_zone_control",
"parts": [],
"description": description,
@ -335,14 +343,15 @@ class HeatingControlRecommender:
description = "Install a Bypass valve, TRVs and a Programmer"
already_installed = "heating_control" in self.property.already_installed
already_installed = "programmer_trvs_bypass" in self.property.already_installed
if already_installed:
cost_result = override_costs(cost_result)
description = "Heating controls have already been upgraded, no further action needed."
self.recommendation.append(
{
"type": "heating_control",
"type": "heating",
"measure_type": "programmer_trvs_bypass",
"parts": [],
"description": description,
**cost_result,

View file

@ -65,7 +65,6 @@ class HeatingRecommender:
self.costs = Costs(self.property)
self.heating_recommendations = []
self.heating_control_recommendations = []
self.has_electric_heating_description = (
self.property.main_heating["has_electric"] or self.property.main_heating["has_electricaire"]
@ -259,7 +258,6 @@ class HeatingRecommender:
"ashp_only_heating_recommendation", False
)
self.heating_recommendations = []
self.heating_control_recommendations = []
# This first iteration of the recommender will provide very basic recommendation
# We recommend heating controls based on the main heating system
@ -302,7 +300,6 @@ class HeatingRecommender:
self.recommend_air_source_heat_pump(
phase=phase,
has_cavity_or_loft_recommendations=has_cavity_or_loft_recommendations,
)
return
@ -360,7 +357,7 @@ class HeatingRecommender:
}
controls_recommender = HeatingControlRecommender(self.property)
controls_recommender.recommend(heating_description="Boiler and radiators, electric")
controls_recommender.recommend(heating_description="Boiler and radiators, electric", phase=phase)
self.heating_recommendations.extend([boiler_recommendation] + controls_recommender.recommendation)
return
@ -453,7 +450,7 @@ class HeatingRecommender:
), {})
controls_recommender = HeatingControlRecommender(self.property)
controls_recommender.recommend(heating_description="Air source heat pump, radiators, electric")
controls_recommender.recommend(heating_description="Air source heat pump, radiators, electric", phase=phase)
ashp_size = self.size_heat_pump()
ashp_costs = self.costs.air_source_heat_pump(ashp_size)
@ -805,7 +802,9 @@ class HeatingRecommender:
description_prefix = ""
controls_recommender.recommend(
heating_description="Electric storage heaters", description_prefix=description_prefix
heating_description="Electric storage heaters",
description_prefix=description_prefix,
phase=phase
)
has_hhr = self.is_hhr_already_installed()
@ -1120,10 +1119,10 @@ class HeatingRecommender:
description_suffix = ""
controls_recommender.recommend(
heating_description="Boiler and radiators, mains gas",
description_suffix=description_suffix
description_suffix=description_suffix,
phase=recommendation_phase
)
# We may have 2 recommendations from the heating controls
if not controls_recommender.recommendation and not boiler_recommendation:
return
@ -1161,10 +1160,6 @@ class HeatingRecommender:
# 3) Heating controls only
# But they are options that are not mutually exclusive
# So, we actually set heating controls as a heating recommendation
for recommendation in controls_recommender.recommendation:
recommendation["phase"] = recommendation_phase
# recommendation["type"] = "heating"
self.heating_control_recommendations.extend(controls_recommender.recommendation)
self.heating_recommendations.extend(controls_recommender.recommendation)
return

View file

@ -149,9 +149,10 @@ class Recommendations:
(self.wall_recomender.recommendations or self.roof_recommender.recommendations) and
("ventilation" in measures)
):
self.ventilation_recomender.recommend()
self.ventilation_recomender.recommend(phase=phase)
if self.ventilation_recomender.recommendation:
property_recommendations.append(self.ventilation_recomender.recommendation)
phase += 1
if "trickle_vents" in measures:
# This is a recommendatin that typically comes from an energy assessment
@ -208,27 +209,25 @@ class Recommendations:
measures=measures,
has_cavity_or_loft_recommendations=has_cavity_or_loft_recommendations,
)
if (
self.heating_recommender.heating_recommendations or
self.heating_recommender.heating_control_recommendations
):
if self.heating_recommender.heating_recommendations:
# We split into first and second phase recommendations
first_phase_recommendations = [
r for r in (
self.heating_recommender.heating_recommendations +
self.heating_recommender.heating_control_recommendations
self.heating_recommender.heating_recommendations
)
if r["phase"] == phase
]
second_phase_recommendations = [
r for r in (
self.heating_recommender.heating_recommendations +
self.heating_recommender.heating_control_recommendations
self.heating_recommender.heating_recommendations
)
if r["phase"] == phase + 1
]
if first_phase_recommendations and second_phase_recommendations:
raise Exception("Imeplement me")
if first_phase_recommendations:
property_recommendations.append(first_phase_recommendations)
@ -240,8 +239,7 @@ class Recommendations:
# otherwise we incremenet by 1
max_used_phase = max(
[rec["phase"] for rec in
self.heating_recommender.heating_recommendations +
self.heating_recommender.heating_control_recommendations]
self.heating_recommender.heating_recommendations]
)
amount_to_increment = max_used_phase - phase + 1
phase += amount_to_increment
@ -306,7 +304,7 @@ class Recommendations:
# want to include the cavity wall insulation recommendation in the defaults
if recommendations_by_type[0].get("type") in [
"mechanical_ventilation", "trickle_vents", "draught_proofing"
"trickle_vents", "draught_proofing"
]:
continue
@ -480,12 +478,14 @@ class Recommendations:
increasing_variables = ["sap"]
decreasing_variables = ["carbon", "heat_demand"]
# If the recommendation is mechanical ventilation, we don't apply the rule that the new value should be higher
mv_increasing_variables = ["carbon", "heat_demand"]
mv_decreasing_variables = ["sap"]
impact_summary = []
for recommendations_by_type in property_recommendations:
for rec in recommendations_by_type:
if rec["type"] in [
"mechanical_ventilation", "trickle_vents", "draught_proofing", "extension_cavity_wall_insulation"
]:
if rec["type"] in ["trickle_vents", "draught_proofing", "extension_cavity_wall_insulation"]:
# We don't have a percieved sap impact of mechanical ventilation or trickle vents, and we don't
# have the capacity to score draught proofing
if rec["type"] == "extension_cavity_wall_insulation":
@ -571,13 +571,23 @@ class Recommendations:
# For decreasing variables, the new value should be lower than the previous, otherwise we set it to
# the previous
# In either case, we adjudge the recommendation to have had no/negligible impact
for v in increasing_variables:
# However, if the recommendation is mechanical ventilation, this can have a negative SAP impact so
# we don't apply this rule
if rec["type"] == "mechanical_ventilation":
phase_increasing_variables = mv_increasing_variables
phase_decreasing_variables = mv_decreasing_variables
else:
phase_increasing_variables = increasing_variables
phase_decreasing_variables = decreasing_variables
for v in phase_increasing_variables:
current_phase_values[v] = (
current_phase_values[v] if current_phase_values[v] > previous_phase_values[v] else
previous_phase_values[v]
)
for v in previous_phase_values:
if v in decreasing_variables:
if v in phase_decreasing_variables:
current_phase_values[v] = (
current_phase_values[v] if current_phase_values[v] < previous_phase_values[v] else
previous_phase_values[v]
@ -592,13 +602,19 @@ class Recommendations:
"heat_demand": previous_phase_values["heat_demand"] - current_phase_values["heat_demand"],
}
# Prevent from being negative
# Prevent from being negative - apart from ventilation
for metric in ["sap", "carbon", "heat_demand"]:
property_phase_impact[metric] = (
0 if property_phase_impact[metric] < 0 else property_phase_impact[metric]
)
if metric == "sap":
property_phase_impact[metric] = round(property_phase_impact[metric], 2)
if rec["type"] != "mechanical_ventilation":
property_phase_impact[metric] = (
0 if property_phase_impact[metric] < 0 else property_phase_impact[metric]
)
if metric == "sap":
property_phase_impact[metric] = round(property_phase_impact[metric], 2)
else:
# We prevent these from being positive
property_phase_impact[metric] = (
0 if property_phase_impact[metric] > 0 else property_phase_impact[metric]
)
# For the moment, we cap the number of SAP points that can be achieved by LEDs at 2
if rec["type"] == "low_energy_lighting":

View file

@ -29,7 +29,7 @@ class VentilationRecommendations(Definitions):
def identify_ventilation(self):
self.has_ventilaion = self.property.data["mechanical-ventilation"] in self.VENTILATION_DESCRIPTIONS
def recommend(self):
def recommend(self, phase):
"""
If there is no ventilation, we recommend installing ventilation
@ -63,7 +63,7 @@ class VentilationRecommendations(Definitions):
# We recommend installing two mechanical ventilation systems
self.recommendation = [
{
"phase": None,
"phase": phase,
"parts": part,
"type": part[0]["type"],
"measure_type": "mechanical_ventilation",
@ -79,7 +79,13 @@ class VentilationRecommendations(Definitions):
"total": estimated_cost,
# We use a very simple and rough estimate of 4 hours per unit
"labour_hours": labour_hours,
"labour_days": labour_days # Assume 8 hour day
"labour_days": labour_days, # Assume 8 hour day
"simulation_config": {
"mechanical_ventilation_ending": "mechanical, extract only",
},
"description_simulation": {
"mechanical-ventilation": "mechanical, extract only"
}
}
]

View file

@ -135,7 +135,10 @@ county_to_region_map = {
'Merthyr Tydfil': 'Wales', 'Monmouthshire': 'Wales', 'Mountain Ash': 'Wales', 'Neath Port Talbot': 'Wales',
'Newport': 'Wales', 'Pembrokeshire': 'Wales', 'Penarth': 'Wales', 'Pentre': 'Wales', 'Pontyclun': 'Wales',
'Pontypridd': 'Wales', 'Porth': 'Wales', 'Porthcawl': 'Wales', 'Powys': 'Wales', 'Rhondda Cynon Taff': 'Wales',
'Rhoose': 'Wales', 'Sully': 'Wales', 'Swansea': 'Wales', 'The Vale of Glamorgan': 'Wales', 'Tonypandy': 'Wales',
'Rhoose': 'Wales', 'Sully': 'Wales', 'Swansea': 'Wales',
'The Vale of Glamorgan': 'Wales',
'Vale of Glamorgan': 'Wales',
'Tonypandy': 'Wales',
'Torfaen': 'Wales', 'Treharris': 'Wales', 'Treorchy': 'Wales', 'Wrexham': 'Wales', 'Birmingham': 'West Midlands',
'Bromsgrove': 'West Midlands', 'Cannock Chase': 'West Midlands', 'Coventry': 'West Midlands',
'Dudley': 'West Midlands', 'East Staffordshire': 'West Midlands', 'Herefordshire': 'West Midlands',