mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
215 lines
7.1 KiB
Python
215 lines
7.1 KiB
Python
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
|
||
from recommendations.tests.test_data.materials import materials
|
||
|
||
|
||
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
|
||
x["has_wood_chips"] = 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
|
||
}
|
||
)
|
||
p.already_installed = []
|
||
|
||
# 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, materials=materials)
|
||
# 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)) == 8.5
|
||
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)) == 11.2 # largest model
|
||
|
||
|
||
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]
|