added some basic level of override so we force solar recommendations if we have inspections

This commit is contained in:
Khalim Conn-Kowlessar 2025-11-03 14:44:36 +00:00
parent 23eb26527c
commit 76716f35d3
3 changed files with 72 additions and 11 deletions

View file

@ -479,9 +479,7 @@ class GoogleSolarApi:
roi_results = pd.DataFrame(roi_results)
panel_performance = panel_performance.merge(
roi_results, how="left", on="n_panels"
)
panel_performance = panel_performance.merge(roi_results, how="left", on="n_panels")
# We want max roi, minimal generation deficit, and max generation value - we create a ranking score
# Assign equal weights to each metric
@ -742,7 +740,7 @@ class GoogleSolarApi:
@classmethod
def building_solar_analysis(
cls, building_solar_config: List, input_properties: List[Property], session, google_solar_api_key: str,
solar_materials: list
solar_materials: list,
):
"""
Perform the solar analysis for the building level
@ -826,9 +824,21 @@ class GoogleSolarApi:
@classmethod
def unit_solar_analysis(
cls, unit_solar_config: List, input_properties: List[Property], session, body, google_solar_api_key: str,
solar_materials: list
solar_materials: list, inspections_map: dict
):
"""
Perform the solar analysis for the unit level
:param unit_solar_config: List of unit solar configurations
:param input_properties: List of properties
:param session: Database session
:param body: PlanTriggerRequest instance
:param google_solar_api_key: Google Solar API key
:param solar_materials: List of solar materials
:param inspections_map: Dictionary mapping property IDs to inspection data
:return:
"""
if not unit_solar_config:
return input_properties
@ -879,6 +889,15 @@ class GoogleSolarApi:
property_instance=property_instance,
)
property_inspections = inspections_map.get(property_instance.id, {})
if property_inspections:
# If we have some inspections data, we check if we have some data which indicates solar cannot
# be installed. We're loose about this now since this is post review
if solar_api_client.panel_performance.empty:
# We assume solar is a suitable option
solar_api_client.panel_performance = solar_api_client.default_panel_performance(property_instance)
# Store the data in the database
solar_api_client.save_to_db(
session=session,
@ -923,12 +942,43 @@ class GoogleSolarApi:
None
)
if material_1_6 is None or material_3_2 is None:
material_4_35 = next(
(m for m in self.solar_materials if m["type"] == "solar_pv" and
abs(m["size"] - 4.35) < 0.1 and not m["includes_battery"]),
None
)
if material_1_6 is None or material_3_2 is None or material_4_35 is None:
raise ValueError("No suitable solar product found for the default configuration.")
# We return a 1.6 and 3.2 kwp system
panel_performance = pd.DataFrame(
[
{
'n_panels': 10,
'yearly_dc_energy': 4350 * assumptions.MEDIAN_WATTAGE_TO_DC,
'total_cost': cost_instance.solar_pv(
solar_product=material_4_35,
scaffolding_options=[
{"total_cost": 1000, "size": property_instance.number_of_floors},
{"total_cost": 1000, "size": 3}
],
n_floors=property_instance.number_of_floors
)["total"],
'weighted_ratio': None,
'panneled_roof_area': 9 * assumptions.RDSAP_AREA_PER_PANEL,
'array_wattage': 4350,
'initial_ac_kwh_per_year': 4350 * assumptions.MEDIAN_WATTAGE_TO_AC,
'lifetime_ac_kwh': None,
'lifetime_dc_kwh': None,
'roi': None,
'generation_value': None,
'generation_deficit': None,
'expected_payback_years': None,
'surplus': None,
'combined_score': None,
'rank': None
},
{
'n_panels': 8,
'yearly_dc_energy': 3200 * assumptions.MEDIAN_WATTAGE_TO_DC,

View file

@ -139,7 +139,7 @@ def parse_eco_packages(config: dict[str, Any]) -> tuple[list[str], int, str] | t
"plan_type": "solar_eco4"
},
"Solar Eligible, Needs Heating Upgrade": {
"measures": ["solar_pv", "loft_insulation", "high_heat_retention_storage_heater"],
"measures": ["solar_pv", "loft_insulation", "high_heat_retention_storage_heater", "mechanical_ventilation"],
"target_sap": 86, # High B
"plan_type": "solar_hhrsh_eco4"
},

View file

@ -400,8 +400,13 @@ async def model_engine(body: PlanTriggerRequest):
plan_input = plan_input.rename(
columns={"domna_address_1": "address", "domna_postcode": "postcode", "epc_os_uprn": "uprn"}
)
# Where the EPC has been estimated, that is because a UPRN wasn't avaialble and so we remote UPRN
plan_input["uprn"] = np.where(plan_input["estimated"].isin([1, True]), None, plan_input["uprn"])
# Where the EPC has been estimated, that is because a UPRN wasn't avaialble and so we remove UPRN
# This will be reflexted
plan_input["uprn"] = np.where(
plan_input["estimated"].isin([1, True]) & (
(plan_input["uprn"] < 0) | pd.isnull(plan_input["uprn"])
), None, plan_input["uprn"]
)
# We handle the landlord property type and built form
plan_input["property_type"] = plan_input["landlord_property_type"].copy()
if "landlord_built_form" in plan_input.columns:
@ -512,7 +517,9 @@ async def model_engine(body: PlanTriggerRequest):
epc_searcher.ordnance_survey_client.property_type = config.get("property_type", None)
# For the moment, our OS API access is unavailable, so we skip and interpolate
epc_searcher.find_property(skip_os=True)
if epc_searcher.newest_epc.get("estimated") and body.file_format == "domna_asset_list":
if epc_searcher.newest_epc.get("estimated") and body.file_format == "domna_asset_list" and (
epc_searcher.newest_epc["uprn"] < 0
):
epc_searcher.newest_epc["uprn-source"] = epc_searcher.UPRN_SOURCE_SIMULATED
# We check for an energy assessment we have performed on this property:
@ -678,7 +685,7 @@ async def model_engine(body: PlanTriggerRequest):
input_properties=input_properties,
session=session,
google_solar_api_key=get_settings().GOOGLE_SOLAR_API_KEY,
solar_materials=[m for m in materials if m["type"] == "solar_pv"]
solar_materials=[m for m in materials if m["type"] == "solar_pv"],
)
input_properties = GoogleSolarApi.unit_solar_analysis(
@ -688,8 +695,12 @@ async def model_engine(body: PlanTriggerRequest):
body=body,
solar_materials=[m for m in materials if m["type"] == "solar_pv"],
google_solar_api_key=get_settings().GOOGLE_SOLAR_API_KEY,
inspections_map=inspections_map
)
# We also make a tweak - if the property has been flagged for solar but doesn't contain
# any panel performance, we ensure that we have a 3kWp and 4kWp option for the property
logger.info("Identifying property recommendations")
recommendations = {}
recommendations_scoring_data = []