"""Tests for crude annual demand approximations (slice 16d).""" import pytest from domain.sap10_ml.demand import ( predicted_hot_water_kwh, predicted_lighting_kwh, predicted_space_heating_kwh, ) def test_predicted_space_heating_scales_with_envelope_w_per_k() -> None: # Arrange — same region, same efficiency, double the HLC -> double the kWh. # Act low = predicted_space_heating_kwh(envelope_heat_loss_w_per_k=100.0, region_code="1", seasonal_efficiency_main=0.84) high = predicted_space_heating_kwh(envelope_heat_loss_w_per_k=200.0, region_code="1", seasonal_efficiency_main=0.84) # Assert assert high == pytest.approx(2.0 * low, abs=0.01) def test_predicted_space_heating_returns_zero_when_efficiency_zero() -> None: # Arrange / Act / Assert assert predicted_space_heating_kwh(envelope_heat_loss_w_per_k=200.0, region_code="1", seasonal_efficiency_main=0.0) == 0.0 def test_predicted_space_heating_falls_back_to_uk_average_when_region_unknown() -> None: # Arrange — region None should still produce a finite positive kWh. # Act result = predicted_space_heating_kwh(envelope_heat_loss_w_per_k=200.0, region_code=None, seasonal_efficiency_main=0.84) # Assert assert result > 0.0 def test_predicted_space_heating_adds_ventilation_to_envelope_hlc() -> None: # Arrange — SAP10.2 HLC = envelope (conduction) + ventilation (infiltration); # demand scales with HLC, so adding 50 W/K of ventilation to a 200 W/K # envelope should give the same kWh as a 250 W/K envelope alone. # Act combined = predicted_space_heating_kwh( envelope_heat_loss_w_per_k=200.0, region_code="1", seasonal_efficiency_main=0.84, ventilation_heat_loss_w_per_k=50.0, ) equivalent = predicted_space_heating_kwh( envelope_heat_loss_w_per_k=250.0, region_code="1", seasonal_efficiency_main=0.84, ) # Assert assert combined == pytest.approx(equivalent, rel=0.001) def test_predicted_space_heating_default_ventilation_zero_preserves_envelope_only_behaviour() -> None: # Arrange — back-compat: callers that don't pass ventilation get the # original envelope-only result. # Act envelope_only = predicted_space_heating_kwh( envelope_heat_loss_w_per_k=200.0, region_code="1", seasonal_efficiency_main=0.84, ) explicit_zero = predicted_space_heating_kwh( envelope_heat_loss_w_per_k=200.0, region_code="1", seasonal_efficiency_main=0.84, ventilation_heat_loss_w_per_k=0.0, ) # Assert assert envelope_only == pytest.approx(explicit_zero, rel=0.001) def test_predicted_space_heating_scotland_higher_than_thames() -> None: # Arrange — same HLC, same efficiency; Scotland's HDH > Thames's. # Act thames = predicted_space_heating_kwh(envelope_heat_loss_w_per_k=200.0, region_code="1", seasonal_efficiency_main=0.84) scotland = predicted_space_heating_kwh(envelope_heat_loss_w_per_k=200.0, region_code="14", seasonal_efficiency_main=0.84) # Assert assert scotland > thames def test_predicted_hot_water_scales_with_floor_area() -> None: # Arrange — same efficiency, larger TFA -> more occupants -> more kWh. # Act small = predicted_hot_water_kwh(total_floor_area_m2=50.0, seasonal_efficiency_water=0.84) large = predicted_hot_water_kwh(total_floor_area_m2=150.0, seasonal_efficiency_water=0.84) # Assert assert large > small def test_predicted_hot_water_returns_zero_for_unspecified_floor_area() -> None: # Arrange / Act / Assert assert predicted_hot_water_kwh(total_floor_area_m2=None, seasonal_efficiency_water=0.84) == 0.0 def test_predicted_hot_water_kwh_adds_storage_loss_when_cylinder_described() -> None: # Arrange — SAP10.2 Appendix J / Table 2: cylinder storage loss adds to # the delivered DHW load. For a 110L cylinder with 38 mm foam (typical # post-1992) the loss factor is 0.0056 kWh/L/day; annual loss in heated # space = 110 * 0.0056 * 365 * 0.6 = 135 kWh useful -> delivered loss # /efficiency. Same home without cylinder description gets the simple # formula (no storage term). # Act with_cylinder = predicted_hot_water_kwh( total_floor_area_m2=80.0, seasonal_efficiency_water=0.84, cylinder_size=1, cylinder_insulation_thickness_mm=38, cylinder_insulation_type=2, # foam ) without_cylinder = predicted_hot_water_kwh( total_floor_area_m2=80.0, seasonal_efficiency_water=0.84, ) # Assert assert with_cylinder > without_cylinder # storage_loss = 110 * 0.0056 * 365 * 0.6 / 0.84 ≈ 161 kWh delivered. assert (with_cylinder - without_cylinder) == pytest.approx(161.0, abs=15.0) def test_predicted_hot_water_kwh_lower_storage_loss_for_thicker_insulation() -> None: # Arrange — same cylinder size, 12mm jacket vs 100mm foam. # Act jacket = predicted_hot_water_kwh( total_floor_area_m2=80.0, seasonal_efficiency_water=0.84, cylinder_size=1, cylinder_insulation_thickness_mm=12, cylinder_insulation_type=1, ) foam_100mm = predicted_hot_water_kwh( total_floor_area_m2=80.0, seasonal_efficiency_water=0.84, cylinder_size=1, cylinder_insulation_thickness_mm=100, cylinder_insulation_type=2, ) # Assert assert jacket > foam_100mm def test_predicted_hot_water_kwh_drops_with_wwhrs() -> None: # Arrange — WWHRS recovers ~15% of bath energy. # Act no_wwhrs = predicted_hot_water_kwh( total_floor_area_m2=80.0, seasonal_efficiency_water=0.84, has_wwhrs=False, ) with_wwhrs = predicted_hot_water_kwh( total_floor_area_m2=80.0, seasonal_efficiency_water=0.84, has_wwhrs=True, ) # Assert assert with_wwhrs < no_wwhrs def test_predicted_hot_water_kwh_drops_with_solar_water_heating() -> None: # Arrange — solar HW saves ~250 kWh/yr (SAP10.2 Appendix G simplified). # Act no_solar = predicted_hot_water_kwh( total_floor_area_m2=80.0, seasonal_efficiency_water=0.84, has_solar_water_heating=False, ) with_solar = predicted_hot_water_kwh( total_floor_area_m2=80.0, seasonal_efficiency_water=0.84, has_solar_water_heating=True, ) # Assert assert with_solar < no_solar def test_predicted_hot_water_kwh_uses_age_band_default_when_insulation_unspecified() -> None: # Arrange — RdSAP10 Table 29: A-F -> 12mm jacket; G-H -> 25mm foam; I-M -> 38mm foam. # Age G cylinder with no explicit insulation should default to 25mm foam, # giving a lower loss than age A (12mm jacket). # Act age_a = predicted_hot_water_kwh( total_floor_area_m2=80.0, seasonal_efficiency_water=0.84, cylinder_size=1, age_band="A", ) age_g = predicted_hot_water_kwh( total_floor_area_m2=80.0, seasonal_efficiency_water=0.84, cylinder_size=1, age_band="G", ) # Assert assert age_g < age_a def test_predicted_hot_water_typical_uk_home_falls_in_sensible_range() -> None: # Arrange — 80 m^2 home, gas-combi efficiency. # Act result = predicted_hot_water_kwh(total_floor_area_m2=80.0, seasonal_efficiency_water=0.84) # Assert — typical UK home DHW is 2000-3500 kWh/yr. assert 1500.0 < result < 4500.0 def test_predicted_lighting_drops_with_led_bulbs() -> None: # Arrange — same TFA, all-incandescent vs all-LED. # Act incandescent = predicted_lighting_kwh(total_floor_area_m2=100.0, cfl_count=0, led_count=0, incandescent_count=10) all_led = predicted_lighting_kwh(total_floor_area_m2=100.0, cfl_count=0, led_count=10, incandescent_count=0) # Assert assert all_led < incandescent def test_predicted_lighting_returns_zero_for_unspecified_floor_area() -> None: # Arrange / Act / Assert assert predicted_lighting_kwh(total_floor_area_m2=None, cfl_count=0, led_count=0, incandescent_count=10) == 0.0 def test_predicted_lighting_with_no_bulb_data_uses_base_demand() -> None: # Arrange — TFA known but no bulb counts (all zero -> treat as full incandescent). # Act result = predicted_lighting_kwh(total_floor_area_m2=100.0, cfl_count=0, led_count=0, incandescent_count=0) # Assert — base demand 9.3 * 100 = 930 kWh. assert result == pytest.approx(930.0, abs=10.0)