mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
tweaking solar recommendations
This commit is contained in:
parent
800b673150
commit
d7ed4dd9a4
9 changed files with 92 additions and 30 deletions
|
|
@ -538,7 +538,8 @@ class Property:
|
|||
"loft_insulation", "room_roof_insulation", "flat_roof_insulation",
|
||||
"solid_floor_insulation", "suspended_floor_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(
|
||||
"Implement me, given type %s" % recommendation["type"]
|
||||
|
|
|
|||
|
|
@ -272,23 +272,10 @@ class GoogleSolarApi:
|
|||
|
||||
roi_summary = []
|
||||
for segment in roof_segment_summaries:
|
||||
|
||||
if segment["panelsCount"] < min_panels:
|
||||
continue
|
||||
|
||||
wattage = segment["panelsCount"] * self.insights_data["solarPotential"]["panelCapacityWatts"]
|
||||
generated_dc_energy = segment["yearlyEnergyDcKwh"]
|
||||
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(
|
||||
{
|
||||
"segmentIndex": segment["segmentIndex"],
|
||||
|
|
@ -296,7 +283,6 @@ class GoogleSolarApi:
|
|||
"generated_dc_energy": generated_dc_energy,
|
||||
"ratio": ratio,
|
||||
"n_panels": segment["panelsCount"],
|
||||
"cost": cost,
|
||||
"panneled_roof_area": self.panel_area * int(segment["panelsCount"])
|
||||
}
|
||||
)
|
||||
|
|
@ -305,10 +291,21 @@ class GoogleSolarApi:
|
|||
if roi_summary.empty:
|
||||
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(
|
||||
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()
|
||||
|
||||
panel_performance.append(
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@ PESSIMISTIC_ASHP_EFFICIENCY = 200
|
|||
AVERAGE_ASHP_EFFICIENCY = 250
|
||||
|
||||
# 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_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
|
||||
RDSAP_AREA_PER_PANEL = 3.4
|
||||
|
|
|
|||
|
|
@ -369,9 +369,9 @@ def extract_property_request_data(
|
|||
property_non_invasive_recommendations["recommendations"] = str(transformed)
|
||||
|
||||
property_valution = next((
|
||||
x for x in valuation_data if
|
||||
float(x["value"]) for x in valuation_data if
|
||||
(str(x["uprn"]) == str(uprn))
|
||||
), {})
|
||||
), None)
|
||||
|
||||
return patch, property_already_installed, property_non_invasive_recommendations, property_valution
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,12 @@ def app():
|
|||
file_name=non_invasive_recommendations_filename
|
||||
)
|
||||
|
||||
valuation_data = [{100050770761: 67_000}]
|
||||
valuation_data = [
|
||||
{
|
||||
"uprn": 100050770761,
|
||||
"value": 67_000
|
||||
}
|
||||
]
|
||||
# Store valuation data to s3
|
||||
valuation_filename = f"{USER_ID}/{PORTFOLIO_ID}/valuation.csv"
|
||||
save_csv_to_s3(
|
||||
|
|
|
|||
|
|
@ -129,10 +129,11 @@ class Recommendations:
|
|||
phase += 1
|
||||
|
||||
# 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:
|
||||
property_recommendations.append(self.wall_recomender.extended_recommendations)
|
||||
# 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)
|
||||
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
|
||||
# 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
|
||||
|
||||
phase_energy_efficiency_metrics = {
|
||||
|
|
@ -571,6 +593,17 @@ class Recommendations:
|
|||
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.
|
||||
if not rec.get("survey", False):
|
||||
rec["sap_points"] = property_phase_impact["sap"]
|
||||
|
|
@ -672,7 +705,11 @@ class Recommendations:
|
|||
{
|
||||
"phase": r["phase"],
|
||||
"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"
|
||||
], columns=["phase", "recommendation_id", "solar_kwh_savings"])
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,23 @@ class RoofRecommendations:
|
|||
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):
|
||||
"""
|
||||
For usages within the mds report
|
||||
|
|
@ -273,7 +290,7 @@ class RoofRecommendations:
|
|||
# loft is already partially insulated.
|
||||
# Note: This requirement is only for loft insulation
|
||||
if (
|
||||
(material["depth"] + insulation_thickness) < self.MINIMUM_RECOMMENDED_LOFT_INSULATION
|
||||
material["depth"] < self.MINIMUM_RECOMMENDED_LOFT_INSULATION
|
||||
) and is_pitched:
|
||||
continue
|
||||
|
||||
|
|
@ -295,6 +312,7 @@ class RoofRecommendations:
|
|||
|
||||
# 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:
|
||||
|
||||
lowest_selected_u_value = update_lowest_selected_u_value(lowest_selected_u_value, new_u_value)
|
||||
|
||||
cost_result = self.costs.loft_and_flat_insulation(
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ class SolarPvRecommendations:
|
|||
]
|
||||
|
||||
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
|
||||
for rank, recommendation_config in solar_configurations.iterrows():
|
||||
|
|
|
|||
|
|
@ -270,7 +270,7 @@ class WallRecommendations(Definitions):
|
|||
# If the u-value is within regulations, we don't do anything
|
||||
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
|
||||
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]
|
||||
if not measures_to_recommend:
|
||||
return
|
||||
return phase
|
||||
|
||||
# We reset this to be empty
|
||||
self.extended_recommendations = []
|
||||
|
||||
recommendation_phase = phase
|
||||
for measure in measures_to_recommend:
|
||||
if measure == "extension_cavity_wall_insulation":
|
||||
recommendation = self.recommend_extension_cavity_wall_insulation()
|
||||
recommendation = self.recommend_extension_cavity_wall_insulation(phase=recommendation_phase)
|
||||
else:
|
||||
raise NotImplementedError(f"Measure {measure} is not implemented")
|
||||
recommendation_phase += 1
|
||||
|
||||
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
|
||||
:return:
|
||||
|
|
@ -331,7 +333,7 @@ class WallRecommendations(Definitions):
|
|||
)
|
||||
|
||||
recommendation = {
|
||||
"phase": None,
|
||||
"phase": phase,
|
||||
"parts": [],
|
||||
"type": "extension_cavity_wall_insulation",
|
||||
"measure_type": "extension_cavity_wall_insulation",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue