diff --git a/recommendations/Recommendations.py b/recommendations/Recommendations.py index e470c1a3..acd49e05 100644 --- a/recommendations/Recommendations.py +++ b/recommendations/Recommendations.py @@ -768,6 +768,24 @@ class Recommendations: # Update the current phase values current_phase_values["sap"] = previous_phase_values["sap"] + property_phase_impact["sap"] + + # This is very much an edge case but we also the end result taking the property + # below a SAP rating of 1, which is the minimum SAP rating + if previous_phase_values["sap"] + property_phase_impact["sap"] < 1: + sap_adjustment = 1 - (previous_phase_values["sap"] + property_phase_impact["sap"]) + adjustments.append( + { + "recommendation_id": rec["recommendation_id"], + "phase": rec["phase"], + "sap_adjustment": sap_adjustment, + } + ) + # The new impact should be the current impact plus the adjustment + property_phase_impact["sap"] = property_phase_impact["sap"] + sap_adjustment + + # Update the current phase values + current_phase_values["sap"] = previous_phase_values["sap"] + property_phase_impact["sap"] + elif 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 diff --git a/recommendations/tests/test_recommendations.py b/recommendations/tests/test_recommendations.py index a9915422..e3bcbb2f 100644 --- a/recommendations/tests/test_recommendations.py +++ b/recommendations/tests/test_recommendations.py @@ -347,21 +347,21 @@ def property_instance(): "input_data, expected", [ ( - [ - {"recommendation_id": "a", "phase": 0, "sap_adjustment": 1.7}, - {"recommendation_id": "b", "phase": 0, "sap_adjustment": 1.7}, - ], - [{"recommendation_id": "a", "phase": 0, "sap_adjustment": 1.7}], + [ + {"recommendation_id": "a", "phase": 0, "sap_adjustment": 1.7}, + {"recommendation_id": "b", "phase": 0, "sap_adjustment": 1.7}, + ], + [{"recommendation_id": "a", "phase": 0, "sap_adjustment": 1.7}], ), ( - [ - {"recommendation_id": "a", "phase": 1, "sap_adjustment": 2}, - {"recommendation_id": "b", "phase": 2, "sap_adjustment": 3}, - ], - [ - {"recommendation_id": "a", "phase": 1, "sap_adjustment": 2}, - {"recommendation_id": "b", "phase": 2, "sap_adjustment": 3}, - ], + [ + {"recommendation_id": "a", "phase": 1, "sap_adjustment": 2}, + {"recommendation_id": "b", "phase": 2, "sap_adjustment": 3}, + ], + [ + {"recommendation_id": "a", "phase": 1, "sap_adjustment": 2}, + {"recommendation_id": "b", "phase": 2, "sap_adjustment": 3}, + ], ), ], ) @@ -1478,3 +1478,103 @@ def test_lighting_and_loft_adjustment_combined(property_instance, heat_demand_pr {'recommendation_id': '0_phase=0', 'phase': 0, 'sap_adjustment': np.float64(1.7)}, {'recommendation_id': '4_phase=2', 'phase': 2, 'sap_adjustment': np.float64(4.0)} ] + + +def test_mechanical_ventilation_sap_floor(property_instance): + rec = { + "type": "mechanical_ventilation", + "recommendation_id": "mv_test", + "phase": 1, + } + + previous_phase_values = {"sap": 2.0} + current_phase_values = {"sap": 0.5} # model prediction already below 1 + property_phase_impact = {"sap": -1.5, "carbon": 0, "heat_demand": 0} + adjustments = [] + + updated_impact, updated_current, updated_adjustments = ( + Recommendations._apply_measure_specific_rules( + rec=rec, + property_phase_impact=property_phase_impact, + previous_phase_values=previous_phase_values, + current_phase_values=current_phase_values, + adjustments=adjustments, + property_instance=property_instance + ) + ) + + # SAP should be clamped to minimum 1 + assert updated_current["sap"] == 1.0 + + # Original final SAP would have been 0.5 → so adjustment = 1 - 0.5 = 0.5 + assert updated_adjustments == [ + { + "recommendation_id": "mv_test", + "phase": 1, + "sap_adjustment": 0.5, + } + ] + + # Impact should now reflect new clamped SAP + assert updated_impact["sap"] == -1.0 # 2.0 → 1.0 + + +def test_mechanical_ventilation_no_floor_adjustment(property_instance): + rec = { + "type": "mechanical_ventilation", + "recommendation_id": "mv_test", + "phase": 1, + } + + previous_phase_values = {"sap": 5.0} + current_phase_values = {"sap": 3.0} + property_phase_impact = {"sap": -2.0, "carbon": 0, "heat_demand": 0} + adjustments = [] + + updated_impact, updated_current, updated_adjustments = ( + Recommendations._apply_measure_specific_rules( + rec=rec, + property_phase_impact=property_phase_impact, + previous_phase_values=previous_phase_values, + current_phase_values=current_phase_values, + adjustments=adjustments, + property_instance=property_instance + ) + ) + + # No adjustment expected + assert updated_adjustments == [] + + # SAP unchanged + assert updated_current["sap"] == 3.0 + assert updated_impact["sap"] == -2.0 + + +def test_mechanical_ventilation_exactly_one_no_adjustment(property_instance): + # Test when SAP = 1 + rec = { + "type": "mechanical_ventilation", + "recommendation_id": "mv_test", + "phase": 1, + } + + previous_phase_values = {"sap": 2.0} + current_phase_values = {"sap": 1.0} + property_phase_impact = {"sap": -1.0, "carbon": 0, "heat_demand": 0} + adjustments = [] + + updated_impact, updated_current, updated_adjustments = ( + Recommendations._apply_measure_specific_rules( + rec=rec, + property_phase_impact=property_phase_impact, + previous_phase_values=previous_phase_values, + current_phase_values=current_phase_values, + adjustments=adjustments, + property_instance=property_instance + ) + ) + + # Exactly 1 → no adjustment + assert updated_adjustments == [] + assert updated_current["sap"] == 1.0 + assert updated_impact["sap"] == -1.0