From b35d021db98c620c3cd45ea6bf3afb749bea0acf Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 19 Mar 2025 19:21:11 +0000 Subject: [PATCH] starting to tidy up tweaks to optimiser --- asset_list/mappings/built_form.py | 28 ++++++++++++- asset_list/mappings/property_type.py | 25 +++++++++++- asset_list/mappings/walls.py | 3 +- backend/app/assumptions.py | 6 +++ backend/app/plan/router.py | 39 +++++++------------ .../optimiser/optimiser_functions.py | 14 ++++--- 6 files changed, 79 insertions(+), 36 deletions(-) diff --git a/asset_list/mappings/built_form.py b/asset_list/mappings/built_form.py index 87f36985..915f84c6 100644 --- a/asset_list/mappings/built_form.py +++ b/asset_list/mappings/built_form.py @@ -3,7 +3,7 @@ STANDARD_BUILT_FORMS = { # Houses "end-terrace", "semi-detached", "detached", "mid-terrace", # Flats - "ground floor", "mid-floor", "top-floor" + "ground floor", "mid-floor", "top-floor", "basement" } BUILT_FORM_MAPPINGS = { @@ -16,5 +16,29 @@ BUILT_FORM_MAPPINGS = { 'Maisonette': 'unknown', 'Flat': 'unknown', 'First Floor Flat General': 'mid-floor', - 'Bungalow (Semi)': 'semi-detached' + 'Bungalow (Semi)': 'semi-detached', + + 'Detached House': 'detached', + 'End Terraced House': 'end-terrace', + 'Studio (Ground floor)': 'ground floor', + 'Mid Terraced House': 'mid-terrace', + 'Ground Floor Flat': 'ground floor', + 'Semi Detached House': 'semi-detached', + 'Detached Property': 'detached', + 'Level not confirmed': 'unknown', + 'Bedsit': 'unknown', + 'Cottage': 'detached', + 'Terraced House': 'mid-terrace', + 'Studio (1st Floor)': 'ground floor', + 'Standard Maisonette': 'unknown', + 'Third Floor Flat or Above': 'top-floor', + 'Town House': 'end-terrace', + 'Guest room in a complex': 'unknown', + 'Back To Back House': 'mid-terrace', + 'PIMSS EMPTY': 'unknown', + 'Flat Basement': 'basement', + 'House': 'unknown', + 'Second Floor Flat': 'mid-floor', + 'First Floor Flat': 'ground floor', + 'Room Only': 'unknown' } diff --git a/asset_list/mappings/property_type.py b/asset_list/mappings/property_type.py index 3182bd45..add53cd8 100644 --- a/asset_list/mappings/property_type.py +++ b/asset_list/mappings/property_type.py @@ -70,6 +70,27 @@ PROPERTY_MAPPING = { 'House (Mid terrace)': 'house', 'Bungalow (Semi)': 'bungalow', 'Ground Floor Flat General': 'flat', - 'House (Semi)': 'house' - + 'House (Semi)': 'house', + 'Detached House': 'house', + 'Bedsit': 'bedsit', + 'Terraced House': 'house', + 'Standard Maisonette': 'maisonette', + 'End Terraced House': 'house', + 'Third Floor Flat or Above': 'flat', + 'Town House': 'house', + 'Mid Terraced House': 'house', + 'Back To Back House': 'house', + 'Flat Basement': 'flat', + 'Ground Floor Flat': 'flat', + 'Semi Detached House': 'house', + 'Second Floor Flat': 'flat', + 'First Floor Flat': 'flat', + 'Level not confirmed': 'flat', + 'Cottage': 'house', + 'Studio (1st Floor)': 'flat', + 'Studio (Ground floor)': 'flat', + 'Guest room in a complex': 'other', + 'PIMSS EMPTY': 'bedsit', + 'Room Only': 'other', + 'Detached Property': 'house' } diff --git a/asset_list/mappings/walls.py b/asset_list/mappings/walls.py index f3156860..894d9e01 100644 --- a/asset_list/mappings/walls.py +++ b/asset_list/mappings/walls.py @@ -134,5 +134,6 @@ WALL_CONSTRUCTION_MAPPINGS = { 'Cavity CWI required': 'uninsulated cavity', 'Solid brick EWI installed': 'insulated solid brick', 'Cavity Cavity batts': 'filled cavity', - 'Cavity CWI Completed by Dyson': 'filled cavity' + 'Cavity CWI Completed by Dyson': 'filled cavity', + None: "unknown" } diff --git a/backend/app/assumptions.py b/backend/app/assumptions.py index 261e2b62..f1090ef3 100644 --- a/backend/app/assumptions.py +++ b/backend/app/assumptions.py @@ -59,3 +59,9 @@ DESCRIPTIONS_TO_FUEL_TYPES = { "Boiler and radiators, coal": {"fuel": "Coal", "cop": 0.85}, "From main system, no cylinderstat": {"fuel": "Natural Gas", "cop": 0.85}, } + +# These are the measure types where if there is a ventilation recommendation, we force the inclusion of it +# if one of these has been recommended. +measures_needing_ventilation = [ + "internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation" +] diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py index d55a4f73..cce47566 100644 --- a/backend/app/plan/router.py +++ b/backend/app/plan/router.py @@ -27,6 +27,7 @@ from backend.app.dependencies import validate_token from backend.app.plan.schemas import PlanTriggerRequest from backend.app.plan.utils import get_cleaned from backend.app.utils import epc_to_sap_lower_bound, sap_to_epc +import backend.app.assumptions as assumptions from backend.ml_models.api import ModelApi from backend.Property import Property @@ -707,32 +708,25 @@ async def trigger_plan(body: PlanTriggerRequest): # we need to double unlist because we have a list of lists property_measure_types = {rec["type"] for recs in recommendations[p.id] for rec in recs} - measures_to_optimise = recommendations[p.id] - property_required_measures = [] - if body.required_measures: - property_required_measures = [ - m for m in measures_to_optimise if m[0]["type"] in body.required_measures - ] - measures_to_optimise = [ - m for m in measures_to_optimise if m[0]["type"] not in body.required_measures - ] + property_required_measures = [ + m for m in recommendations[p.id] if m[0]["type"] in body.required_measures + ] + measures_to_optimise = [ + m for m in recommendations[p.id] if m[0]["type"] not in body.required_measures + ] # If we have a wall insulation measure, we MUST include mechanical ventilation # Additionally, if we have required measures, they should also be included. Therefore # we can discount the number of points required to get to the target SAP band (or increase) # in the case of ventilation - measures_needing_ventilation = [ - "internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation" - ] - needs_ventilation = any(x in property_measure_types for x in measures_needing_ventilation) + needs_ventilation = any(x in property_measure_types for x in assumptions.measures_needing_ventilation) - input_measures = prepare_input_measures( - measures_to_optimise, body.goal, needs_ventilation, measures_needing_ventilation - ) + input_measures = prepare_input_measures(measures_to_optimise, body.goal, needs_ventilation) if not input_measures[0]: # This means that we have no defaults selected_recommendations = {} + solution = [] else: fixed_gain = 0 @@ -755,7 +749,7 @@ async def trigger_plan(body: PlanTriggerRequest): # if the property needs ventilation, but the measure we optimise didn't include # venilation we add the points for ventilation as a fixed gain if needs_ventilation and any( - r in property_required_measure_types for r in measures_needing_ventilation + r in property_required_measure_types for r in assumptions.measures_needing_ventilation ): fixed_gain += next( (r[0]["sap_points"] for r in recommendations[p.id] if @@ -823,9 +817,7 @@ async def trigger_plan(body: PlanTriggerRequest): ) # If wall insulation is selected, we also include mechanical ventilation as a best practice measure - if any(x in [r["type"] for r in solution] for x in [ - "internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation" - ]): + if any(x in [r["type"] for r in solution] for x in assumptions.measures_needing_ventilation): ventilation_rec = next( (r[0] for r in recommendations[p.id] if r[0]["type"] == "mechanical_ventilation"), None @@ -854,14 +846,9 @@ async def trigger_plan(body: PlanTriggerRequest): ] # We'll also unlist the recommendations so they're a bit easier to handle from here onwards - final_recommendations = [ + recommendations[p.id] = [ rec for recommendations_by_type in final_recommendations for rec in recommendations_by_type ] - # Get defaults - defaults = [r for r in final_recommendations if r["default"]] - sum([r['sap_points'] for r in defaults]) - - recommendations[p.id] = final_recommendations # when we have buildings, we tweak our solar PV recommendations as if one unit needs it, we apply it to all # of them diff --git a/recommendations/optimiser/optimiser_functions.py b/recommendations/optimiser/optimiser_functions.py index a0c3719d..05b9ec42 100644 --- a/recommendations/optimiser/optimiser_functions.py +++ b/recommendations/optimiser/optimiser_functions.py @@ -1,4 +1,7 @@ -def prepare_input_measures(property_recommendations, goal, needs_ventilation, measures_needing_ventilation): +import backend.app.assumptions as assumptions + + +def prepare_input_measures(property_recommendations, goal, needs_ventilation): """ Basic function to convert recommendations_to_upload to a format that is suitable for the optimiser - large @@ -6,7 +9,6 @@ def prepare_input_measures(property_recommendations, goal, needs_ventilation, me :param goal: goal to be optimised for, should be one of the keys in gain_map. E.g. if the gain is SAP points, the goal should reflect that desired gain :param needs_ventilation: boolean to indicate if the property needs ventilation - :param measures_needing_ventilation: list of measures that need ventilation :return: Nested list of input measures """ @@ -44,18 +46,20 @@ def prepare_input_measures(property_recommendations, goal, needs_ventilation, me for rec in recs: # We bundle the impact of ventilation with the measure total = ( - rec["total"] + ventilation_recommendation["total"] if rec["type"] in measures_needing_ventilation + rec["total"] + ventilation_recommendation["total"] + if rec["type"] in assumptions.measures_needing_ventilation else rec["total"] ) gain = ( - rec[goal_key] + ventilation_recommendation[goal_key] if rec["type"] in measures_needing_ventilation + rec[goal_key] + ventilation_recommendation[goal_key] + if rec["type"] in assumptions.measures_needing_ventilation else rec[goal_key] ) rec_type = ( "+".join( [rec["type"], ventilation_recommendation["type"]] - ) if rec["type"] in measures_needing_ventilation + ) if rec["type"] in assumptions.measures_needing_ventilation else rec["type"] )