Model/recommendations/tests/test_recommendations.py
Khalim Conn-Kowlessar 088ea1e1c2 zero gain
2026-02-24 19:54:31 +00:00

1765 lines
88 KiB
Python

import pytest
import pandas as pd
import numpy as np
from unittest.mock import Mock
from recommendations.Recommendations import Recommendations
@pytest.fixture
def heat_demand_predictions():
return pd.DataFrame(
[
{'id': '614626+0_phase=0', 'predictions': 256.6, 'property_id': '614626',
'recommendation_id': '0_phase=0',
'phase': 0},
{'id': '614626+1_phase=0', 'predictions': 256.6, 'property_id': '614626',
'recommendation_id': '1_phase=0',
'phase': 0},
{'id': '614626+2_phase=0', 'predictions': 256.6, 'property_id': '614626',
'recommendation_id': '2_phase=0',
'phase': 0},
{'id': '614626+3_phase=1', 'predictions': 263.1, 'property_id': '614626',
'recommendation_id': '3_phase=1',
'phase': 1},
{'id': '614626+4_phase=2', 'predictions': 259.0, 'property_id': '614626',
'recommendation_id': '4_phase=2',
'phase': 2},
{'id': '614626+5_phase=3', 'predictions': 250.5, 'property_id': '614626',
'recommendation_id': '5_phase=3',
'phase': 3},
{'id': '614626+6_phase=3', 'predictions': 245.7, 'property_id': '614626',
'recommendation_id': '6_phase=3',
'phase': 3},
{'id': '614626+7_phase=3', 'predictions': 199.7, 'property_id': '614626',
'recommendation_id': '7_phase=3',
'phase': 3},
{'id': '614626+8_phase=4', 'predictions': 250.5, 'property_id': '614626',
'recommendation_id': '8_phase=4',
'phase': 4},
{'id': '614626+9_phase=5', 'predictions': 139.5, 'property_id': '614626',
'recommendation_id': '9_phase=5',
'phase': 5}, {'id': '614626+10_phase=5', 'predictions': 139.5, 'property_id': '614626',
'recommendation_id': '10_phase=5', 'phase': 5},
{'id': '614626+11_phase=5', 'predictions': 139.5, 'property_id': '614626',
'recommendation_id': '11_phase=5', 'phase': 5},
{'id': '614626+12_phase=5', 'predictions': 133.6, 'property_id': '614626',
'recommendation_id': '12_phase=5', 'phase': 5},
{'id': '614626+13_phase=5', 'predictions': 133.6, 'property_id': '614626',
'recommendation_id': '13_phase=5', 'phase': 5},
{'id': '614626+14_phase=5', 'predictions': 133.6, 'property_id': '614626',
'recommendation_id': '14_phase=5', 'phase': 5},
{'id': '614626+15_phase=5', 'predictions': 133.6, 'property_id': '614626',
'recommendation_id': '15_phase=5', 'phase': 5},
{'id': '614626+16_phase=5', 'predictions': 133.6, 'property_id': '614626',
'recommendation_id': '16_phase=5', 'phase': 5},
{'id': '614626+17_phase=5', 'predictions': 133.6, 'property_id': '614626',
'recommendation_id': '17_phase=5', 'phase': 5},
{'id': '614626+18_phase=5', 'predictions': 133.6, 'property_id': '614626',
'recommendation_id': '18_phase=5', 'phase': 5},
{'id': '614626+19_phase=5', 'predictions': 114.3, 'property_id': '614626',
'recommendation_id': '19_phase=5', 'phase': 5},
{'id': '614626+20_phase=5', 'predictions': 114.3, 'property_id': '614626',
'recommendation_id': '20_phase=5', 'phase': 5},
{'id': '614626+21_phase=5', 'predictions': 114.3, 'property_id': '614626',
'recommendation_id': '21_phase=5', 'phase': 5},
{'id': '614626+22_phase=5', 'predictions': 114.3, 'property_id': '614626',
'recommendation_id': '22_phase=5', 'phase': 5},
{'id': '614626+23_phase=5', 'predictions': 114.3, 'property_id': '614626',
'recommendation_id': '23_phase=5', 'phase': 5},
{'id': '614626+24_phase=5', 'predictions': 114.3, 'property_id': '614626',
'recommendation_id': '24_phase=5', 'phase': 5},
{'id': '614626+25_phase=5', 'predictions': 102.5, 'property_id': '614626',
'recommendation_id': '25_phase=5', 'phase': 5},
{'id': '614626+26_phase=5', 'predictions': 102.5, 'property_id': '614626',
'recommendation_id': '26_phase=5', 'phase': 5},
{'id': '614626+27_phase=5', 'predictions': 102.5, 'property_id': '614626',
'recommendation_id': '27_phase=5', 'phase': 5},
{'id': '614626+28_phase=5', 'predictions': 102.5, 'property_id': '614626',
'recommendation_id': '28_phase=5', 'phase': 5},
{'id': '614626+29_phase=5', 'predictions': 82.5, 'property_id': '614626',
'recommendation_id': '29_phase=5', 'phase': 5},
{'id': '614626+30_phase=5', 'predictions': 130.0, 'property_id': '614626',
'recommendation_id': '30_phase=5', 'phase': 5},
{'id': '614626+31_phase=5', 'predictions': 130.0, 'property_id': '614626',
'recommendation_id': '31_phase=5', 'phase': 5},
{'id': '614626+32_phase=5', 'predictions': 130.0, 'property_id': '614626',
'recommendation_id': '32_phase=5', 'phase': 5},
{'id': '614626+33_phase=5', 'predictions': 114.3, 'property_id': '614626',
'recommendation_id': '33_phase=5', 'phase': 5},
{'id': '614626+34_phase=5', 'predictions': 114.3, 'property_id': '614626',
'recommendation_id': '34_phase=5', 'phase': 5},
{'id': '614626+35_phase=5', 'predictions': 114.3, 'property_id': '614626',
'recommendation_id': '35_phase=5', 'phase': 5},
{'id': '614626+36_phase=5', 'predictions': 114.3, 'property_id': '614626',
'recommendation_id': '36_phase=5', 'phase': 5},
{'id': '614626+37_phase=5', 'predictions': 169.2, 'property_id': '614626',
'recommendation_id': '37_phase=5', 'phase': 5},
{'id': '614626+38_phase=5', 'predictions': 169.2, 'property_id': '614626',
'recommendation_id': '38_phase=5', 'phase': 5},
{'id': '614626+39_phase=5', 'predictions': 169.2, 'property_id': '614626',
'recommendation_id': '39_phase=5', 'phase': 5},
{'id': '614626+40_phase=5', 'predictions': 155.1, 'property_id': '614626',
'recommendation_id': '40_phase=5', 'phase': 5},
{'id': '614626+41_phase=5', 'predictions': 155.1, 'property_id': '614626',
'recommendation_id': '41_phase=5', 'phase': 5},
{'id': '614626+42_phase=5', 'predictions': 155.1, 'property_id': '614626',
'recommendation_id': '42_phase=5', 'phase': 5},
{'id': '614626+43_phase=5', 'predictions': 155.1, 'property_id': '614626',
'recommendation_id': '43_phase=5', 'phase': 5},
{'id': '614626+44_phase=5', 'predictions': 133.6, 'property_id': '614626',
'recommendation_id': '44_phase=5', 'phase': 5},
{'id': '614626+45_phase=5', 'predictions': 133.6, 'property_id': '614626',
'recommendation_id': '45_phase=5', 'phase': 5},
{'id': '614626+46_phase=5', 'predictions': 133.6, 'property_id': '614626',
'recommendation_id': '46_phase=5', 'phase': 5},
{'id': '614626+47_phase=5', 'predictions': 130.0, 'property_id': '614626',
'recommendation_id': '47_phase=5', 'phase': 5},
{'id': '614626+48_phase=5', 'predictions': 130.0, 'property_id': '614626',
'recommendation_id': '48_phase=5', 'phase': 5},
{'id': '614626+49_phase=5', 'predictions': 130.0, 'property_id': '614626',
'recommendation_id': '49_phase=5', 'phase': 5},
{'id': '614626+50_phase=5', 'predictions': 130.0, 'property_id': '614626',
'recommendation_id': '50_phase=5', 'phase': 5},
{'id': '614626+51_phase=5', 'predictions': 130.0, 'property_id': '614626',
'recommendation_id': '51_phase=5', 'phase': 5},
{'id': '614626+52_phase=5', 'predictions': 130.0, 'property_id': '614626',
'recommendation_id': '52_phase=5', 'phase': 5},
{'id': '614626+53_phase=5', 'predictions': 130.0, 'property_id': '614626',
'recommendation_id': '53_phase=5', 'phase': 5},
{'id': '614626+54_phase=5', 'predictions': 130.0, 'property_id': '614626',
'recommendation_id': '54_phase=5', 'phase': 5},
{'id': '614626+55_phase=5', 'predictions': 182.6, 'property_id': '614626',
'recommendation_id': '55_phase=5', 'phase': 5},
{'id': '614626+56_phase=5', 'predictions': 169.2, 'property_id': '614626',
'recommendation_id': '56_phase=5', 'phase': 5},
{'id': '614626+57_phase=5', 'predictions': 169.2, 'property_id': '614626',
'recommendation_id': '57_phase=5', 'phase': 5}
]
)
@pytest.fixture
def carbon_predictions():
return pd.DataFrame(
[
{'id': '614626+0_phase=0', 'predictions': 2.2, 'property_id': '614626',
'recommendation_id': '0_phase=0',
'phase': 0},
{'id': '614626+1_phase=0', 'predictions': 2.2, 'property_id': '614626',
'recommendation_id': '1_phase=0',
'phase': 0},
{'id': '614626+2_phase=0', 'predictions': 2.2, 'property_id': '614626',
'recommendation_id': '2_phase=0',
'phase': 0},
{'id': '614626+3_phase=1', 'predictions': 2.2, 'property_id': '614626',
'recommendation_id': '3_phase=1',
'phase': 1},
{'id': '614626+4_phase=2', 'predictions': 2.2, 'property_id': '614626',
'recommendation_id': '4_phase=2',
'phase': 2},
{'id': '614626+5_phase=3', 'predictions': 2.1, 'property_id': '614626',
'recommendation_id': '5_phase=3',
'phase': 3},
{'id': '614626+6_phase=3', 'predictions': 2.1, 'property_id': '614626',
'recommendation_id': '6_phase=3',
'phase': 3},
{'id': '614626+7_phase=3', 'predictions': 1.4, 'property_id': '614626',
'recommendation_id': '7_phase=3',
'phase': 3},
{'id': '614626+8_phase=4', 'predictions': 2.1, 'property_id': '614626',
'recommendation_id': '8_phase=4',
'phase': 4},
{'id': '614626+9_phase=5', 'predictions': 1.3, 'property_id': '614626',
'recommendation_id': '9_phase=5',
'phase': 5},
{'id': '614626+10_phase=5', 'predictions': 1.3, 'property_id': '614626',
'recommendation_id': '10_phase=5',
'phase': 5},
{'id': '614626+11_phase=5', 'predictions': 1.3, 'property_id': '614626',
'recommendation_id': '11_phase=5',
'phase': 5},
{'id': '614626+12_phase=5', 'predictions': 1.2, 'property_id': '614626',
'recommendation_id': '12_phase=5',
'phase': 5},
{'id': '614626+13_phase=5', 'predictions': 1.2, 'property_id': '614626',
'recommendation_id': '13_phase=5',
'phase': 5},
{'id': '614626+14_phase=5', 'predictions': 1.2, 'property_id': '614626',
'recommendation_id': '14_phase=5',
'phase': 5},
{'id': '614626+15_phase=5', 'predictions': 1.2, 'property_id': '614626',
'recommendation_id': '15_phase=5',
'phase': 5},
{'id': '614626+16_phase=5', 'predictions': 1.2, 'property_id': '614626',
'recommendation_id': '16_phase=5',
'phase': 5},
{'id': '614626+17_phase=5', 'predictions': 1.2, 'property_id': '614626',
'recommendation_id': '17_phase=5',
'phase': 5},
{'id': '614626+18_phase=5', 'predictions': 1.2, 'property_id': '614626',
'recommendation_id': '18_phase=5',
'phase': 5},
{'id': '614626+19_phase=5', 'predictions': 1.0, 'property_id': '614626',
'recommendation_id': '19_phase=5',
'phase': 5},
{'id': '614626+20_phase=5', 'predictions': 1.0, 'property_id': '614626',
'recommendation_id': '20_phase=5',
'phase': 5},
{'id': '614626+21_phase=5', 'predictions': 1.0, 'property_id': '614626',
'recommendation_id': '21_phase=5',
'phase': 5},
{'id': '614626+22_phase=5', 'predictions': 1.0, 'property_id': '614626',
'recommendation_id': '22_phase=5',
'phase': 5},
{'id': '614626+23_phase=5', 'predictions': 1.0, 'property_id': '614626',
'recommendation_id': '23_phase=5',
'phase': 5},
{'id': '614626+24_phase=5', 'predictions': 1.0, 'property_id': '614626',
'recommendation_id': '24_phase=5',
'phase': 5},
{'id': '614626+25_phase=5', 'predictions': 0.9, 'property_id': '614626',
'recommendation_id': '25_phase=5',
'phase': 5},
{'id': '614626+26_phase=5', 'predictions': 0.9, 'property_id': '614626',
'recommendation_id': '26_phase=5',
'phase': 5},
{'id': '614626+27_phase=5', 'predictions': 0.9, 'property_id': '614626',
'recommendation_id': '27_phase=5',
'phase': 5},
{'id': '614626+28_phase=5', 'predictions': 0.9, 'property_id': '614626',
'recommendation_id': '28_phase=5',
'phase': 5},
{'id': '614626+29_phase=5', 'predictions': 0.8, 'property_id': '614626',
'recommendation_id': '29_phase=5',
'phase': 5},
{'id': '614626+30_phase=5', 'predictions': 1.1, 'property_id': '614626',
'recommendation_id': '30_phase=5',
'phase': 5},
{'id': '614626+31_phase=5', 'predictions': 1.1, 'property_id': '614626',
'recommendation_id': '31_phase=5',
'phase': 5},
{'id': '614626+32_phase=5', 'predictions': 1.1, 'property_id': '614626',
'recommendation_id': '32_phase=5',
'phase': 5},
{'id': '614626+33_phase=5', 'predictions': 1.0, 'property_id': '614626',
'recommendation_id': '33_phase=5',
'phase': 5},
{'id': '614626+34_phase=5', 'predictions': 1.0, 'property_id': '614626',
'recommendation_id': '34_phase=5',
'phase': 5},
{'id': '614626+35_phase=5', 'predictions': 1.0, 'property_id': '614626',
'recommendation_id': '35_phase=5',
'phase': 5},
{'id': '614626+36_phase=5', 'predictions': 1.0, 'property_id': '614626',
'recommendation_id': '36_phase=5',
'phase': 5},
{'id': '614626+37_phase=5', 'predictions': 1.5, 'property_id': '614626',
'recommendation_id': '37_phase=5',
'phase': 5},
{'id': '614626+38_phase=5', 'predictions': 1.5, 'property_id': '614626',
'recommendation_id': '38_phase=5',
'phase': 5},
{'id': '614626+39_phase=5', 'predictions': 1.5, 'property_id': '614626',
'recommendation_id': '39_phase=5',
'phase': 5},
{'id': '614626+40_phase=5', 'predictions': 1.4, 'property_id': '614626',
'recommendation_id': '40_phase=5',
'phase': 5},
{'id': '614626+41_phase=5', 'predictions': 1.4, 'property_id': '614626',
'recommendation_id': '41_phase=5',
'phase': 5},
{'id': '614626+42_phase=5', 'predictions': 1.4, 'property_id': '614626',
'recommendation_id': '42_phase=5',
'phase': 5},
{'id': '614626+43_phase=5', 'predictions': 1.4, 'property_id': '614626',
'recommendation_id': '43_phase=5',
'phase': 5},
{'id': '614626+44_phase=5', 'predictions': 1.2, 'property_id': '614626',
'recommendation_id': '44_phase=5',
'phase': 5},
{'id': '614626+45_phase=5', 'predictions': 1.2, 'property_id': '614626',
'recommendation_id': '45_phase=5',
'phase': 5},
{'id': '614626+46_phase=5', 'predictions': 1.2, 'property_id': '614626',
'recommendation_id': '46_phase=5',
'phase': 5},
{'id': '614626+47_phase=5', 'predictions': 1.1, 'property_id': '614626',
'recommendation_id': '47_phase=5',
'phase': 5},
{'id': '614626+48_phase=5', 'predictions': 1.1, 'property_id': '614626',
'recommendation_id': '48_phase=5',
'phase': 5},
{'id': '614626+49_phase=5', 'predictions': 1.1, 'property_id': '614626',
'recommendation_id': '49_phase=5',
'phase': 5},
{'id': '614626+50_phase=5', 'predictions': 1.1, 'property_id': '614626',
'recommendation_id': '50_phase=5',
'phase': 5},
{'id': '614626+51_phase=5', 'predictions': 1.1, 'property_id': '614626',
'recommendation_id': '51_phase=5',
'phase': 5},
{'id': '614626+52_phase=5', 'predictions': 1.1, 'property_id': '614626',
'recommendation_id': '52_phase=5',
'phase': 5},
{'id': '614626+53_phase=5', 'predictions': 1.1, 'property_id': '614626',
'recommendation_id': '53_phase=5',
'phase': 5},
{'id': '614626+54_phase=5', 'predictions': 1.1, 'property_id': '614626',
'recommendation_id': '54_phase=5',
'phase': 5},
{'id': '614626+55_phase=5', 'predictions': 1.6, 'property_id': '614626',
'recommendation_id': '55_phase=5',
'phase': 5},
{'id': '614626+56_phase=5', 'predictions': 1.5, 'property_id': '614626',
'recommendation_id': '56_phase=5',
'phase': 5},
{'id': '614626+57_phase=5', 'predictions': 1.5, 'property_id': '614626',
'recommendation_id': '57_phase=5',
'phase': 5}
]
)
@pytest.fixture
def property_instance():
return Mock(
id=614626,
data={
"current-energy-efficiency": 65,
"co2-emissions-current": 2.4,
"energy-consumption-current": 284,
"roof-energy-eff": "Good",
"lighting-energy-eff": "Good",
},
roof={
"is_loft": True,
"insulation_thickness": "250",
"is_valid": True,
},
lighting={
"low_energy_proportion": 0.5
}
)
@pytest.mark.parametrize(
"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": 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},
],
),
],
)
def test_filter_phase_adjustment(input_data, expected):
assert Recommendations._filter_phase_adjustment(input_data) == expected
@pytest.mark.parametrize(
"sap_impact, limit, expected",
[
(1.0, -4, True), # positive SAP not allowed
(0.0, -4, False), # zero is allowed
(-1.0, -4, False), # valid range
(-3.9, -4, False), # valid range
(-4.0, -4, False), # exact lower bound allowed
(-4.1, -4, True), # below lower bound
],
)
def test_check_ventilation_out_of_bounds(sap_impact, limit, expected):
assert Recommendations._check_ventilation_out_of_bounds(
sap_impact, limit
) is expected
@pytest.mark.parametrize(
"sap_impact, limit, expected",
[
(1.2, -4, -1), # positive → capped to -1
(0.0, -4, -1), # zero → capped to -1
(-5.0, -4, -4), # below limit → clamp
(-3.0, -4, -3.0), # already valid → unchanged
],
)
def test_adjust_ventilation_sap(sap_impact, limit, expected):
assert Recommendations._adjust_ventilation_sap(
sap_impact, limit
) == expected
def test_get_previous_phase_values_phase_0_starting_phase_0(property_instance):
result = Recommendations._get_previous_phase_values(
rec_phase=0,
starting_phase=0,
impact_summary=[],
property_instance=property_instance,
)
assert result == {
"sap": 65.0,
"sap_prediction": 65.0,
"carbon": 2.4,
"heat_demand": 284.0,
}
def test_get_previous_phase_values_single_rep(property_instance):
impact_summary = [
{
"phase": 0,
"representative": True,
"sap": 66,
"carbon": 2.2,
"heat_demand": 260,
}
]
result = Recommendations._get_previous_phase_values(
rec_phase=1,
starting_phase=0,
impact_summary=impact_summary,
property_instance=property_instance,
)
assert result["sap"] == 66
assert result["carbon"] == 2.2
assert result["heat_demand"] == 260
def test_get_previous_phase_values_median(property_instance):
impact_summary = [
{"phase": 1, "representative": True, "sap": 70, "carbon": 2.0, "heat_demand": 250, "sap_prediction": 70},
{"phase": 1, "representative": True, "sap": 74, "carbon": 1.6, "heat_demand": 230, "sap_prediction": 74},
]
result = Recommendations._get_previous_phase_values(
rec_phase=2,
starting_phase=0,
impact_summary=impact_summary,
property_instance=property_instance,
)
assert result["sap"] == np.median([70, 74])
assert result["carbon"] == np.median([2.0, 1.6])
assert result["heat_demand"] == np.median([250, 230])
def test_compute_phase_impact_standard():
previous = {"sap": 65, "carbon": 2.4, "heat_demand": 284}
current = {"sap": 64, "carbon": 2.6, "heat_demand": 300}
impact = Recommendations._compute_phase_impact(
rec_type="loft_insulation",
previous_phase_values=previous,
current_phase_values=current,
)
# monotonicity enforced
assert impact["sap"] == 0
assert impact["carbon"] == 0
assert impact["heat_demand"] == 0
def test_compute_phase_impact_mechanical_ventilation():
previous = {"sap": 65, "carbon": 2.4, "heat_demand": 284}
current = {"sap": 63, "carbon": 2.4, "heat_demand": 284}
impact = Recommendations._compute_phase_impact(
rec_type="mechanical_ventilation",
previous_phase_values=previous,
current_phase_values=current,
)
assert impact["sap"] == -2
def test_resolve_current_phase_sap_with_adjustments():
rec = {"phase": 3, "survey": False}
previous = {"sap": 65}
phase_metrics = {"sap_change": 70}
adjustments = [
{"phase": 1, "sap_adjustment": 1.5},
{"phase": 2, "sap_adjustment": 2.0},
]
sap = Recommendations._resolve_current_phase_sap(
rec=rec,
previous_phase_values=previous,
phase_energy_efficiency_metrics=phase_metrics,
adjustments=adjustments,
)
assert sap == 70 - (1.5 + 2.0)
def test_validate_recommendation_updates_raises():
rec = {
"sap_points": None,
"co2_equivalent_savings": None,
"heat_demand": None,
}
with pytest.raises(ValueError):
Recommendations._validate_recommendation_updates(rec)
def test_calculate_recommendation_impact(property_instance, heat_demand_predictions, carbon_predictions):
#######
# Case 3
#######
# Here, the solar impact falls below our threshold and so we expect a solar adjustment to increase the impact
# above the minimum threshold
all_predictions3 = {
"sap_change_predictions": pd.DataFrame(
[
{'id': '614626+0_phase=0', 'predictions': 66.7, 'property_id': '614626',
'recommendation_id': '0_phase=0',
'phase': 0},
{'id': '614626+1_phase=0', 'predictions': 66.7, 'property_id': '614626',
'recommendation_id': '1_phase=0',
'phase': 0},
{'id': '614626+2_phase=0', 'predictions': 66.7, 'property_id': '614626',
'recommendation_id': '2_phase=0',
'phase': 0},
{'id': '614626+3_phase=1', 'predictions': 65.3, 'property_id': '614626',
'recommendation_id': '3_phase=1',
'phase': 1},
{'id': '614626+4_phase=2', 'predictions': 66.3, 'property_id': '614626',
'recommendation_id': '4_phase=2',
'phase': 2},
{'id': '614626+5_phase=3', 'predictions': 67.3, 'property_id': '614626',
'recommendation_id': '5_phase=3',
'phase': 3},
{'id': '614626+6_phase=3', 'predictions': 68.1, 'property_id': '614626',
'recommendation_id': '6_phase=3',
'phase': 3},
{'id': '614626+7_phase=3', 'predictions': 70.1, 'property_id': '614626',
'recommendation_id': '7_phase=3',
'phase': 3},
{'id': '614626+8_phase=4', 'predictions': 67.3, 'property_id': '614626',
'recommendation_id': '8_phase=4',
'phase': 4},
{'id': '614626+9_phase=5', 'predictions': 85.3, 'property_id': '614626',
'recommendation_id': '9_phase=5',
'phase': 5}, {'id': '614626+10_phase=5', 'predictions': 85.3, 'property_id': '614626',
'recommendation_id': '10_phase=5', 'phase': 5},
{'id': '614626+11_phase=5', 'predictions': 85.3, 'property_id': '614626',
'recommendation_id': '11_phase=5', 'phase': 5},
{'id': '614626+12_phase=5', 'predictions': 85.5, 'property_id': '614626',
'recommendation_id': '12_phase=5', 'phase': 5},
{'id': '614626+13_phase=5', 'predictions': 85.5, 'property_id': '614626',
'recommendation_id': '13_phase=5', 'phase': 5},
{'id': '614626+14_phase=5', 'predictions': 85.5, 'property_id': '614626',
'recommendation_id': '14_phase=5', 'phase': 5},
{'id': '614626+15_phase=5', 'predictions': 85.5, 'property_id': '614626',
'recommendation_id': '15_phase=5', 'phase': 5},
{'id': '614626+16_phase=5', 'predictions': 85.5, 'property_id': '614626',
'recommendation_id': '16_phase=5', 'phase': 5},
{'id': '614626+17_phase=5', 'predictions': 85.5, 'property_id': '614626',
'recommendation_id': '17_phase=5', 'phase': 5},
{'id': '614626+18_phase=5', 'predictions': 85.5, 'property_id': '614626',
'recommendation_id': '18_phase=5', 'phase': 5},
{'id': '614626+19_phase=5', 'predictions': 86.4, 'property_id': '614626',
'recommendation_id': '19_phase=5', 'phase': 5},
{'id': '614626+20_phase=5', 'predictions': 86.4, 'property_id': '614626',
'recommendation_id': '20_phase=5', 'phase': 5},
{'id': '614626+21_phase=5', 'predictions': 86.4, 'property_id': '614626',
'recommendation_id': '21_phase=5', 'phase': 5},
{'id': '614626+22_phase=5', 'predictions': 86.4, 'property_id': '614626',
'recommendation_id': '22_phase=5', 'phase': 5},
{'id': '614626+23_phase=5', 'predictions': 86.4, 'property_id': '614626',
'recommendation_id': '23_phase=5', 'phase': 5},
{'id': '614626+24_phase=5', 'predictions': 86.4, 'property_id': '614626',
'recommendation_id': '24_phase=5', 'phase': 5},
{'id': '614626+25_phase=5', 'predictions': 86.7, 'property_id': '614626',
'recommendation_id': '25_phase=5', 'phase': 5},
{'id': '614626+26_phase=5', 'predictions': 86.7, 'property_id': '614626',
'recommendation_id': '26_phase=5', 'phase': 5},
{'id': '614626+27_phase=5', 'predictions': 86.7, 'property_id': '614626',
'recommendation_id': '27_phase=5', 'phase': 5},
{'id': '614626+28_phase=5', 'predictions': 86.7, 'property_id': '614626',
'recommendation_id': '28_phase=5', 'phase': 5},
{'id': '614626+29_phase=5', 'predictions': 83.8, 'property_id': '614626',
'recommendation_id': '29_phase=5', 'phase': 5},
{'id': '614626+30_phase=5', 'predictions': 85.4, 'property_id': '614626',
'recommendation_id': '30_phase=5', 'phase': 5},
{'id': '614626+31_phase=5', 'predictions': 85.4, 'property_id': '614626',
'recommendation_id': '31_phase=5', 'phase': 5},
{'id': '614626+32_phase=5', 'predictions': 85.4, 'property_id': '614626',
'recommendation_id': '32_phase=5', 'phase': 5},
{'id': '614626+33_phase=5', 'predictions': 86.4, 'property_id': '614626',
'recommendation_id': '33_phase=5', 'phase': 5},
{'id': '614626+34_phase=5', 'predictions': 86.4, 'property_id': '614626',
'recommendation_id': '34_phase=5', 'phase': 5},
{'id': '614626+35_phase=5', 'predictions': 86.4, 'property_id': '614626',
'recommendation_id': '35_phase=5', 'phase': 5},
{'id': '614626+36_phase=5', 'predictions': 86.4, 'property_id': '614626',
'recommendation_id': '36_phase=5', 'phase': 5},
{'id': '614626+37_phase=5', 'predictions': 81.2, 'property_id': '614626',
'recommendation_id': '37_phase=5', 'phase': 5},
{'id': '614626+38_phase=5', 'predictions': 81.2, 'property_id': '614626',
'recommendation_id': '38_phase=5', 'phase': 5},
{'id': '614626+39_phase=5', 'predictions': 81.2, 'property_id': '614626',
'recommendation_id': '39_phase=5', 'phase': 5},
{'id': '614626+40_phase=5', 'predictions': 83.4, 'property_id': '614626',
'recommendation_id': '40_phase=5', 'phase': 5},
{'id': '614626+41_phase=5', 'predictions': 83.4, 'property_id': '614626',
'recommendation_id': '41_phase=5', 'phase': 5},
{'id': '614626+42_phase=5', 'predictions': 83.4, 'property_id': '614626',
'recommendation_id': '42_phase=5', 'phase': 5},
{'id': '614626+43_phase=5', 'predictions': 83.4, 'property_id': '614626',
'recommendation_id': '43_phase=5', 'phase': 5},
{'id': '614626+44_phase=5', 'predictions': 85.5, 'property_id': '614626',
'recommendation_id': '44_phase=5', 'phase': 5},
{'id': '614626+45_phase=5', 'predictions': 85.5, 'property_id': '614626',
'recommendation_id': '45_phase=5', 'phase': 5},
{'id': '614626+46_phase=5', 'predictions': 85.5, 'property_id': '614626',
'recommendation_id': '46_phase=5', 'phase': 5},
{'id': '614626+47_phase=5', 'predictions': 85.4, 'property_id': '614626',
'recommendation_id': '47_phase=5', 'phase': 5},
{'id': '614626+48_phase=5', 'predictions': 85.4, 'property_id': '614626',
'recommendation_id': '48_phase=5', 'phase': 5},
{'id': '614626+49_phase=5', 'predictions': 85.4, 'property_id': '614626',
'recommendation_id': '49_phase=5', 'phase': 5},
{'id': '614626+50_phase=5', 'predictions': 85.4, 'property_id': '614626',
'recommendation_id': '50_phase=5', 'phase': 5},
{'id': '614626+51_phase=5', 'predictions': 85.4, 'property_id': '614626',
'recommendation_id': '51_phase=5', 'phase': 5},
{'id': '614626+52_phase=5', 'predictions': 85.4, 'property_id': '614626',
'recommendation_id': '52_phase=5', 'phase': 5},
{'id': '614626+53_phase=5', 'predictions': 85.4, 'property_id': '614626',
'recommendation_id': '53_phase=5', 'phase': 5},
{'id': '614626+54_phase=5', 'predictions': 85.4, 'property_id': '614626',
'recommendation_id': '54_phase=5', 'phase': 5},
{'id': '614626+55_phase=5', 'predictions': 79.4, 'property_id': '614626',
'recommendation_id': '55_phase=5', 'phase': 5},
{'id': '614626+56_phase=5', 'predictions': 81.2, 'property_id': '614626',
'recommendation_id': '56_phase=5', 'phase': 5},
{'id': '614626+57_phase=5', 'predictions': 81.2, 'property_id': '614626',
'recommendation_id': '57_phase=5', 'phase': 5}]
),
"heat_demand_predictions": heat_demand_predictions,
"carbon_change_predictions": carbon_predictions,
"hotwater_kwh_predictions": pd.DataFrame([]),
"heating_kwh_predictions": pd.DataFrame([]),
}
recommendations3 = {
614626: [
[
{
'phase': 0,
'type': 'loft_insulation',
'measure_type': 'loft_insulation',
'sap_points': 0,
'survey': False,
'recommendation_id': '0_phase=0',
'co2_equivalent_savings': np.float64(0.19999999999999973),
'heat_demand': np.float64(27.399999999999977)},
],
[
{
'phase': 1,
'type': 'mechanical_ventilation',
'measure_type': 'mechanical_ventilation',
'sap_points': np.float64(-1.4000000000000057),
'heat_demand': np.float64(-6.5),
'kwh_savings': 0,
'co2_equivalent_savings': np.float64(0.0),
'energy_cost_savings': 0,
'innovation_rate': 0.0,
'recommendation_id': '3_phase=1', 'efficiency': 0}
],
[
{
'phase': 2,
'type': 'low_energy_lighting',
'measure_type': 'low_energy_lighting',
'already_installed': False, 'sap_points': 5, 'kwh_savings': 164.25,
'energy_cost_savings': 45.480824999999996, 'co2_equivalent_savings': np.float64(0.0),
'total': 10.5, 'contingency': 2.73,
'contingency_rate': 0.26, 'labour_hours': 1, 'labour_days': 0.125, 'survey': True,
'recommendation_id': '4_phase=2', 'efficiency': 10.5, 'heat_demand': np.float64(4.100000000000023)}
],
[
{
'type': 'heating', 'measure_type': 'roomstat_programmer_trvs', 'phase': 3,
'total': 70,
'contingency': 7.0, 'contingency_rate': 0.1, 'subtotal': 58.333333333333336,
'vat': 11.666666666666664,
'labour_hours': 0.5, 'labour_days': 1,
'sap_points': np.float64(1.0), 'already_installed': False,
'innovation_rate': 0.0,
'recommendation_id': '5_phase=3', 'efficiency': 70,
'co2_equivalent_savings': np.float64(0.10000000000000009), 'heat_demand': np.float64(8.5)
},
{
'type': 'heating', 'phase': 3, 'measure_type': 'time_temperature_zone_control',
'total': 604.5840000000001, 'contingency': 60.45840000000001, 'contingency_rate': 0.1,
'subtotal': 571.32,
'vat': 33.264, 'labour_hours': 3.08, 'labour_days': np.float64(1.0),
'sap_points': np.float64(1.8), 'already_installed': False,
'recommendation_id': '6_phase=3', 'efficiency': 604.5840000000001,
'co2_equivalent_savings': np.float64(0.10000000000000009),
'heat_demand': np.float64(13.300000000000011)
},
{
'phase': 3, 'type': 'heating', 'measure_type': 'air_source_heat_pump',
'sap_points': np.float64(3.8),
'already_installed': False,
'total': 17144.924,
'contingency': 4195.5434000000005, 'contingency_rate': 0.35, 'vat': 33.264, 'labour_hours': 83.08,
'labour_days': np.float64(11.0), 'innovation_rate': 0, 'recommendation_id': '7_phase=3',
'efficiency': 17144.924, 'co2_equivalent_savings': np.float64(0.8000000000000003),
'heat_demand': np.float64(59.30000000000001)
}
],
[
{'phase': 4, 'type': 'secondary_heating', 'measure_type': 'secondary_heating',
'sap_points': np.float64(0.0), 'already_installed': False, 'total': 60.0, 'contingency': 6.0,
'contingency_rate': 0.1, 'subtotal': 50.0, 'vat': 10.0, 'labour_hours': 6.0,
'labour_days': np.float64(1.0), 'innovation_rate': 0.0,
'recommendation_id': '8_phase=4', 'efficiency': 60.0, 'co2_equivalent_savings': np.float64(0.0),
'heat_demand': np.float64(0.0)}
],
[
{
'phase': 5,
'type': 'solar_pv',
'measure_type': 'solar_pv',
'starting_u_value': None, 'new_u_value': None,
'sap_points': np.float64(16.0), 'already_installed': False,
'total': 5892.21, 'subtotal': 5892.21, 'contingency': 883.8315,
'contingency_rate': 0.15, 'vat': 0, 'labour_hours': 48,
'labour_days': 2, 'has_battery': False,
'initial_ac_kwh_per_year': np.float64(4844.465553999999),
'innovation_rate': 0.0, 'recommendation_id': '29_phase=5',
'efficiency': np.float64(368.263125)
}
]
]
}
representative_recommendations3 = {
614626: [
{
'phase': 0,
'type': 'loft_insulation',
'measure_type': 'loft_insulation',
'sap_points': 0,
'already_installed': False,
'total': 1029.0, 'contingency': 102.9,
'contingency_rate': 0.1, 'labour_hours': 8, 'labour_days': 1, 'survey': False,
'innovation_rate': 0.0,
'recommendation_id': '0_phase=0', 'efficiency': np.float64(6052.801176470587),
'co2_equivalent_savings': np.float64(0.19999999999999973),
'heat_demand': np.float64(27.399999999999977)
},
{
'phase': 1,
'type': 'mechanical_ventilation',
'measure_type': 'mechanical_ventilation',
'starting_u_value': None, 'new_u_value': None,
'already_installed': False,
'sap_points': np.float64(-1.4000000000000057),
'heat_demand': np.float64(-6.5), 'kwh_savings': 0,
'co2_equivalent_savings': np.float64(0.0),
'energy_cost_savings': 0, 'total': 560.0,
'labour_hours': 8, 'labour_days': 1.0,
'innovation_rate': 0.0,
'recommendation_id': '3_phase=1', 'efficiency': 0},
{
'phase': 2,
'type': 'low_energy_lighting', 'measure_type': 'low_energy_lighting',
'already_installed': False, 'sap_points': 5, 'kwh_savings': 164.25,
'energy_cost_savings': 45.480824999999996, 'co2_equivalent_savings': np.float64(0.0),
'total': 10.5, 'contingency': 2.73,
'contingency_rate': 0.26, 'labour_hours': 1, 'labour_days': 0.125, 'survey': True,
'innovation_rate': 0.0, 'recommendation_id': '4_phase=2', 'efficiency': 10.5,
'heat_demand': np.float64(4.100000000000023)
},
{
'type': 'heating', 'measure_type':
'roomstat_programmer_trvs', 'phase': 3,
'total': 70,
'contingency': 7.0, 'contingency_rate': 0.1, 'subtotal': 58.333333333333336,
'vat': 11.666666666666664, 'labour_hours': 0.5, 'labour_days': 1, 'starting_u_value': None,
'new_u_value': None, 'sap_points': np.float64(1.0), 'already_installed': False,
'innovation_rate': 0.0,
'recommendation_id': '5_phase=3', 'efficiency': 70,
'co2_equivalent_savings': np.float64(0.10000000000000009),
'heat_demand': np.float64(8.5)
},
{
'phase': 4, 'type': 'secondary_heating', 'measure_type': 'secondary_heating',
'sap_points': np.float64(0.0), 'already_installed': False, 'total': 60.0, 'contingency': 6.0,
'contingency_rate': 0.1, 'subtotal': 50.0, 'vat': 10.0, 'labour_hours': 6.0,
'labour_days': np.float64(1.0),
'recommendation_id': '8_phase=4', 'efficiency': 60.0, 'co2_equivalent_savings': np.float64(0.0),
'heat_demand': np.float64(0.0)},
{
'phase': 5,
'type': 'solar_pv',
'measure_type': 'solar_pv',
'starting_u_value': None, 'new_u_value': None,
'sap_points': np.float64(16.0), 'already_installed': False,
'total': 5892.21, 'subtotal': 5892.21, 'contingency': 883.8315,
'contingency_rate': 0.15, 'vat': 0, 'labour_hours': 48,
'labour_days': 2, 'has_battery': False,
'innovation_rate': 0.0, 'recommendation_id': '29_phase=5',
'efficiency': np.float64(368.263125)
}
]
}
recommendations_with_impact3, impact_summary3, adjustments3 = (
Recommendations.calculate_recommendation_impact(
property_instance=property_instance,
all_predictions=all_predictions3,
recommendations=recommendations3,
representative_recommendations=representative_recommendations3,
debug=True
)
)
# We expect adjustments for loft insulation, lighting and solar
assert adjustments3 == [
{'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)},
{'recommendation_id': '29_phase=5', 'phase': 5, 'sap_adjustment': np.float64(-2.5)}
]
# Check the impact has slowed through to solar - the final on the impact summary. The 5
# point prediction isn't associated to the prediction from the model so the adjustment
# should be
df = all_predictions3["sap_change_predictions"]
raw_prediction = 83.8
# We expect 1.7 decrease from loft, 4 decrease from lighting, and 2.5 increase from solar
# for a total of a 3.2 decrease
expected_adjusted_prediction = raw_prediction - 3.2
assert impact_summary3[-1]["sap"] == expected_adjusted_prediction
def test_loft_adjustment_flows_to_solar(property_instance, heat_demand_predictions, carbon_predictions):
########################
# Case 1
########################
# Just an adjustment to loft insulation
sap_change_predictions = pd.DataFrame(
[
{'id': '614626+0_phase=0', 'predictions': 66.7, 'property_id': '614626',
'recommendation_id': '0_phase=0',
'phase': 0},
{'id': '614626+1_phase=0', 'predictions': 66.7, 'property_id': '614626',
'recommendation_id': '1_phase=0',
'phase': 0},
{'id': '614626+2_phase=0', 'predictions': 66.7, 'property_id': '614626',
'recommendation_id': '2_phase=0',
'phase': 0},
{'id': '614626+3_phase=1', 'predictions': 65.3, 'property_id': '614626',
'recommendation_id': '3_phase=1',
'phase': 1},
{'id': '614626+4_phase=2', 'predictions': 66.3, 'property_id': '614626',
'recommendation_id': '4_phase=2',
'phase': 2},
{'id': '614626+5_phase=3', 'predictions': 67.3, 'property_id': '614626',
'recommendation_id': '5_phase=3',
'phase': 3},
{'id': '614626+6_phase=3', 'predictions': 68.1, 'property_id': '614626',
'recommendation_id': '6_phase=3',
'phase': 3},
{'id': '614626+7_phase=3', 'predictions': 70.1, 'property_id': '614626',
'recommendation_id': '7_phase=3',
'phase': 3},
{'id': '614626+8_phase=4', 'predictions': 67.3, 'property_id': '614626',
'recommendation_id': '8_phase=4',
'phase': 4},
{'id': '614626+9_phase=5', 'predictions': 85.3, 'property_id': '614626',
'recommendation_id': '9_phase=5',
'phase': 5}, {'id': '614626+10_phase=5', 'predictions': 85.3, 'property_id': '614626',
'recommendation_id': '10_phase=5', 'phase': 5},
{'id': '614626+11_phase=5', 'predictions': 85.3, 'property_id': '614626',
'recommendation_id': '11_phase=5', 'phase': 5},
{'id': '614626+12_phase=5', 'predictions': 85.5, 'property_id': '614626',
'recommendation_id': '12_phase=5', 'phase': 5},
{'id': '614626+13_phase=5', 'predictions': 85.5, 'property_id': '614626',
'recommendation_id': '13_phase=5', 'phase': 5},
{'id': '614626+14_phase=5', 'predictions': 85.5, 'property_id': '614626',
'recommendation_id': '14_phase=5', 'phase': 5},
{'id': '614626+15_phase=5', 'predictions': 85.5, 'property_id': '614626',
'recommendation_id': '15_phase=5', 'phase': 5},
{'id': '614626+16_phase=5', 'predictions': 85.5, 'property_id': '614626',
'recommendation_id': '16_phase=5', 'phase': 5},
{'id': '614626+17_phase=5', 'predictions': 85.5, 'property_id': '614626',
'recommendation_id': '17_phase=5', 'phase': 5},
{'id': '614626+18_phase=5', 'predictions': 85.5, 'property_id': '614626',
'recommendation_id': '18_phase=5', 'phase': 5},
{'id': '614626+19_phase=5', 'predictions': 86.4, 'property_id': '614626',
'recommendation_id': '19_phase=5', 'phase': 5},
{'id': '614626+20_phase=5', 'predictions': 86.4, 'property_id': '614626',
'recommendation_id': '20_phase=5', 'phase': 5},
{'id': '614626+21_phase=5', 'predictions': 86.4, 'property_id': '614626',
'recommendation_id': '21_phase=5', 'phase': 5},
{'id': '614626+22_phase=5', 'predictions': 86.4, 'property_id': '614626',
'recommendation_id': '22_phase=5', 'phase': 5},
{'id': '614626+23_phase=5', 'predictions': 86.4, 'property_id': '614626',
'recommendation_id': '23_phase=5', 'phase': 5},
{'id': '614626+24_phase=5', 'predictions': 86.4, 'property_id': '614626',
'recommendation_id': '24_phase=5', 'phase': 5},
{'id': '614626+25_phase=5', 'predictions': 86.7, 'property_id': '614626',
'recommendation_id': '25_phase=5', 'phase': 5},
{'id': '614626+26_phase=5', 'predictions': 86.7, 'property_id': '614626',
'recommendation_id': '26_phase=5', 'phase': 5},
{'id': '614626+27_phase=5', 'predictions': 86.7, 'property_id': '614626',
'recommendation_id': '27_phase=5', 'phase': 5},
{'id': '614626+28_phase=5', 'predictions': 86.7, 'property_id': '614626',
'recommendation_id': '28_phase=5', 'phase': 5},
{'id': '614626+29_phase=5', 'predictions': 83.8, 'property_id': '614626',
'recommendation_id': '29_phase=5', 'phase': 5},
{'id': '614626+30_phase=5', 'predictions': 85.4, 'property_id': '614626',
'recommendation_id': '30_phase=5', 'phase': 5},
{'id': '614626+31_phase=5', 'predictions': 85.4, 'property_id': '614626',
'recommendation_id': '31_phase=5', 'phase': 5},
{'id': '614626+32_phase=5', 'predictions': 85.4, 'property_id': '614626',
'recommendation_id': '32_phase=5', 'phase': 5},
{'id': '614626+33_phase=5', 'predictions': 86.4, 'property_id': '614626',
'recommendation_id': '33_phase=5', 'phase': 5},
{'id': '614626+34_phase=5', 'predictions': 86.4, 'property_id': '614626',
'recommendation_id': '34_phase=5', 'phase': 5},
{'id': '614626+35_phase=5', 'predictions': 86.4, 'property_id': '614626',
'recommendation_id': '35_phase=5', 'phase': 5},
{'id': '614626+36_phase=5', 'predictions': 86.4, 'property_id': '614626',
'recommendation_id': '36_phase=5', 'phase': 5},
{'id': '614626+37_phase=5', 'predictions': 81.2, 'property_id': '614626',
'recommendation_id': '37_phase=5', 'phase': 5},
{'id': '614626+38_phase=5', 'predictions': 81.2, 'property_id': '614626',
'recommendation_id': '38_phase=5', 'phase': 5},
{'id': '614626+39_phase=5', 'predictions': 81.2, 'property_id': '614626',
'recommendation_id': '39_phase=5', 'phase': 5},
{'id': '614626+40_phase=5', 'predictions': 83.4, 'property_id': '614626',
'recommendation_id': '40_phase=5', 'phase': 5},
{'id': '614626+41_phase=5', 'predictions': 83.4, 'property_id': '614626',
'recommendation_id': '41_phase=5', 'phase': 5},
{'id': '614626+42_phase=5', 'predictions': 83.4, 'property_id': '614626',
'recommendation_id': '42_phase=5', 'phase': 5},
{'id': '614626+43_phase=5', 'predictions': 83.4, 'property_id': '614626',
'recommendation_id': '43_phase=5', 'phase': 5},
{'id': '614626+44_phase=5', 'predictions': 85.5, 'property_id': '614626',
'recommendation_id': '44_phase=5', 'phase': 5},
{'id': '614626+45_phase=5', 'predictions': 85.5, 'property_id': '614626',
'recommendation_id': '45_phase=5', 'phase': 5},
{'id': '614626+46_phase=5', 'predictions': 85.5, 'property_id': '614626',
'recommendation_id': '46_phase=5', 'phase': 5},
{'id': '614626+47_phase=5', 'predictions': 85.4, 'property_id': '614626',
'recommendation_id': '47_phase=5', 'phase': 5},
{'id': '614626+48_phase=5', 'predictions': 85.4, 'property_id': '614626',
'recommendation_id': '48_phase=5', 'phase': 5},
{'id': '614626+49_phase=5', 'predictions': 85.4, 'property_id': '614626',
'recommendation_id': '49_phase=5', 'phase': 5},
{'id': '614626+50_phase=5', 'predictions': 85.4, 'property_id': '614626',
'recommendation_id': '50_phase=5', 'phase': 5},
{'id': '614626+51_phase=5', 'predictions': 85.4, 'property_id': '614626',
'recommendation_id': '51_phase=5', 'phase': 5},
{'id': '614626+52_phase=5', 'predictions': 85.4, 'property_id': '614626',
'recommendation_id': '52_phase=5', 'phase': 5},
{'id': '614626+53_phase=5', 'predictions': 85.4, 'property_id': '614626',
'recommendation_id': '53_phase=5', 'phase': 5},
{'id': '614626+54_phase=5', 'predictions': 85.4, 'property_id': '614626',
'recommendation_id': '54_phase=5', 'phase': 5},
{'id': '614626+55_phase=5', 'predictions': 79.4, 'property_id': '614626',
'recommendation_id': '55_phase=5', 'phase': 5},
{'id': '614626+56_phase=5', 'predictions': 81.2, 'property_id': '614626',
'recommendation_id': '56_phase=5', 'phase': 5},
{'id': '614626+57_phase=5', 'predictions': 81.2, 'property_id': '614626',
'recommendation_id': '57_phase=5', 'phase': 5}]
)
all_predictions = {
"sap_change_predictions": sap_change_predictions,
"heat_demand_predictions": heat_demand_predictions,
"carbon_change_predictions": carbon_predictions,
"hotwater_kwh_predictions": pd.DataFrame([]),
"heating_kwh_predictions": pd.DataFrame([]),
}
recommendations = {
614626: [
[
{
'phase': 0, 'type': 'loft_insulation',
'measure_type': 'loft_insulation',
'sap_points': 0,
'already_installed': False,
'contingency_rate': 0.1, 'labour_hours': 8, 'labour_days': 1, 'survey': False,
'innovation_rate': 0.0,
'recommendation_id': '0_phase=0', 'efficiency': np.float64(6052.801176470587),
'co2_equivalent_savings': np.float64(0.19999999999999973),
'heat_demand': np.float64(27.399999999999977)},
],
[
{
'phase': 1,
'type': 'mechanical_ventilation', 'measure_type': 'mechanical_ventilation',
'already_installed': False,
'sap_points': np.float64(-1.4000000000000057),
'heat_demand': np.float64(-6.5), 'kwh_savings': 0, 'co2_equivalent_savings': np.float64(0.0),
'energy_cost_savings': 0, 'total': 560.0, 'labour_hours': 8, 'labour_days': 1.0,
'innovation_rate': 0.0,
'recommendation_id': '3_phase=1', 'efficiency': 0}
],
[
{'phase': 2, 'type': 'low_energy_lighting', 'measure_type': 'low_energy_lighting',
'new_u_value': None,
'already_installed': False, 'sap_points': 1, 'kwh_savings': 164.25,
'energy_cost_savings': 45.480824999999996, 'co2_equivalent_savings': np.float64(0.0),
'contingency_rate': 0.26, 'labour_hours': 1, 'labour_days': 0.125, 'survey': True,
'innovation_rate': 0.0,
'recommendation_id': '4_phase=2', 'efficiency': 10.5, 'heat_demand': np.float64(4.100000000000023)}
],
[
{
'type': 'heating', 'measure_type': 'roomstat_programmer_trvs', 'phase': 3,
'total': 70,
'contingency': 7.0, 'contingency_rate': 0.1, 'subtotal': 58.333333333333336,
'vat': 11.666666666666664,
'labour_hours': 0.5, 'labour_days': 1, 'starting_u_value': None, 'new_u_value': None,
'sap_points': np.float64(1.0), 'already_installed': False,
'recommendation_id': '5_phase=3', 'efficiency': 70,
'co2_equivalent_savings': np.float64(0.10000000000000009), 'heat_demand': np.float64(8.5)
},
{
'type': 'heating', 'phase': 3, 'measure_type': 'time_temperature_zone_control',
'total': 604.5840000000001, 'contingency': 60.45840000000001, 'contingency_rate': 0.1,
'subtotal': 571.32,
'vat': 33.264, 'labour_hours': 3.08, 'labour_days': np.float64(1.0), 'starting_u_value': None,
'new_u_value': None, 'sap_points': np.float64(1.8), 'already_installed': False,
'recommendation_id': '6_phase=3',
'co2_equivalent_savings': np.float64(0.10000000000000009),
'heat_demand': np.float64(13.300000000000011)
},
{
'phase': 3, 'type': 'heating', 'measure_type': 'air_source_heat_pump',
'starting_u_value': None, 'new_u_value': None, 'sap_points': np.float64(3.8),
'already_installed': False,
'total': 17144.924,
'contingency': 4195.5434000000005, 'contingency_rate': 0.35, 'vat': 33.264, 'labour_hours': 83.08,
'labour_days': np.float64(11.0), 'innovation_rate': 0, 'recommendation_id': '7_phase=3',
'efficiency': 17144.924, 'co2_equivalent_savings': np.float64(0.8000000000000003),
'heat_demand': np.float64(59.30000000000001)
}
],
[
{'phase': 4, 'type': 'secondary_heating', 'measure_type': 'secondary_heating',
'starting_u_value': None, 'new_u_value': None,
'sap_points': np.float64(0.0), 'already_installed': False, 'total': 60.0, 'contingency': 6.0,
'contingency_rate': 0.1, 'subtotal': 50.0, 'vat': 10.0, 'labour_hours': 6.0,
'labour_days': np.float64(1.0),
'recommendation_id': '8_phase=4', 'efficiency': 60.0, 'co2_equivalent_savings': np.float64(0.0),
'heat_demand': np.float64(0.0)}
],
[
{
'phase': 5, 'type': 'solar_pv',
'measure_type': 'solar_pv',
'starting_u_value': None, 'new_u_value': None,
'sap_points': np.float64(16.0), 'already_installed': False,
'total': 5892.21, 'subtotal': 5892.21, 'contingency': 883.8315,
'contingency_rate': 0.15, 'vat': 0, 'labour_hours': 48,
'labour_days': 2, 'has_battery': False,
'initial_ac_kwh_per_year': np.float64(4844.465553999999),
'innovation_rate': 0.0, 'recommendation_id': '29_phase=5',
'efficiency': np.float64(368.263125)
}
]
]
}
representative_recommendations = {
614626: [
{
'phase': 0, 'type': 'loft_insulation',
'measure_type': 'loft_insulation',
'starting_u_value': np.float64(0.17), 'new_u_value': np.float64(0.14), 'sap_points': 0,
'already_installed': False,
'contingency_rate': 0.1, 'labour_hours': 8, 'labour_days': 1, 'survey': False,
'innovation_rate': 0.0,
'recommendation_id': '0_phase=0', 'efficiency': np.float64(6052.801176470587),
'co2_equivalent_savings': np.float64(0.19999999999999973),
'heat_demand': np.float64(27.399999999999977)
},
{
'phase': 1,
'type': 'mechanical_ventilation',
'measure_type': 'mechanical_ventilation',
'starting_u_value': None, 'new_u_value': None,
'already_installed': False,
'sap_points': np.float64(-1.4000000000000057),
'heat_demand': np.float64(-6.5), 'kwh_savings': 0,
'co2_equivalent_savings': np.float64(0.0),
'energy_cost_savings': 0, 'total': 560.0,
'labour_hours': 8, 'labour_days': 1.0,
'recommendation_id': '3_phase=1', 'efficiency': 0},
{
'phase': 2, 'type': 'low_energy_lighting', 'measure_type': 'low_energy_lighting',
'starting_u_value': None,
'new_u_value': None, 'already_installed': False, 'sap_points': 1, 'kwh_savings': 164.25,
'energy_cost_savings': 45.480824999999996, 'co2_equivalent_savings': np.float64(0.0),
'contingency_rate': 0.26, 'labour_hours': 1, 'labour_days': 0.125, 'survey': True,
'innovation_rate': 0.0, 'recommendation_id': '4_phase=2', 'efficiency': 10.5,
'heat_demand': np.float64(4.100000000000023)
},
{
'type': 'heating', 'measure_type': 'roomstat_programmer_trvs', 'phase': 3,
'total': 70,
'contingency': 7.0, 'contingency_rate': 0.1, 'subtotal': 58.333333333333336,
'vat': 11.666666666666664, 'labour_hours': 0.5, 'labour_days': 1, 'starting_u_value': None,
'new_u_value': None, 'sap_points': np.float64(1.0), 'already_installed': False,
'recommendation_id': '5_phase=3', 'efficiency': 70,
'co2_equivalent_savings': np.float64(0.10000000000000009), 'heat_demand': np.float64(8.5)
},
{
'phase': 4, 'type': 'secondary_heating', 'measure_type': 'secondary_heating',
'starting_u_value': None, 'new_u_value': None,
'sap_points': np.float64(0.0), 'already_installed': False, 'total': 60.0, 'contingency': 6.0,
'contingency_rate': 0.1, 'subtotal': 50.0, 'vat': 10.0, 'labour_hours': 6.0,
'recommendation_id': '8_phase=4', 'efficiency': 60.0, 'co2_equivalent_savings': np.float64(0.0),
'heat_demand': np.float64(0.0)},
{
'phase': 5, 'type': 'solar_pv',
'measure_type': 'solar_pv',
'starting_u_value': None, 'new_u_value': None,
'sap_points': np.float64(16.0), 'already_installed': False,
'total': 5892.21, 'subtotal': 5892.21, 'contingency': 883.8315,
'contingency_rate': 0.15, 'vat': 0, 'labour_hours': 48,
'labour_days': 2, 'has_battery': False,
'recommendation_id': '29_phase=5',
}
]
}
recommendations_with_impact, impact_summary, adjustments = (
Recommendations.calculate_recommendation_impact(
property_instance=property_instance,
all_predictions=all_predictions,
recommendations=recommendations,
representative_recommendations=representative_recommendations,
debug=True
)
)
# We expect an adjustment to be made for loft insulation, reducing the impact by
# 1.7
assert adjustments == [{'recommendation_id': '0_phase=0', 'phase': 0, 'sap_adjustment': np.float64(1.7)}]
# We expect that adjustment to flow through to the final recommendation so that the solar recommendation has
# a 1.7 sap point reduction in impact
final_impact_summary = impact_summary[-1]
assert float(final_impact_summary["sap"]) == 82.1
assert float(final_impact_summary["sap_prediction"]) == 83.8
assert final_impact_summary["measure_type"] == "solar_pv"
assert recommendations_with_impact[0][0]["sap_points"] == 0
def test_lighting_and_loft_adjustment_combined(property_instance, heat_demand_predictions, carbon_predictions):
########################
# Case 2
########################
# Example case with both a loft insulation and lighting adjustment
# lighting now has a SAP point impact of 5 - the affected recommendation is
# recommendation_id=4_phase=2
all_predictions2 = {
"sap_change_predictions": pd.DataFrame(
[
{'id': '614626+0_phase=0', 'predictions': 66.7, 'property_id': '614626',
'recommendation_id': '0_phase=0',
'phase': 0},
{'id': '614626+1_phase=0', 'predictions': 66.7, 'property_id': '614626',
'recommendation_id': '1_phase=0',
'phase': 0},
{'id': '614626+2_phase=0', 'predictions': 66.7, 'property_id': '614626',
'recommendation_id': '2_phase=0',
'phase': 0},
{'id': '614626+3_phase=1', 'predictions': 65.3, 'property_id': '614626',
'recommendation_id': '3_phase=1',
'phase': 1},
{'id': '614626+4_phase=2', 'predictions': 71.3, 'property_id': '614626',
'recommendation_id': '4_phase=2',
'phase': 2},
{'id': '614626+5_phase=3', 'predictions': 72.3, 'property_id': '614626',
'recommendation_id': '5_phase=3',
'phase': 3},
{'id': '614626+6_phase=3', 'predictions': 73.1, 'property_id': '614626',
'recommendation_id': '6_phase=3',
'phase': 3},
{'id': '614626+7_phase=3', 'predictions': 75.1, 'property_id': '614626',
'recommendation_id': '7_phase=3',
'phase': 3},
{'id': '614626+8_phase=4', 'predictions': 72.3, 'property_id': '614626',
'recommendation_id': '8_phase=4',
'phase': 4},
{'id': '614626+9_phase=5', 'predictions': 90.3, 'property_id': '614626',
'recommendation_id': '9_phase=5',
'phase': 5}, {'id': '614626+10_phase=5', 'predictions': 85.3, 'property_id': '614626',
'recommendation_id': '10_phase=5', 'phase': 5},
{'id': '614626+11_phase=5', 'predictions': 90.3, 'property_id': '614626',
'recommendation_id': '11_phase=5', 'phase': 5},
{'id': '614626+12_phase=5', 'predictions': 90.5, 'property_id': '614626',
'recommendation_id': '12_phase=5', 'phase': 5},
{'id': '614626+13_phase=5', 'predictions': 90.5, 'property_id': '614626',
'recommendation_id': '13_phase=5', 'phase': 5},
{'id': '614626+14_phase=5', 'predictions': 90.5, 'property_id': '614626',
'recommendation_id': '14_phase=5', 'phase': 5},
{'id': '614626+15_phase=5', 'predictions': 90.5, 'property_id': '614626',
'recommendation_id': '15_phase=5', 'phase': 5},
{'id': '614626+16_phase=5', 'predictions': 90.5, 'property_id': '614626',
'recommendation_id': '16_phase=5', 'phase': 5},
{'id': '614626+17_phase=5', 'predictions': 90.5, 'property_id': '614626',
'recommendation_id': '17_phase=5', 'phase': 5},
{'id': '614626+18_phase=5', 'predictions': 90.5, 'property_id': '614626',
'recommendation_id': '18_phase=5', 'phase': 5},
{'id': '614626+19_phase=5', 'predictions': 91.4, 'property_id': '614626',
'recommendation_id': '19_phase=5', 'phase': 5},
{'id': '614626+20_phase=5', 'predictions': 91.4, 'property_id': '614626',
'recommendation_id': '20_phase=5', 'phase': 5},
{'id': '614626+21_phase=5', 'predictions': 91.4, 'property_id': '614626',
'recommendation_id': '21_phase=5', 'phase': 5},
{'id': '614626+22_phase=5', 'predictions': 91.4, 'property_id': '614626',
'recommendation_id': '22_phase=5', 'phase': 5},
{'id': '614626+23_phase=5', 'predictions': 91.4, 'property_id': '614626',
'recommendation_id': '23_phase=5', 'phase': 5},
{'id': '614626+24_phase=5', 'predictions': 91.4, 'property_id': '614626',
'recommendation_id': '24_phase=5', 'phase': 5},
{'id': '614626+25_phase=5', 'predictions': 91.7, 'property_id': '614626',
'recommendation_id': '25_phase=5', 'phase': 5},
{'id': '614626+26_phase=5', 'predictions': 91.7, 'property_id': '614626',
'recommendation_id': '26_phase=5', 'phase': 5},
{'id': '614626+27_phase=5', 'predictions': 91.7, 'property_id': '614626',
'recommendation_id': '27_phase=5', 'phase': 5},
{'id': '614626+28_phase=5', 'predictions': 91.7, 'property_id': '614626',
'recommendation_id': '28_phase=5', 'phase': 5},
{'id': '614626+29_phase=5', 'predictions': 88.8, 'property_id': '614626',
'recommendation_id': '29_phase=5', 'phase': 5},
{'id': '614626+30_phase=5', 'predictions': 90.4, 'property_id': '614626',
'recommendation_id': '30_phase=5', 'phase': 5},
{'id': '614626+31_phase=5', 'predictions': 90.4, 'property_id': '614626',
'recommendation_id': '31_phase=5', 'phase': 5},
{'id': '614626+32_phase=5', 'predictions': 90.4, 'property_id': '614626',
'recommendation_id': '32_phase=5', 'phase': 5},
{'id': '614626+33_phase=5', 'predictions': 91.4, 'property_id': '614626',
'recommendation_id': '33_phase=5', 'phase': 5},
{'id': '614626+34_phase=5', 'predictions': 91.4, 'property_id': '614626',
'recommendation_id': '34_phase=5', 'phase': 5},
{'id': '614626+35_phase=5', 'predictions': 91.4, 'property_id': '614626',
'recommendation_id': '35_phase=5', 'phase': 5},
{'id': '614626+36_phase=5', 'predictions': 91.4, 'property_id': '614626',
'recommendation_id': '36_phase=5', 'phase': 5},
{'id': '614626+37_phase=5', 'predictions': 86.2, 'property_id': '614626',
'recommendation_id': '37_phase=5', 'phase': 5},
{'id': '614626+38_phase=5', 'predictions': 86.2, 'property_id': '614626',
'recommendation_id': '38_phase=5', 'phase': 5},
{'id': '614626+39_phase=5', 'predictions': 86.2, 'property_id': '614626',
'recommendation_id': '39_phase=5', 'phase': 5},
{'id': '614626+40_phase=5', 'predictions': 88.4, 'property_id': '614626',
'recommendation_id': '40_phase=5', 'phase': 5},
{'id': '614626+41_phase=5', 'predictions': 88.4, 'property_id': '614626',
'recommendation_id': '41_phase=5', 'phase': 5},
{'id': '614626+42_phase=5', 'predictions': 88.4, 'property_id': '614626',
'recommendation_id': '42_phase=5', 'phase': 5},
{'id': '614626+43_phase=5', 'predictions': 88.4, 'property_id': '614626',
'recommendation_id': '43_phase=5', 'phase': 5},
{'id': '614626+44_phase=5', 'predictions': 90.5, 'property_id': '614626',
'recommendation_id': '44_phase=5', 'phase': 5},
{'id': '614626+45_phase=5', 'predictions': 90.5, 'property_id': '614626',
'recommendation_id': '45_phase=5', 'phase': 5},
{'id': '614626+46_phase=5', 'predictions': 90.5, 'property_id': '614626',
'recommendation_id': '46_phase=5', 'phase': 5},
{'id': '614626+47_phase=5', 'predictions': 90.4, 'property_id': '614626',
'recommendation_id': '47_phase=5', 'phase': 5},
{'id': '614626+48_phase=5', 'predictions': 90.4, 'property_id': '614626',
'recommendation_id': '48_phase=5', 'phase': 5},
{'id': '614626+49_phase=5', 'predictions': 90.4, 'property_id': '614626',
'recommendation_id': '49_phase=5', 'phase': 5},
{'id': '614626+50_phase=5', 'predictions': 90.4, 'property_id': '614626',
'recommendation_id': '50_phase=5', 'phase': 5},
{'id': '614626+51_phase=5', 'predictions': 90.4, 'property_id': '614626',
'recommendation_id': '51_phase=5', 'phase': 5},
{'id': '614626+52_phase=5', 'predictions': 90.4, 'property_id': '614626',
'recommendation_id': '52_phase=5', 'phase': 5},
{'id': '614626+53_phase=5', 'predictions': 90.4, 'property_id': '614626',
'recommendation_id': '53_phase=5', 'phase': 5},
{'id': '614626+54_phase=5', 'predictions': 90.4, 'property_id': '614626',
'recommendation_id': '54_phase=5', 'phase': 5},
{'id': '614626+55_phase=5', 'predictions': 84.4, 'property_id': '614626',
'recommendation_id': '55_phase=5', 'phase': 5},
{'id': '614626+56_phase=5', 'predictions': 86.2, 'property_id': '614626',
'recommendation_id': '56_phase=5', 'phase': 5},
{'id': '614626+57_phase=5', 'predictions': 86.2, 'property_id': '614626',
'recommendation_id': '57_phase=5', 'phase': 5}]
),
"heat_demand_predictions": heat_demand_predictions,
"carbon_change_predictions": carbon_predictions,
"hotwater_kwh_predictions": pd.DataFrame([]),
"heating_kwh_predictions": pd.DataFrame([]),
}
recommendations2 = {
614626: [
[
{
'phase': 0,
'type': 'loft_insulation',
'measure_type': 'loft_insulation',
'sap_points': 0,
'survey': False,
'innovation_rate': 0.0,
'recommendation_id': '0_phase=0', 'efficiency': np.float64(6052.801176470587),
'co2_equivalent_savings': np.float64(0.19999999999999973),
'heat_demand': np.float64(27.399999999999977)},
],
[
{
'phase': 1,
'type': 'mechanical_ventilation', 'measure_type': 'mechanical_ventilation',
'starting_u_value': None,
'new_u_value': None, 'already_installed': False, 'sap_points': np.float64(-1.4000000000000057),
'heat_demand': np.float64(-6.5), 'kwh_savings': 0, 'co2_equivalent_savings': np.float64(0.0),
'energy_cost_savings': 0, 'total': 560.0, 'labour_hours': 8, 'labour_days': 1.0,
'recommendation_id': '3_phase=1',
}
],
[
{
'phase': 2, 'type': 'low_energy_lighting', 'measure_type': 'low_energy_lighting',
'new_u_value': None,
'already_installed': False, 'sap_points': 5, 'kwh_savings': 164.25,
'energy_cost_savings': 45.480824999999996, 'co2_equivalent_savings': np.float64(0.0),
'total': 10.5, 'contingency': 2.73,
'contingency_rate': 0.26, 'labour_hours': 1, 'labour_days': 0.125, 'survey': True,
'innovation_rate': 0.0,
'recommendation_id': '4_phase=2', 'efficiency': 10.5, 'heat_demand': np.float64(4.100000000000023)}
],
[
{
'type': 'heating', 'measure_type': 'roomstat_programmer_trvs', 'phase': 3,
'total': 70,
'contingency': 7.0, 'contingency_rate': 0.1, 'subtotal': 58.333333333333336,
'vat': 11.666666666666664,
'labour_hours': 0.5, 'labour_days': 1, 'starting_u_value': None, 'new_u_value': None,
'sap_points': np.float64(1.0), 'already_installed': False,
'recommendation_id': '5_phase=3',
'co2_equivalent_savings': np.float64(0.10000000000000009), 'heat_demand': np.float64(8.5)},
{
'type': 'heating', 'phase': 3, 'measure_type': 'time_temperature_zone_control',
'total': 604.5840000000001, 'contingency': 60.45840000000001, 'contingency_rate': 0.1,
'subtotal': 571.32,
'vat': 33.264, 'labour_hours': 3.08, 'labour_days': np.float64(1.0), 'starting_u_value': None,
'new_u_value': None, 'sap_points': np.float64(1.8), 'already_installed': False,
'innovation_rate': 0.0,
'recommendation_id': '6_phase=3', 'efficiency': 604.5840000000001,
'co2_equivalent_savings': np.float64(0.10000000000000009),
'heat_demand': np.float64(13.300000000000011)},
{
'phase': 3, 'type': 'heating', 'measure_type': 'air_source_heat_pump',
'starting_u_value': None, 'new_u_value': None, 'sap_points': np.float64(3.8),
'already_installed': False,
'total': 17144.924,
'contingency': 4195.5434000000005, 'contingency_rate': 0.35, 'vat': 33.264, 'labour_hours': 83.08,
'labour_days': np.float64(11.0), 'innovation_rate': 0, 'recommendation_id': '7_phase=3',
'efficiency': 17144.924, 'co2_equivalent_savings': np.float64(0.8000000000000003),
'heat_demand': np.float64(59.30000000000001)}
],
[
{
'phase': 4, 'type': 'secondary_heating', 'measure_type': 'secondary_heating',
'starting_u_value': None, 'new_u_value': None,
'sap_points': np.float64(0.0), 'already_installed': False, 'total': 60.0, 'contingency': 6.0,
'contingency_rate': 0.1, 'subtotal': 50.0, 'vat': 10.0, 'labour_hours': 6.0,
'labour_days': np.float64(1.0), 'innovation_rate': 0.0,
'recommendation_id': '8_phase=4', 'efficiency': 60.0, 'co2_equivalent_savings': np.float64(0.0),
'heat_demand': np.float64(0.0)
}
],
[
{
'phase': 5, 'type': 'solar_pv',
'measure_type': 'solar_pv',
'starting_u_value': None, 'new_u_value': None,
'sap_points': np.float64(16.0), 'already_installed': False,
'total': 5892.21, 'subtotal': 5892.21, 'contingency': 883.8315,
'contingency_rate': 0.15, 'vat': 0, 'labour_hours': 48,
'labour_days': 2, 'has_battery': False,
'innovation_rate': 0.0, 'recommendation_id': '29_phase=5',
'efficiency': np.float64(368.263125)
}
]
]
}
representative_recommendations2 = {
614626: [
{
'phase': 0,
'type': 'loft_insulation',
'measure_type': 'loft_insulation',
'sap_points': 0,
'survey': False,
'recommendation_id': '0_phase=0',
},
{
'phase': 1,
'type': 'mechanical_ventilation',
'measure_type': 'mechanical_ventilation',
'sap_points': np.float64(-1.4000000000000057),
'recommendation_id': '3_phase=1'
},
{
'phase': 2,
'type': 'low_energy_lighting',
'measure_type': 'low_energy_lighting',
'sap_points': 5,
'survey': True,
'recommendation_id': '4_phase=2',
},
{
'type': 'heating',
'measure_type': 'roomstat_programmer_trvs',
'phase': 3,
'sap_points': np.float64(1.0),
'recommendation_id': '5_phase=3',
},
{
'phase': 4,
'type': 'secondary_heating',
'measure_type': 'secondary_heating',
'sap_points': np.float64(0.0),
'recommendation_id': '8_phase=4',
},
{
'phase': 5,
'type': 'solar_pv',
'measure_type': 'solar_pv',
'sap_points': np.float64(16.0),
'recommendation_id': '29_phase=5',
}
]
}
recommendations_with_impact2, impact_summary2, adjustments2 = (
Recommendations.calculate_recommendation_impact(
property_instance=property_instance,
all_predictions=all_predictions2,
recommendations=recommendations2,
representative_recommendations=representative_recommendations2,
debug=True
)
)
assert adjustments2 == [
{'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)},
{'recommendation_id': '5_phase=3', 'phase': 3, 'sap_adjustment': np.float64(1.0)},
{'recommendation_id': '6_phase=3', 'phase': 3, 'sap_adjustment': np.float64(1.0000000000000027)}
]
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,
model_predicted_sap=0
)
)
# 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,
model_predicted_sap=0
)
)
# 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,
model_predicted_sap=0
)
)
# Exactly 1 → no adjustment
assert updated_adjustments == []
assert updated_current["sap"] == 1.0
assert updated_impact["sap"] == -1.0
def test_mechanical_ventilation_sap_zero_no_adjustment(property_instance):
# Test when SAP = 0
rec = {
"type": "mechanical_ventilation",
"recommendation_id": "mv_test",
"phase": 1,
}
previous_phase_values = {'phase': 0, 'representative': True, 'recommendation_id': '0_phase=0',
'measure_type': 'flat_roof_insulation', 'sap': 68.0, 'carbon': np.float64(0.5),
'heat_demand': np.float64(300.1), 'sap_prediction': np.float64(71.7)}
current_phase_values = {'sap': 68.0, 'carbon': np.float64(0.5), 'heat_demand': np.float64(307.0)}
property_phase_impact = {'sap': 0, 'carbon': 0, 'heat_demand': np.float64(-6.899999999999977)}
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,
model_predicted_sap=0
)
)
# SAP is already at 0 → no adjustment expected
assert updated_adjustments == []
assert updated_current["sap"] == 68.0
assert updated_impact["sap"] == 0
def test_mv_valid_negative_no_adjustment(property_instance):
rec = {"type": "mechanical_ventilation", "recommendation_id": "mv", "phase": 1}
previous = {"sap": 70.0}
current = {"sap": 67.0}
impact = {"sap": -3.0, "carbon": 0, "heat_demand": 0}
adjustments = []
updated_impact, updated_current, updated_adjustments = (
Recommendations._apply_measure_specific_rules(
rec, impact, previous, current, adjustments, property_instance, 0
)
)
assert updated_adjustments == []
assert updated_current["sap"] == 67.0
assert updated_impact["sap"] == -3.0
def test_mv_zero_impact_allowed(property_instance):
rec = {"type": "mechanical_ventilation", "recommendation_id": "mv", "phase": 1}
previous = {"sap": 68.0, "sap_prediction": 71.7}
current = {"sap": 68.0}
impact = {"sap": 0.0, "carbon": 0, "heat_demand": 0}
adjustments = []
updated_impact, updated_current, updated_adjustments = (
Recommendations._apply_measure_specific_rules(
rec, impact, previous, current, adjustments, property_instance, 0
)
)
assert updated_adjustments == []
assert updated_current["sap"] == 68.0
assert updated_impact["sap"] == 0.0
def test_mv_positive_impact_corrected(property_instance):
rec = {"type": "mechanical_ventilation", "recommendation_id": "mv", "phase": 1}
previous = {"sap": 60.0}
current = {"sap": 61.0}
impact = {"sap": 1.0, "carbon": 0, "heat_demand": 0}
adjustments = []
updated_impact, updated_current, updated_adjustments = (
Recommendations._apply_measure_specific_rules(
rec, impact, previous, current, adjustments, property_instance, 0
)
)
assert len(updated_adjustments) == 1
assert updated_current["sap"] == previous["sap"] + updated_impact["sap"]
assert updated_impact["sap"] <= 0
def test_mv_below_lower_bound_corrected(property_instance):
rec = {"type": "mechanical_ventilation", "recommendation_id": "mv", "phase": 1}
previous = {"sap": 70.0}
current = {"sap": 64.0}
impact = {"sap": -6.0, "carbon": 0, "heat_demand": 0}
adjustments = []
updated_impact, updated_current, updated_adjustments = (
Recommendations._apply_measure_specific_rules(
rec, impact, previous, current, adjustments, property_instance, 0
)
)
assert len(updated_adjustments) == 1
assert updated_impact["sap"] >= -4
def test_mv_floor_triggered(property_instance):
rec = {"type": "mechanical_ventilation", "recommendation_id": "mv", "phase": 1}
previous = {"sap": 2.0}
current = {"sap": 0.5}
impact = {"sap": -1.5, "carbon": 0, "heat_demand": 0}
adjustments = []
updated_impact, updated_current, updated_adjustments = (
Recommendations._apply_measure_specific_rules(
rec, impact, previous, current, adjustments, property_instance, 0
)
)
assert updated_current["sap"] == 1.0
assert updated_adjustments[0]["sap_adjustment"] > 0
def test_mv_exactly_one_no_floor(property_instance):
rec = {"type": "mechanical_ventilation", "recommendation_id": "mv", "phase": 1}
previous = {"sap": 2.0}
current = {"sap": 1.0}
impact = {"sap": -1.0, "carbon": 0, "heat_demand": 0}
adjustments = []
updated_impact, updated_current, updated_adjustments = (
Recommendations._apply_measure_specific_rules(
rec, impact, previous, current, adjustments, property_instance, 0
)
)
assert updated_adjustments == []
assert updated_current["sap"] == 1.0
def test_lighting_no_cap(property_instance):
rec = {"type": "low_energy_lighting", "recommendation_id": "led", "phase": 1,
"co2_equivalent_savings": 0}
previous = {"sap": 60.0, "carbon": 2.0}
current = {"sap": 61.0, "carbon": 2.0}
impact = {"sap": 1.0, "carbon": 0, "heat_demand": 0}
adjustments = []
updated_impact, updated_current, updated_adjustments = (
Recommendations._apply_measure_specific_rules(
rec, impact, previous, current, adjustments, property_instance, 0
)
)
assert updated_adjustments == []
def test_filter_phase_adjustments():
example_adjustments = [
{'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)},
{'recommendation_id': '5_phase=3', 'phase': 3, 'sap_adjustment': np.float64(1.0)},
{'recommendation_id': '6_phase=3', 'phase': 3, 'sap_adjustment': np.float64(1.0000000000000027)}
]
res = Recommendations._filter_phase_adjustment(example_adjustments)
assert res == [
{'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)},
{'recommendation_id': '6_phase=3', 'phase': 3, 'sap_adjustment': np.float64(1.0000000000000027)}
]