Merge pull request #705 from Hestia-Homes/bug/sap-below-zero

handling cases where ventilation recommendation has a score taking a property below 1
This commit is contained in:
KhalimCK 2026-02-13 11:15:28 +00:00 committed by GitHub
commit b4d1dae748
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 131 additions and 13 deletions

View file

@ -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

View file

@ -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