diff --git a/recommendations/FloorRecommendations.py b/recommendations/FloorRecommendations.py index a8193aec..a19cf1a8 100644 --- a/recommendations/FloorRecommendations.py +++ b/recommendations/FloorRecommendations.py @@ -5,7 +5,7 @@ from datatypes.enums import QuantityUnits from backend.Property import Property from recommendations.recommendation_utils import ( r_value_per_mm_to_u_value, calculate_u_value_uplift, is_diminishing_returns, update_lowest_selected_u_value, - get_recommended_part, estimate_perimeter, estimate_perimeter_2_rooms, get_wall_type, + get_recommended_part, estimate_perimeter, get_wall_type, get_floor_u_value ) @@ -103,12 +103,7 @@ class FloorRecommendations(Definitions): else: raise NotImplementedError("Implement me") - scaled_num_rooms = number_of_rooms / num_floors - - if scaled_num_rooms <= 2.5: - estimated_perimeter = estimate_perimeter_2_rooms(total_floor_area / num_floors) - else: - estimated_perimeter = estimate_perimeter(total_floor_area / num_floors, scaled_num_rooms) + estimated_perimeter = estimate_perimeter(total_floor_area / num_floors, number_of_rooms / num_floors) wall_type = get_wall_type(**self.property.walls) diff --git a/recommendations/recommendation_utils.py b/recommendations/recommendation_utils.py index 712eabdd..8a113050 100644 --- a/recommendations/recommendation_utils.py +++ b/recommendations/recommendation_utils.py @@ -300,6 +300,16 @@ def get_roof_u_value( def estimate_perimeter(floor_area, num_rooms): + """ + Uses a basic methodology to attempt to estimate perimeter. Works better for + :param floor_area: floor area of the home + :param num_rooms: number of rooms in the home + :return: estimated perimeter + """ + if floor_area < 0: + raise ValueError("Floor area cannot be negative.") + if num_rooms <= 0: + raise ValueError("Number of rooms must be greater than zero.") # Compute average room size based on total floor area and number of rooms avg_room_size = floor_area / num_rooms @@ -319,16 +329,6 @@ def estimate_perimeter(floor_area, num_rooms): return perimeter -def estimate_perimeter_2_rooms(floor_area): - # Assuming a square layout for the entire floor area to get a first-order approximation of the perimeter - side_length = math.sqrt(floor_area) - - # Calculating the perimeter of the square layout - perimeter = 4 * side_length - - return perimeter - - def get_floor_u_value(floor_type, area, perimeter, age_band, wall_type, insulation_thickness=None): """ Estimate the u-value of a suspended floor, based on RdSap methodology diff --git a/recommendations/tests/test_data/floor_uvalue_test_cases.py b/recommendations/tests/test_data/floor_uvalue_test_cases.py new file mode 100644 index 00000000..91d3814f --- /dev/null +++ b/recommendations/tests/test_data/floor_uvalue_test_cases.py @@ -0,0 +1,32 @@ +floor_uvalue_test_cases = [ + # Test with solid floor, no insulation + { + "floor_type": "solid", + "area": 100, + "perimeter": 40, + "age_band": "A", + "wall_type": "cavity", + "insulation_thickness": None, + "expected": 0.62, + }, + # Test with suspended floor, with insulation + { + "floor_type": "suspended", + "area": 120, + "perimeter": 44, + "age_band": "B", + "wall_type": "solid brick", + "insulation_thickness": "50mm", + "expected": 0.33, + }, + # Test with invalid floor type + { + "floor_type": "invalid", + "area": 100, + "perimeter": 40, + "age_band": "A", + "wall_type": "cavity", + "insulation_thickness": None, + "expected": ValueError, + }, +] diff --git a/recommendations/tests/test_floor_recommendations.py b/recommendations/tests/test_floor_recommendations.py index a1b117b7..1632f468 100644 --- a/recommendations/tests/test_floor_recommendations.py +++ b/recommendations/tests/test_floor_recommendations.py @@ -128,7 +128,7 @@ class TestWallRecommendations: assert recommender.estimated_u_value is None recommender.recommend() assert recommender.property.floor["is_suspended"] - assert recommender.estimated_u_value == 0.51 + assert recommender.estimated_u_value == 0.52 assert recommender.recommendations types = {part["type"] for x in recommender.recommendations for part in x["parts"]} diff --git a/recommendations/tests/test_recommendation_utils.py b/recommendations/tests/test_recommendation_utils.py index 5705aa1c..b3ea0141 100644 --- a/recommendations/tests/test_recommendation_utils.py +++ b/recommendations/tests/test_recommendation_utils.py @@ -1,8 +1,10 @@ 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: @@ -227,3 +229,78 @@ class TestRecommendationUtils: 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_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)