import numpy as np import pytest import math from unittest.mock import MagicMock from recommendations import recommendation_utils from datatypes.enums import QuantityUnits from recommendations.tests.test_data.wall_uvalue_test_cases import wall_uvalue_test_cases from recommendations.tests.test_data.floor_uvalue_test_cases import floor_uvalue_test_cases class TestRecommendationUtils: @pytest.fixture def property_mock(self): PropertyMock = MagicMock() PropertyMock.data = { 'total_floor_area_group_decile': 'Decile 1', 'number-habitable-rooms': 3, 'number-heated-rooms': 2 } return PropertyMock def test_r_value_per_mm_to_u_value(self): assert recommendation_utils.r_value_per_mm_to_u_value(1, 2) == 0.5 with pytest.raises(ZeroDivisionError): recommendation_utils.r_value_per_mm_to_u_value(0, 2) def test_calculate_u_value_uplift(self): assert recommendation_utils.calculate_u_value_uplift(1, 2) == (0.33333333333333337, 0.6666666666666666) with pytest.raises(ZeroDivisionError): recommendation_utils.calculate_u_value_uplift(0, 2) with pytest.raises(ZeroDivisionError): recommendation_utils.calculate_u_value_uplift(1, 0) def test_is_diminishing_returns(self): assert not recommendation_utils.is_diminishing_returns([1, 2, 3], 1, 1, 1) assert recommendation_utils.is_diminishing_returns([1, 2, 3], 0.5, 1, 1) assert not recommendation_utils.is_diminishing_returns([], 1, None, 1) def test_update_lowest_selected_u_value(self): assert recommendation_utils.update_lowest_selected_u_value(1, 2) == 1 assert recommendation_utils.update_lowest_selected_u_value(None, 2) == 2 assert recommendation_utils.update_lowest_selected_u_value(1, 0.5) == 0.5 def test_get_recommended_part(self): part = {'depths': [1, 2, 3]} assert recommendation_utils.get_recommended_part( part=part, selected_depth=1, selected_total_cost=50, quantity=99, quantity_unit="m2" ) == {'depths': [1], 'estimated_cost': 50, 'quantity': 99, 'quantity_unit': QuantityUnits.m2.value} def test_get_roof_u_value(self): # Test case 1: Insulation thickness is known and is_loft is True inputs = { 'insulation_thickness': '50', 'is_loft': True, 'is_roof_room': False, 'is_thatched': False, 'has_dwelling_above': False, 'is_flat': False, 'is_pitched': True, 'is_at_rafters': False, } for age_band in ["A", "B", "C", "D"]: assert recommendation_utils.get_roof_u_value(**{**inputs, "age_band": age_band}) == 0.68 def test_get_roof_u_value_case_2(self): inputs = { 'original_description': 'Pitched, 400+ mm insulation at joists', 'clean_description': 'Pitched, 400+ mm insulation at joists', 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': True, 'is_roof_room': False, 'is_loft': False, 'is_flat': False, 'is_thatched': False, 'is_at_rafters': False, 'is_assumed': False, 'has_dwelling_above': False, 'is_valid': True, 'insulation_thickness': '400+', 'age_band': "J" } u_value = recommendation_utils.get_roof_u_value(**inputs) assert u_value == 0.16, f"Expected 0.16, but got {u_value}" def test_get_roof_u_value_case_3(self): inputs = { 'original_description': 'Room-in-roof, 200 mm insulation at rafters', 'clean_description': 'Room-in-roof, 200 mm insulation at rafters', 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False, 'is_roof_room': True, 'is_loft': False, 'is_flat': False, 'is_thatched': False, 'is_at_rafters': True, 'is_assumed': False, 'has_dwelling_above': False, 'is_valid': True, 'insulation_thickness': '200', 'age_band': "J" } u_value = recommendation_utils.get_roof_u_value(**inputs) assert u_value == 0.21, f"Expected 0.21, but got {u_value}" def test_get_roof_u_value_case_4(self): inputs = { 'original_description': 'Pitched, below average insulation', 'clean_description': 'Pitched, below average insulation', 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': True, 'is_roof_room': False, 'is_loft': False, 'is_flat': False, 'is_thatched': False, 'is_at_rafters': False, 'is_assumed': False, 'has_dwelling_above': False, 'is_valid': True, 'insulation_thickness': 'below average', 'age_band': "E" } u_value = recommendation_utils.get_roof_u_value(**inputs) assert u_value == 1.5, f"Expected 1.5, but got {u_value}" def test_get_roof_u_value_case_5(self): # Test case where insulation thickness is exactly specified inputs = { 'original_description': 'Pitched, 100mm insulation', 'clean_description': 'Pitched, 100mm insulation', 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': True, 'is_roof_room': False, 'is_loft': False, 'is_flat': False, 'is_thatched': False, 'is_at_rafters': False, 'is_assumed': False, 'has_dwelling_above': False, 'is_valid': True, 'insulation_thickness': '100', 'age_band': "G" } u_value = recommendation_utils.get_roof_u_value(**inputs) assert u_value == 0.40, f"Expected 0.40, but got {u_value}" def test_get_roof_u_value_case_6(self): # Test case for a thatched roof inputs = { 'original_description': 'Thatched, 75mm insulation', 'clean_description': 'Thatched, 75mm insulation', 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False, 'is_roof_room': False, 'is_loft': False, 'is_flat': False, 'is_thatched': True, 'is_at_rafters': False, 'is_assumed': False, 'has_dwelling_above': False, 'is_valid': True, 'insulation_thickness': '75', 'age_band': "H" } u_value = recommendation_utils.get_roof_u_value(**inputs) assert u_value == 0.35, f"Expected 0.35, but got {u_value}" def test_get_roof_u_value_case_7(self): # Test case where the roof has a room in it inputs = { 'original_description': 'Pitched, room-in-roof, 100mm insulation', 'clean_description': 'Pitched, room-in-roof, 100mm insulation', 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': True, 'is_roof_room': True, 'is_loft': False, 'is_flat': False, 'is_thatched': False, 'is_at_rafters': False, 'is_assumed': False, 'has_dwelling_above': False, 'is_valid': True, 'insulation_thickness': '100', 'age_band': "J" } u_value = recommendation_utils.get_roof_u_value(**inputs) assert u_value == 0.40, f"Expected 0.40, but got {u_value}" def test_get_roof_u_value_case_8(self): # Test case where there is a dwelling above the roof, U-value should be 0 inputs = { 'original_description': 'Pitched, 100mm insulation', 'clean_description': 'Pitched, 100mm insulation', 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': True, 'is_roof_room': False, 'is_loft': False, 'is_flat': False, 'is_thatched': False, 'is_at_rafters': False, 'is_assumed': False, 'has_dwelling_above': True, 'is_valid': True, 'insulation_thickness': '100', 'age_band': "J" } u_value = recommendation_utils.get_roof_u_value(**inputs) assert u_value == 0.0, f"Expected 0.0, but got {u_value}" @pytest.mark.parametrize( "test_case", wall_uvalue_test_cases ) def test_get_wall_uvalue(self, test_case): expected_uvalue = test_case["uvalue"] inputs = test_case.copy() del inputs["uvalue"] uvalue = recommendation_utils.get_wall_u_value(**inputs) assert expected_uvalue == uvalue, f"Expected u value {expected_uvalue}, recieved {uvalue}" @pytest.mark.parametrize("test_input", floor_uvalue_test_cases) def test_get_floor_u_value(self, test_input): if not isinstance(test_input["expected"], float): with pytest.raises(test_input["expected"]): recommendation_utils.get_floor_u_value( test_input["floor_type"], test_input["area"], test_input["perimeter"], test_input["age_band"], test_input["wall_type"], test_input["insulation_thickness"], ) else: result = recommendation_utils.get_floor_u_value( floor_type=test_input["floor_type"], area=test_input["area"], perimeter=test_input["perimeter"], age_band=test_input["age_band"], wall_type=test_input["wall_type"], insulation_thickness=test_input["insulation_thickness"], ) assert result == pytest.approx(test_input["expected"], abs=1e-2) # Test with wall_type not in default_wall_thickness def test_wall_type_not_in_default_wall_thickness(self): with pytest.raises(IndexError): recommendation_utils.get_floor_u_value( floor_type="solid", area=100, perimeter=40, age_band="A", wall_type="InvalidWallType", insulation_thickness=None, ) # Test with age_band not in s11 def test_age_band_not_in_s11(self): with pytest.raises(IndexError): recommendation_utils.get_floor_u_value( floor_type="solid", area=100, perimeter=40, age_band="Z", wall_type="Cavity", insulation_thickness=None, ) def test_convert_thickness_to_numeric(self): assert recommendation_utils.convert_thickness_to_numeric("none", True) == 0 assert recommendation_utils.convert_thickness_to_numeric("below average", True) == 50 assert recommendation_utils.convert_thickness_to_numeric("average", True) == 100 assert recommendation_utils.convert_thickness_to_numeric("above average", True) == 270 assert recommendation_utils.convert_thickness_to_numeric("300+", True) == 300 assert recommendation_utils.convert_thickness_to_numeric("400+", True) == 400 assert recommendation_utils.convert_thickness_to_numeric("270", True) == 270 assert recommendation_utils.convert_thickness_to_numeric("none", False) == 0 assert recommendation_utils.convert_thickness_to_numeric("below average", False) == 100 assert recommendation_utils.convert_thickness_to_numeric("average", False) == 270 assert recommendation_utils.convert_thickness_to_numeric("above average", False) == 270 def test_estimate_perimeter_regular_inputs(): assert math.isclose( recommendation_utils.estimate_perimeter(100, 5), 40.24922359499622, rel_tol=1e-2 ) assert math.isclose( recommendation_utils.estimate_perimeter(123, 5), 44.63854836349408, rel_tol=1e-2 ) def test_estimate_perimeter_zero_floor_area(): with pytest.raises(ZeroDivisionError): recommendation_utils.estimate_perimeter(0, 5) with pytest.raises(ValueError): assert recommendation_utils.estimate_perimeter(0, 0) == 0 def test_estimate_perimeter_invalid_inputs(): with pytest.raises(ValueError): recommendation_utils.estimate_perimeter(100, 0) with pytest.raises(ValueError): recommendation_utils.estimate_perimeter(-100, 5) with pytest.raises(ValueError): recommendation_utils.estimate_perimeter(100, -5) def test_solid_floor(): assert math.isclose( recommendation_utils.get_floor_u_value( 'solid', 100, 40, 'A', 'stone', insulation_thickness="20mm" ), 0.4, rel_tol=1e-2) def test_suspended_floor(): assert math.isclose( recommendation_utils.get_floor_u_value( 'suspended', 100, 40, 'A', 'timber frame', insulation_thickness="20mm" ), 0.49, rel_tol=1e-2) def test_invalid_floor_type(): with pytest.raises(ValueError): recommendation_utils.get_floor_u_value( 'invalid_type', 100, 40, 'A', 'stone', insulation_thickness="20mm" ) def test_park_home(): assert recommendation_utils.get_floor_u_value( 'suspended', 100, 40, 'A', 'park home', insulation_thickness="20mm" ) == 0 def test_esimtate_pitched_roof_area(): roof_area1 = recommendation_utils.esimtate_pitched_roof_area( floor_area=100, floor_height=2 ) assert np.isclose(roof_area1, 107.70329614269008) # As the floor height gets bigger, the area should get bigger roof_area2 = recommendation_utils.esimtate_pitched_roof_area( floor_area=100, floor_height=3 ) assert np.isclose(roof_area2, 116.61903789690601) # As the floor area gets smaller, the area should get smaller roof_area3 = recommendation_utils.esimtate_pitched_roof_area( floor_area=100, floor_height=1 ) assert np.isclose(roof_area3, 101.9803902718557) # As the floor area decreases, area should decrease roof_area4 = recommendation_utils.esimtate_pitched_roof_area( floor_area=50, floor_height=2 ) assert np.isclose(roof_area4, 57.44562646538029) # As the floor area increases, area should increase roof_area5 = recommendation_utils.esimtate_pitched_roof_area( floor_area=150, floor_height=2 ) assert np.isclose(roof_area5, 157.797338380595) zero_roof_area = recommendation_utils.esimtate_pitched_roof_area( floor_area=0, floor_height=1000 ) assert zero_roof_area == 0 # If the floor height zero, we don't have a traingle, it's a flat roof flat_roof_area = recommendation_utils.esimtate_pitched_roof_area( floor_area=1000, floor_height=0 ) assert flat_roof_area == 1000 zero_roof_area2 = recommendation_utils.esimtate_pitched_roof_area( floor_area=0, floor_height=0 ) assert zero_roof_area2 == 0