Model/recommendations/tests/test_heating_recommendations.py
Khalim Conn-Kowlessar b9a60e10d1 debugging backend
2025-11-04 20:55:01 +00:00

212 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from datetime import datetime
import pandas as pd
import pickle
import pytest
from backend.Property import Property
from etl.epc.Record import EPCRecord
from etl.bill_savings.KwhData import KwhData
from recommendations.HeatingRecommender import HeatingRecommender
from recommendations.tests.test_data.heating_recommendations_data import testing_examples
class TestHeatingRecommendations:
@pytest.fixture()
def cleaning_data(self):
with open("recommendations/tests/test_data/cleaning_data.pkl", "rb") as f:
data = pickle.load(f)
return data
@pytest.fixture()
def cleaned(self):
with open("recommendations/tests/test_data/cleaned.pkl", "rb") as f:
df = pickle.load(f)
return df
@pytest.fixture()
def kwh_client(self):
client = KwhData(bucket="retrofit-data-dev", read_consumption_data=False)
# We fix this pricing table for these tests
client.retail_price_comparison = pd.DataFrame(
[
{
"Date": datetime.today().strftime("%Y-%m-%d"),
'Average standard variable tariff (Large legacy suppliers)': 1
}
]
)
client.retail_price_comparison["Date"] = pd.to_datetime(client.retail_price_comparison["Date"])
return client
@pytest.mark.parametrize(
"test_case",
testing_examples
)
def test_recommend(self, test_case, cleaning_data, cleaned, kwh_client):
"""
With this function, we test out multiple heating descriptions and check which recomendations
we retrieve alongside them
:return:
"""
# We patch an old version of cleaned which is missing some attributes for 'mainheat-description'
for x in cleaned['mainheat-description']:
x["has_hot-water-only"] = False
x["has_mineral_and_wood"] = False
x["has_dual_fuel_appliance"] = False
epc_records = {"original_epc": test_case["epc"].copy(), "full_sap_epc": {}, "old_data": []}
epc_record = EPCRecord(
epc_records=epc_records,
run_mode="newdata",
cleaning_data=cleaning_data
)
p = Property(
id=0,
postcode=test_case["epc"]["postcode"],
address=test_case["epc"]["address"],
epc_record=epc_record,
energy_assessment={
"condition": {},
"energy_assessment_is_newer": False
}
)
# For these tests, this can be fixed
kwh_predictions = {
"heating_kwh_predictions": pd.DataFrame(
[
{"id": p.uprn, "predictions": 12000}
]
),
"hotwater_kwh_predictions": pd.DataFrame(
[
{"id": p.uprn, "predictions": 3000}
]
),
}
p.set_features(cleaned=cleaned, kwh_client=kwh_client, kwh_predictions=kwh_predictions)
recommender = HeatingRecommender(property_instance=p)
# Check they're empty
assert not recommender.heating_recommendations
recommender.recommend(has_cavity_or_loft_recommendations=False)
assert len(recommender.heating_recommendations) == len(test_case["heating_measure_types"])
# Check the exact measure types
assert (
{x["measure_type"] for x in recommender.heating_recommendations} ==
set(test_case["heating_measure_types"])
)
@pytest.mark.parametrize(
"floor_area, epc_primary, expected_band, expected_model",
[
# Case 1 Typical pre-2000 house, gas heating
(
93.75,
270.19,
(2.5, 4.6), # expected rough band (low, high)
5, # chosen model
),
# Case 2 Efficient new-build (low EPC energy)
(
93.75,
142.28,
(1.4, 2.4),
3, # assume 3 or 5 kW model covers this
),
],
)
def test_estimate_peak_kw_basic(floor_area, epc_primary, expected_band, expected_model):
"""
Ensure the peak load estimate is within a sensible range and
that the model selection logic picks the correct bracket.
"""
load_band = HeatingRecommender.estimate_peak_kw(
floor_area_m2=floor_area,
epc_primary_kwh_per_m2_yr=epc_primary,
primary_to_delivered_factor=1.55, # electricity
space_heat_fraction_range=(0.35, 0.60),
hdd_base_dd=2000.0,
t_indoor_C=21.0,
t_design_ext_C=-1.0,
)
# Assert range sanity
assert expected_band[0] * 0.8 <= load_band[0] <= expected_band[1] * 1.2
assert expected_band[0] <= load_band[1] <= expected_band[1] * 1.2
# Pick model
model = HeatingRecommender.pick_model(load_band, models_kw=(3, 5, 6, 8.5, 11.2))
assert model == expected_model
def test_estimate_peak_kw_with_hlp():
"""
Test direct HLP input path (best-quality data).
"""
hlp = 1.5 # W/m²K typical for semi-detached
floor_area = 100
load_band = HeatingRecommender.estimate_peak_kw(
floor_area_m2=floor_area,
heat_loss_parameter_W_per_m2K=hlp,
t_indoor_C=21,
t_design_ext_C=-2,
)
# Should return identical low/high values since it's direct
assert isinstance(load_band, tuple)
assert abs(load_band[0] - load_band[1]) < 1e-6
# Expected peak = 1.5 * 100 * 23 / 1000 = 3.45 kW
assert pytest.approx(load_band[0], rel=0.05) == 3.45
def test_estimate_peak_kw_with_space_heat_demand():
"""
Test the space-heating-demand path.
"""
floor_area = 120
space_heat_kwh_m2 = 100
load_band = HeatingRecommender.estimate_peak_kw(
floor_area_m2=floor_area,
space_heat_kwh_per_m2_yr=space_heat_kwh_m2,
hdd_base_dd=2100,
t_indoor_C=21,
t_design_ext_C=-3,
)
# Rough expected peak ~ (100*120*1000)/(2100*24) * 24 /1000 = 5.4 kW
assert 4.5 < load_band[0] < 6.0
assert abs(load_band[0] - load_band[1]) < 1e-6
def test_pick_model_boundaries():
"""
Ensure pick_model correctly selects the smallest model covering the upper band.
"""
assert HeatingRecommender.pick_model((2.0, 4.9), models_kw=(3, 5, 6, 8.5)) == 5
assert HeatingRecommender.pick_model((5.0, 5.0), models_kw=(3, 5, 6, 8.5)) == 5
assert HeatingRecommender.pick_model((5.0, 6.1), models_kw=(3, 5, 6, 8.5)) == 6
assert HeatingRecommender.pick_model((8.6, 9.0), models_kw=(3, 5, 6, 8.5, 11.2)) == 11.2
assert HeatingRecommender.pick_model((20, 25), models_kw=(3, 5, 6, 8.5, 11.2)) is None
def test_parameter_validation_and_defaults():
"""
Validate that the function handles missing or minimal parameters properly.
"""
# Minimal path using primary energy only
load_band = HeatingRecommender.estimate_peak_kw(
floor_area_m2=80,
epc_primary_kwh_per_m2_yr=250,
)
assert isinstance(load_band, tuple)
assert load_band[0] < load_band[1]