tweaking solar recommendations

This commit is contained in:
Khalim Conn-Kowlessar 2024-10-17 18:31:37 +01:00
parent 800b673150
commit d7ed4dd9a4
9 changed files with 92 additions and 30 deletions

View file

@ -538,7 +538,8 @@ class Property:
"loft_insulation", "room_roof_insulation", "flat_roof_insulation", "loft_insulation", "room_roof_insulation", "flat_roof_insulation",
"solid_floor_insulation", "suspended_floor_insulation", "solid_floor_insulation", "suspended_floor_insulation",
"windows_glazing", "solar_pv", "heating", "hot_water_tank_insulation", "windows_glazing", "solar_pv", "heating", "hot_water_tank_insulation",
"heating_control", "secondary_heating", "cylinder_thermostat", "mixed_glazing" "heating_control", "secondary_heating", "cylinder_thermostat", "mixed_glazing",
"extension_cavity_wall_insulation",
]: ]:
raise NotImplementedError( raise NotImplementedError(
"Implement me, given type %s" % recommendation["type"] "Implement me, given type %s" % recommendation["type"]

View file

@ -272,23 +272,10 @@ class GoogleSolarApi:
roi_summary = [] roi_summary = []
for segment in roof_segment_summaries: for segment in roof_segment_summaries:
if segment["panelsCount"] < min_panels:
continue
wattage = segment["panelsCount"] * self.insights_data["solarPotential"]["panelCapacityWatts"] wattage = segment["panelsCount"] * self.insights_data["solarPotential"]["panelCapacityWatts"]
generated_dc_energy = segment["yearlyEnergyDcKwh"] generated_dc_energy = segment["yearlyEnergyDcKwh"]
ratio = generated_dc_energy / wattage ratio = generated_dc_energy / wattage
if cost_instance is None:
cost = MCS_SOLAR_PV_COST_DATA["average_cost_per_kwh"] * (wattage / 1000)
else:
cost = cost_instance.solar_pv(
n_panels=segment["panelsCount"],
has_battery=False,
n_floors=property_instance.number_of_floors,
)["total"]
roi_summary.append( roi_summary.append(
{ {
"segmentIndex": segment["segmentIndex"], "segmentIndex": segment["segmentIndex"],
@ -296,7 +283,6 @@ class GoogleSolarApi:
"generated_dc_energy": generated_dc_energy, "generated_dc_energy": generated_dc_energy,
"ratio": ratio, "ratio": ratio,
"n_panels": segment["panelsCount"], "n_panels": segment["panelsCount"],
"cost": cost,
"panneled_roof_area": self.panel_area * int(segment["panelsCount"]) "panneled_roof_area": self.panel_area * int(segment["panelsCount"])
} }
) )
@ -305,10 +291,21 @@ class GoogleSolarApi:
if roi_summary.empty: if roi_summary.empty:
continue continue
if roi_summary["n_panels"].sum() < min_panels:
continue
if cost_instance is None:
total_cost = MCS_SOLAR_PV_COST_DATA["average_cost_per_kwh"] * (wattage / 1000)
else:
total_cost = cost_instance.solar_pv(
n_panels=roi_summary["n_panels"].sum(),
has_battery=False,
n_floors=property_instance.number_of_floors,
)["total"]
weighted_ratio = np.average( weighted_ratio = np.average(
roi_summary["ratio"].values, weights=roi_summary["generated_dc_energy"].values roi_summary["ratio"].values, weights=roi_summary["generated_dc_energy"].values
) )
total_cost = roi_summary["cost"].sum()
yearly_dc_energy = roi_summary["generated_dc_energy"].sum() yearly_dc_energy = roi_summary["generated_dc_energy"].sum()
panel_performance.append( panel_performance.append(

View file

@ -4,8 +4,10 @@ PESSIMISTIC_ASHP_EFFICIENCY = 200
AVERAGE_ASHP_EFFICIENCY = 250 AVERAGE_ASHP_EFFICIENCY = 250
# Conservative estimate of the proportion of electricity that will be consumed, whereas the rest will # Conservative estimate of the proportion of electricity that will be consumed, whereas the rest will
# be exported # be exported. These are averages based on Google research. E.g
# https://www.nea.org.uk/who-we-are/innovation-technical-evaluation/solarpv/solarpv-batteries
SOLAR_CONSUMPTION_PROPORTION = 0.5 SOLAR_CONSUMPTION_PROPORTION = 0.5
SOLAR_CONSUMPTION_WITH_BATTERY_PROPORTION = 0.7
# Typically, each solar panel takes up around 3.4 m2 of roof space under RdSAP. This was been verified in Elmhurst # Typically, each solar panel takes up around 3.4 m2 of roof space under RdSAP. This was been verified in Elmhurst
RDSAP_AREA_PER_PANEL = 3.4 RDSAP_AREA_PER_PANEL = 3.4

View file

@ -369,9 +369,9 @@ def extract_property_request_data(
property_non_invasive_recommendations["recommendations"] = str(transformed) property_non_invasive_recommendations["recommendations"] = str(transformed)
property_valution = next(( property_valution = next((
x for x in valuation_data if float(x["value"]) for x in valuation_data if
(str(x["uprn"]) == str(uprn)) (str(x["uprn"]) == str(uprn))
), {}) ), None)
return patch, property_already_installed, property_non_invasive_recommendations, property_valution return patch, property_already_installed, property_non_invasive_recommendations, property_valution

View file

@ -47,7 +47,12 @@ def app():
file_name=non_invasive_recommendations_filename file_name=non_invasive_recommendations_filename
) )
valuation_data = [{100050770761: 67_000}] valuation_data = [
{
"uprn": 100050770761,
"value": 67_000
}
]
# Store valuation data to s3 # Store valuation data to s3
valuation_filename = f"{USER_ID}/{PORTFOLIO_ID}/valuation.csv" valuation_filename = f"{USER_ID}/{PORTFOLIO_ID}/valuation.csv"
save_csv_to_s3( save_csv_to_s3(

View file

@ -129,10 +129,11 @@ class Recommendations:
phase += 1 phase += 1
# We handle recommendations covering specific non-invasive measures # We handle recommendations covering specific non-invasive measures
self.wall_recomender.recommend_extended(measures=measures) new_phase = self.wall_recomender.recommend_extended(phase=phase, measures=measures)
if self.wall_recomender.extended_recommendations: if self.wall_recomender.extended_recommendations:
property_recommendations.append(self.wall_recomender.extended_recommendations) property_recommendations.append(self.wall_recomender.extended_recommendations)
# We don't have any phasing here # We don't have any phasing here
phase = new_phase
self.roof_recommender.recommend(phase=phase, measures=measures, default_u_values=self.default_u_values) self.roof_recommender.recommend(phase=phase, measures=measures, default_u_values=self.default_u_values)
if self.roof_recommender.recommendations: if self.roof_recommender.recommendations:
@ -481,6 +482,27 @@ class Recommendations:
]: ]:
# We don't have a percieved sap impact of mechanical ventilation or trickle vents, and we don't # 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 # have the capacity to score draught proofing
if rec["type"] == "extension_cavity_wall_insulation":
previous_phase = [x for x in impact_summary if x["phase"] == (rec["phase"] - 1)]
if previous_phase:
sap = previous_phase[0]["sap"]
carbon = previous_phase[0]["carbon"]
heat_demand = previous_phase[0]["heat_demand"]
else:
sap = float(property_instance.data["current-energy-efficiency"])
carbon = float(property_instance.data["co2-emissions-current"])
heat_demand = float(property_instance.data["energy-consumption-current"])
impact_summary.append(
{
"phase": rec["phase"],
"recommendation_id": rec["recommendation_id"],
"sap": sap + rec["sap_points"],
"carbon": carbon - rec["co2_equivalent_savings"],
"heat_demand": heat_demand - rec["heat_demand"],
}
)
continue continue
phase_energy_efficiency_metrics = { phase_energy_efficiency_metrics = {
@ -571,6 +593,17 @@ class Recommendations:
property_phase_impact["carbon"], rec["co2_equivalent_savings"] property_phase_impact["carbon"], rec["co2_equivalent_savings"]
) )
if rec["type"] == "loft_insulation":
# When we have a loft insulation recommendation, where there is an extension and the existing
# amount of loft insulation is already good, we limit the SAP points
# By limiting here, we don't change the value in current_phase_values. This means that the
# future recommendations won't have an impact that is too large
li_sap_limit = RoofRecommendations.get_loft_insulation_sap_limit(
property_instance.data["roof-energy-eff"], property_instance.data["extension-count"]
)
if li_sap_limit is not None:
property_phase_impact["sap"] = min(property_phase_impact["sap"], li_sap_limit)
# Insert this information into the recommendation. # Insert this information into the recommendation.
if not rec.get("survey", False): if not rec.get("survey", False):
rec["sap_points"] = property_phase_impact["sap"] rec["sap_points"] = property_phase_impact["sap"]
@ -672,7 +705,11 @@ class Recommendations:
{ {
"phase": r["phase"], "phase": r["phase"],
"recommendation_id": r["recommendation_id"], "recommendation_id": r["recommendation_id"],
"solar_kwh_savings": r["initial_ac_kwh_per_year"] * assumptions.SOLAR_CONSUMPTION_PROPORTION, "solar_kwh_savings": (
r["initial_ac_kwh_per_year"] * assumptions.SOLAR_CONSUMPTION_PROPORTION
) if not r["has_battery"] else (
r["initial_ac_kwh_per_year"] * assumptions.SOLAR_CONSUMPTION_WITH_BATTERY_PROPORTION
),
} for recs in property_recommendations for r in recs if r["type"] == "solar_pv" } for recs in property_recommendations for r in recs if r["type"] == "solar_pv"
], columns=["phase", "recommendation_id", "solar_kwh_savings"]) ], columns=["phase", "recommendation_id", "solar_kwh_savings"])

View file

@ -59,6 +59,23 @@ class RoofRecommendations:
self.property.roof["is_flat"] self.property.roof["is_flat"]
) )
@classmethod
def get_loft_insulation_sap_limit(cls, roof_energy_eff, extension_count):
"""
Get the SAP limit for loft insulation
:param roof_energy_eff:
:return:
"""
if extension_count == 0:
# No limit
return None
if roof_energy_eff in ["Good", "Very Good"]:
return 1
return None
def mds_loft_insulation(self, phase): def mds_loft_insulation(self, phase):
""" """
For usages within the mds report For usages within the mds report
@ -273,7 +290,7 @@ class RoofRecommendations:
# loft is already partially insulated. # loft is already partially insulated.
# Note: This requirement is only for loft insulation # Note: This requirement is only for loft insulation
if ( if (
(material["depth"] + insulation_thickness) < self.MINIMUM_RECOMMENDED_LOFT_INSULATION material["depth"] < self.MINIMUM_RECOMMENDED_LOFT_INSULATION
) and is_pitched: ) and is_pitched:
continue continue
@ -295,6 +312,7 @@ class RoofRecommendations:
# We allow a small tolerance for error so we don't discount the recommendation entirely # We allow a small tolerance for error so we don't discount the recommendation entirely
if new_u_value <= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE: if new_u_value <= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE:
lowest_selected_u_value = update_lowest_selected_u_value(lowest_selected_u_value, new_u_value) lowest_selected_u_value = update_lowest_selected_u_value(lowest_selected_u_value, new_u_value)
cost_result = self.costs.loft_and_flat_insulation( cost_result = self.costs.loft_and_flat_insulation(

View file

@ -196,7 +196,7 @@ class SolarPvRecommendations:
] ]
roof_area = self.property.roof_area roof_area = self.property.roof_area
solar_configurations = panel_performance.head(3).reset_index(drop=True) solar_configurations = panel_performance.head(6).reset_index(drop=True)
# We combine each of these configurations with estimates with and without a battery # We combine each of these configurations with estimates with and without a battery
for rank, recommendation_config in solar_configurations.iterrows(): for rank, recommendation_config in solar_configurations.iterrows():

View file

@ -270,7 +270,7 @@ class WallRecommendations(Definitions):
# If the u-value is within regulations, we don't do anything # If the u-value is within regulations, we don't do anything
return return
def recommend_extended(self, measures): def recommend_extended(self, phase, measures):
""" """
Where we have extended measures, such as extension insulation, which cannot typically be picked up Where we have extended measures, such as extension insulation, which cannot typically be picked up
from the EPC api, we handle the recommendation of these here from the EPC api, we handle the recommendation of these here
@ -283,22 +283,24 @@ class WallRecommendations(Definitions):
measures_to_recommend = [measure for measure in measures if measure in extended_measures] measures_to_recommend = [measure for measure in measures if measure in extended_measures]
if not measures_to_recommend: if not measures_to_recommend:
return return phase
# We reset this to be empty # We reset this to be empty
self.extended_recommendations = [] self.extended_recommendations = []
recommendation_phase = phase
for measure in measures_to_recommend: for measure in measures_to_recommend:
if measure == "extension_cavity_wall_insulation": if measure == "extension_cavity_wall_insulation":
recommendation = self.recommend_extension_cavity_wall_insulation() recommendation = self.recommend_extension_cavity_wall_insulation(phase=recommendation_phase)
else: else:
raise NotImplementedError(f"Measure {measure} is not implemented") raise NotImplementedError(f"Measure {measure} is not implemented")
recommendation_phase += 1
self.extended_recommendations.append(recommendation) self.extended_recommendations.append(recommendation)
return return recommendation_phase
def recommend_extension_cavity_wall_insulation(self): def recommend_extension_cavity_wall_insulation(self, phase):
""" """
This function produces the recommendation for extension cavity wall insulation This function produces the recommendation for extension cavity wall insulation
:return: :return:
@ -331,7 +333,7 @@ class WallRecommendations(Definitions):
) )
recommendation = { recommendation = {
"phase": None, "phase": phase,
"parts": [], "parts": [],
"type": "extension_cavity_wall_insulation", "type": "extension_cavity_wall_insulation",
"measure_type": "extension_cavity_wall_insulation", "measure_type": "extension_cavity_wall_insulation",