Model/tests/domain/sap10_calculator/climate/test_appendix_u.py
Khalim Conn-Kowlessar d7d5084f90 Move sap10_calculator tests to tests/domain/sap10_calculator/ for CI
The calculator tests lived under domain/sap10_calculator/{tests,worksheet/
tests,rdsap/tests,climate/tests,validation/tests}, none of which are in
pytest.ini testpaths — so CI (which collects tests/) never ran them. Relocate
all five dirs to tests/domain/sap10_calculator/{,worksheet,rdsap,climate,
validation}, mirroring the tests/domain/property_baseline/ convention, so the
cascade-pin / golden / e2e conformance suites run in CI.

Mechanics:
- git mv preserves history (110 files).
- Flattening the trailing /tests keeps each file's depth-to-repo-root
  identical, so all 16 repo-root parents[4] fixture refs stay valid. Only
  test_pcdb_etl.py's parents[1] (→ pcdb data) and one hardcoded absolute
  golden-fixture path in test_cert_to_inputs.py needed rebasing.
- Cross-imports rewritten domain.sap10_calculator.worksheet.tests →
  tests.domain.sap10_calculator.worksheet (21 files incl. the external
  importer backend/documents_parser/tests/test_summary_pdf_mapper_chain.py).
- Golden-fixture path strings in test_summary_pdf_mapper_chain.py +
  scripts/fetch_cohort2_api_jsons.py updated to the new location (the JSONs
  moved with the rdsap tests).

load_cells / gitignored worksheet xlsx: the xlsx-pinned tests (test_dimensions
/ ventilation / water_heating) read 2026-05-19-17-18 RdSap10Worksheet.xlsx,
which is gitignored (.gitignore `*.xlsx`) and so absent in CI. _xlsx_loader.
load_cells now pytest.skip()s when the file is absent, so those tests run
locally and skip cleanly in CI instead of erroring — no new CI failures from
the move, and the gitignore policy is respected.

Verified: tests/domain/sap10_calculator + backend/documents_parser +
tests/domain/property_baseline = 2248 pass, 1 skipped; pyright resolves the
new import paths with zero import-resolution errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 16:58:00 +00:00

148 lines
4.8 KiB
Python

"""Tests for SAP 10.2 Appendix U climate-data lookups.
Reference: SAP 10.2 specification (BRE, 14-03-2025), Appendix U:
Table U1 mean external temperature, Table U2 wind speed, Table U3 mean
global solar irradiance on a horizontal plane and monthly solar declination.
22 regions (0 = UK average, 1-21 = SAP climate regions) by 12 months.
"""
import pytest
from domain.sap10_calculator.climate.appendix_u import (
external_temperature_c,
horizontal_solar_irradiance_w_per_m2,
solar_declination_deg,
wind_speed_m_per_s,
)
def test_external_temperature_uk_average_january_returns_table_u1_value() -> None:
# Arrange — SAP 10.2 Appendix U Table U1: Region 0 (UK average), January.
# Act
result = external_temperature_c(0, month=1)
# Assert
assert abs(result - 4.3) <= 0.05
def test_external_temperature_thames_july_returns_named_region_value() -> None:
# Arrange — Table U1: Region 1 (Thames), July. Hotter than the UK average
# in summer — sanity check that named regions diverge from region 0.
# Act
result = external_temperature_c(1, month=7)
# Assert
assert abs(result - 17.9) <= 0.05
def test_wind_speed_uk_average_january_returns_table_u2_value() -> None:
# Arrange — Table U2 row 0 (UK average) column Jan -> 5.1 m/s. Used by the
# SAP infiltration calc (worksheet lines 9-16).
# Act
result = wind_speed_m_per_s(0, month=1)
# Assert
assert abs(result - 5.1) <= 0.05
def test_horizontal_solar_irradiance_uk_average_july_returns_table_u3_value() -> None:
# Arrange — Table U3 row 0 (UK average) column Jul -> 189 W/m². Peak month
# for global horizontal irradiance in the UK.
# Act
result = horizontal_solar_irradiance_w_per_m2(0, month=7)
# Assert
assert abs(result - 189.0) <= 0.5
def test_horizontal_solar_irradiance_southern_england_brighter_than_shetland() -> None:
# Arrange — Table U3 row 3 (Southern England) Jun -> 235, row 20 (Shetland)
# Jun -> 190. Higher-latitude regions get less June irradiance.
# Act
south = horizontal_solar_irradiance_w_per_m2(3, month=6)
shetland = horizontal_solar_irradiance_w_per_m2(20, month=6)
# Assert
assert abs(south - 235.0) <= 0.5
assert abs(shetland - 190.0) <= 0.5
assert south > shetland
def test_solar_declination_winter_solstice_returns_table_u3_value() -> None:
# Arrange — Table U3 footer "Solar declination" row: December = -23.0°.
# Declination is region-independent (function only of month).
# Act
result = solar_declination_deg(month=12)
# Assert
assert abs(result - -23.0) <= 0.05
def test_solar_declination_summer_solstice_positive_value() -> None:
# Arrange — Table U3 footer: June declination = +23.1°.
# Act
result = solar_declination_deg(month=6)
# Assert
assert abs(result - 23.1) <= 0.05
def test_external_temperature_out_of_range_region_raises_value_error() -> None:
# Arrange — there are 22 regions (0-21); 22 is the first invalid index.
# The callers (postcode resolver in particular) should fail fast on a
# bad region rather than silently aliasing to row 0 or wrapping around.
# Act / Assert
with pytest.raises(ValueError, match="region"):
external_temperature_c(22, month=1)
with pytest.raises(ValueError, match="region"):
external_temperature_c(-1, month=1)
def test_region_21_northern_ireland_returns_table_u1_value() -> None:
# Arrange — region 21 (Northern Ireland) is the last valid region. Catches
# off-by-one errors in the region-bound check (would otherwise reject 21).
# Table U1 row 21 July -> 15.0 °C.
# Act
result = external_temperature_c(21, month=7)
# Assert
assert abs(result - 15.0) <= 0.05
def test_out_of_range_month_raises_value_error_on_every_lookup() -> None:
# Arrange — months are 1..12. Month 0 and month 13 must reject across
# all four climate lookups, including the region-independent declination.
# Act / Assert
with pytest.raises(ValueError, match="month"):
external_temperature_c(0, month=0)
with pytest.raises(ValueError, match="month"):
wind_speed_m_per_s(0, month=13)
with pytest.raises(ValueError, match="month"):
horizontal_solar_irradiance_w_per_m2(0, month=0)
with pytest.raises(ValueError, match="month"):
solar_declination_deg(month=13)
def test_wind_speed_shetland_january_higher_than_thames() -> None:
# Arrange — Table U2 row 20 (Shetland), the windiest UK region by a wide
# margin: 9.5 m/s in January vs Thames 4.2 m/s. Sanity check the table is
# populated for the upper region indices, not silently aliasing to row 0.
# Act
shetland = wind_speed_m_per_s(20, month=1)
thames = wind_speed_m_per_s(1, month=1)
# Assert
assert abs(shetland - 9.5) <= 0.05
assert abs(thames - 4.2) <= 0.05
assert shetland > thames