"""Tests for SAP10.2 efficiency + fuel-price lookups. Reference values: - Table 4a (Heating systems — space and water) in SAP10.2 (14-03-2025) - Table 4b (Seasonal efficiency for gas and liquid fuel boilers) - Table 32 (RdSAP10-specific fuel prices, emission factors, primary energy) Returns decimal efficiencies (0.80, not 80) and pence-per-kWh prices. Helpers never raise on missing codes; they fall back to typical-fuel values. """ import pytest from domain.sap10_ml.sap_efficiencies import ( fuel_unit_price_p_per_kwh, seasonal_efficiency, water_heating_efficiency, ) # ----- Space-heating seasonal efficiency (Table 4a / 4b) ----- def test_seasonal_efficiency_condensing_gas_combi_returns_table4b_winter_value() -> None: # Arrange — Table 4b, code 104 condensing combi with automatic ignition -> 84% winter. # Act result = seasonal_efficiency(sap_main_heating_code=104) # Assert assert result == pytest.approx(0.84, abs=0.005) def test_seasonal_efficiency_air_source_heat_pump_returns_table4a_value() -> None: # Arrange — Table 4a, code 214 ASHP <=35C -> 170% space. # Act result = seasonal_efficiency(sap_main_heating_code=214) # Assert assert result == pytest.approx(1.70, abs=0.005) def test_seasonal_efficiency_ground_source_heat_pump_returns_table4a_value() -> None: # Arrange — Table 4a, code 211 GSHP <=35C -> 230% space. # Act result = seasonal_efficiency(sap_main_heating_code=211) # Assert assert result == pytest.approx(2.30, abs=0.005) def test_seasonal_efficiency_oil_boiler_returns_table4b_value() -> None: # Arrange — Table 4b, code 126 standard oil 1998+ -> 80% winter. # Act result = seasonal_efficiency(sap_main_heating_code=126) # Assert assert result == pytest.approx(0.80, abs=0.005) def test_seasonal_efficiency_electric_storage_heater_returns_unity() -> None: # Arrange — Table 4a, code 401 storage heater -> 100%. # Act result = seasonal_efficiency(sap_main_heating_code=401) # Assert assert result == pytest.approx(1.0, abs=0.005) def test_seasonal_efficiency_unknown_code_falls_back_to_mid_range() -> None: # Arrange — code not in table. # Act result = seasonal_efficiency(sap_main_heating_code=None) # Assert — gas-boiler typical ~0.80 W/W. assert result == pytest.approx(0.80, abs=0.01) def test_seasonal_efficiency_null_code_uses_heat_pump_category_fallback() -> None: # Arrange — many real certs have sap_main_heating_code=None but the gov # API still gives main_heating_category=4 (heat pump). Without the # category fallback `seasonal_efficiency` returns 0.80 (gas boiler), # under-counting a heat pump's COP by ~3x and driving sap_score down. # Act result = seasonal_efficiency( sap_main_heating_code=None, main_heating_category=4, ) # Assert — SAP10.2 Table 4a heat-pump space COP ~2.30 (code 211 typical). assert result == pytest.approx(2.30, abs=0.01) def test_seasonal_efficiency_null_code_uses_storage_heater_category_fallback() -> None: # Arrange — cat=7 (high-heat-retention electric storage) with null code. # Act result = seasonal_efficiency( sap_main_heating_code=None, main_heating_category=7, ) # Assert — Table 4a electric storage = 1.00. assert result == pytest.approx(1.00, abs=0.01) def test_seasonal_efficiency_null_code_room_heaters_gas_fuel_fallback() -> None: # Arrange — cat=10 (room heaters) + fuel=26 (mains gas, gov API code). # Without the fuel-aware fallback, gas room heaters get the 0.80 default # (gas boiler) when they should be ~0.55 (Table 4a 605-606 gas decorative). # Act result = seasonal_efficiency( sap_main_heating_code=None, main_heating_category=10, main_fuel_type=26, ) # Assert assert result == pytest.approx(0.55, abs=0.05) def test_seasonal_efficiency_null_code_room_heaters_electric_fuel_fallback() -> None: # Arrange — cat=10 + fuel=29 (electricity not community). # Act result = seasonal_efficiency( sap_main_heating_code=None, main_heating_category=10, main_fuel_type=29, ) # Assert — electric room heater = 1.00. assert result == pytest.approx(1.00, abs=0.01) def test_seasonal_efficiency_explicit_code_beats_category_fallback() -> None: # Arrange — when both are present, the SAP code is authoritative. # Code 211 GSHP -> 2.30; category=2 (boilers) would otherwise return 0.80. # Act result = seasonal_efficiency( sap_main_heating_code=211, main_heating_category=2, ) # Assert assert result == pytest.approx(2.30, abs=0.01) def test_seasonal_efficiency_null_code_central_heating_category_keeps_default() -> None: # Arrange — cat=2 (central heating with separate HW) -> keep gas-boiler default. # Act result = seasonal_efficiency( sap_main_heating_code=None, main_heating_category=2, ) # Assert assert result == pytest.approx(0.80, abs=0.01) # ----- Water-heating efficiency (Table 4a hot-water section) ----- def test_water_heating_efficiency_immersion_returns_unity() -> None: # Arrange — Table 4a, code 903 electric immersion -> 100%. # Act result = water_heating_efficiency(water_heating_code=903, main_heating_code=None) # Assert assert result == pytest.approx(1.0, abs=0.005) def test_water_heating_efficiency_from_main_system_inherits_main_efficiency() -> None: # Arrange — Table 4a, code 901 "from main heating system". # Act result = water_heating_efficiency(water_heating_code=901, main_heating_code=104) # Assert — inherits main code 104 (condensing gas combi) -> 0.84. assert result == pytest.approx(0.84, abs=0.005) def test_water_heating_efficiency_unknown_falls_back_to_typical() -> None: # Arrange — no signal. # Act result = water_heating_efficiency(water_heating_code=None, main_heating_code=None) # Assert — gas-combi typical 0.78. assert result == pytest.approx(0.78, abs=0.05) # ----- Fuel prices (Table 32) ----- def test_fuel_unit_price_mains_gas_returns_table32_value() -> None: # Arrange — Table 32, code 1 mains gas -> 3.48 p/kWh. # Act result = fuel_unit_price_p_per_kwh(fuel_code=1) # Assert assert result == pytest.approx(3.48, abs=0.01) def test_fuel_unit_price_oil_returns_table32_value() -> None: # Arrange — Table 32, code 4 heating oil -> 5.44 p/kWh. # Act result = fuel_unit_price_p_per_kwh(fuel_code=4) # Assert assert result == pytest.approx(5.44, abs=0.01) def test_fuel_unit_price_standard_electricity_returns_table32_value() -> None: # Arrange — Table 32, code 30 standard tariff -> 13.19 p/kWh. # Act result = fuel_unit_price_p_per_kwh(fuel_code=30) # Assert assert result == pytest.approx(13.19, abs=0.01) def test_fuel_unit_price_off_peak_low_rate_returns_table32_value() -> None: # Arrange — Table 32, code 31 7-hour low rate -> 5.50 p/kWh. # Act result = fuel_unit_price_p_per_kwh(fuel_code=31) # Assert assert result == pytest.approx(5.50, abs=0.01) def test_fuel_unit_price_unknown_falls_back_to_mains_gas() -> None: # Arrange — unknown code. # Act result = fuel_unit_price_p_per_kwh(fuel_code=None) # Assert — mains gas typical (most common UK heating fuel). assert result == pytest.approx(3.48, abs=0.01) # Gov EPC API uses a different fuel enum from SAP10.2 Table 32. The mapper # stores the API codes in primary_main_fuel_type / water_heating_fuel so we # must translate (e.g. API 29 = electricity not community -> Table 32 30). def test_fuel_unit_price_recognises_api_code_26_mains_gas_not_community() -> None: # Arrange / Act — gov API code 26 ("mains gas (not community)") -> Table 32 code 1 (3.48 p/kWh). assert fuel_unit_price_p_per_kwh(fuel_code=26) == pytest.approx(3.48, abs=0.01) def test_fuel_unit_price_recognises_api_code_28_oil_not_community() -> None: # Arrange / Act — gov API code 28 = oil; should map to Table 32 oil (5.44 p/kWh). assert fuel_unit_price_p_per_kwh(fuel_code=28) == pytest.approx(5.44, abs=0.01) def test_fuel_unit_price_recognises_api_code_29_electricity_not_community() -> None: # Arrange / Act — gov API code 29 = electricity; standard tariff 13.19 p/kWh. assert fuel_unit_price_p_per_kwh(fuel_code=29) == pytest.approx(13.19, abs=0.01) def test_fuel_unit_price_recognises_api_code_27_lpg_not_community() -> None: # Arrange / Act — gov API code 27 = LPG not community -> bulk LPG 7.60 p/kWh. assert fuel_unit_price_p_per_kwh(fuel_code=27) == pytest.approx(7.60, abs=0.01)