mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
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>
142 lines
4.7 KiB
Python
142 lines
4.7 KiB
Python
"""Tests for SAP 10.2 §13 Energy Cost Rating and §14 Environmental Impact
|
||
Rating formulas.
|
||
|
||
(7) ECF = deflator × total_cost / (TFA + 45)
|
||
(8) if ECF ≥ 3.5, SAP10 = 117 − 121 × log10(ECF)
|
||
(9) if ECF < 3.5, SAP10 = 100 − 13.95 × ECF
|
||
(10) CF = CO2_emissions / (TFA + 45)
|
||
(11) if CF ≥ 28.3, EI = 200 − 95 × log10(CF)
|
||
(12) if CF < 28.3, EI = 100 − 1.34 × CF
|
||
|
||
Energy Cost Deflator = 0.42 per Table 12 (page 191). SAP and EI ratings
|
||
are rounded to nearest integer; if the result is less than 1 it is quoted
|
||
as 1.
|
||
|
||
Reference: SAP 10.2 specification (14-03-2025) §13 + §14 (pages 38-39),
|
||
Table 12 Energy Cost Deflator (page 191). Per ADR-0010, active spec
|
||
target is SAP 10.2.
|
||
"""
|
||
|
||
import pytest
|
||
|
||
from domain.sap10_calculator.worksheet.rating import (
|
||
energy_cost_factor,
|
||
environmental_impact_rating,
|
||
sap_rating,
|
||
sap_rating_integer,
|
||
)
|
||
|
||
|
||
def test_ecf_applies_deflator_0_42_to_total_cost_per_adjusted_area() -> None:
|
||
# Arrange — Equation (7): ECF = 0.42 × total_cost / (TFA + 45). For a
|
||
# 100 m² dwelling costing £500/year:
|
||
# ECF = 0.42 × 500 / 145 ≈ 1.4483
|
||
|
||
# Act
|
||
result = energy_cost_factor(total_cost_gbp=500.0, total_floor_area_m2=100.0)
|
||
|
||
# Assert
|
||
assert result == pytest.approx(1.4483, abs=0.005)
|
||
|
||
|
||
def test_sap_rating_uses_linear_branch_when_ecf_below_3_5() -> None:
|
||
# Arrange — Equation (9): SAP = 100 − 13.95 × ECF when ECF < 3.5.
|
||
# For ECF = 1.4483 → SAP = 100 − 20.204 ≈ 79.80, rounds to 80.
|
||
|
||
# Act
|
||
result = sap_rating(ecf=1.4483)
|
||
|
||
# Assert — un-rounded SAP value for hand-verification.
|
||
assert result == pytest.approx(79.80, abs=0.05)
|
||
|
||
|
||
def test_sap_rating_uses_log_branch_when_ecf_above_3_5() -> None:
|
||
# Arrange — Equation (8): SAP = 117 − 121 × log10(ECF) for ECF ≥ 3.5.
|
||
# For a high-cost dwelling, ECF = 4.965:
|
||
# log10(4.965) ≈ 0.6960
|
||
# SAP = 117 − 121 × 0.6960 ≈ 32.78
|
||
|
||
# Act
|
||
result = sap_rating(ecf=4.965)
|
||
|
||
# Assert
|
||
assert result == pytest.approx(32.78, abs=0.05)
|
||
|
||
|
||
def test_sap_rating_log_threshold_boundary_is_continuous() -> None:
|
||
# Arrange — At ECF = 3.5 (the branch boundary) both formulas should
|
||
# yield the same value within tolerance, since the rating curve has
|
||
# been calibrated to be continuous at the transition.
|
||
# linear: 100 − 13.95 × 3.5 = 51.175
|
||
# log: 117 − 121 × log10(3.5) = 117 − 65.83 ≈ 51.17
|
||
|
||
# Act
|
||
at_threshold = sap_rating(ecf=3.5)
|
||
just_below = sap_rating(ecf=3.499)
|
||
|
||
# Assert
|
||
assert at_threshold == pytest.approx(just_below, abs=0.05)
|
||
|
||
|
||
def test_net_energy_exporter_returns_sap_above_100() -> None:
|
||
# Arrange — §13 explicitly: "The SAP rating scale has been set so that
|
||
# SAP 100 is achieved at zero-ECF. It can rise above 100 if the dwelling
|
||
# is a net exporter of energy." For ECF = -0.3:
|
||
# SAP = 100 − 13.95 × (−0.3) = 100 + 4.185 = 104.185
|
||
|
||
# Act
|
||
result = sap_rating(ecf=-0.3)
|
||
|
||
# Assert
|
||
# 100 − 13.95 × (−0.3) = 104.185 is exact arithmetic — no float drift.
|
||
assert result == pytest.approx(104.185, abs=1e-12)
|
||
assert result > 100.0
|
||
|
||
|
||
def test_sap_rating_integer_rounds_to_nearest_and_clamps_to_minimum_one() -> None:
|
||
# Arrange — §13: "The SAP rating is rounded to the nearest integer. If
|
||
# the result of the calculation is less than 1 the rating should be
|
||
# quoted as 1." So a catastrophically high-cost dwelling (e.g. ECF ≈ 10)
|
||
# → SAP = 117 − 121 × 1 = −4, clamps to 1.
|
||
# A typical case ECF = 1.4483 → 79.80 → 80.
|
||
|
||
# Act
|
||
typical = sap_rating_integer(ecf=1.4483)
|
||
catastrophic = sap_rating_integer(ecf=10.0)
|
||
|
||
# Assert
|
||
assert typical == 80
|
||
assert catastrophic == 1
|
||
|
||
|
||
def test_environmental_impact_rating_linear_branch_below_threshold() -> None:
|
||
# Arrange — Equations (10)/(12): CF = CO2/(TFA+45); when CF < 28.3,
|
||
# EI = 100 − 1.34 × CF. For a 100 m² home emitting 1500 kg CO2/yr:
|
||
# CF = 1500 / 145 ≈ 10.345
|
||
# EI = 100 − 1.34 × 10.345 ≈ 86.14
|
||
|
||
# Act
|
||
result = environmental_impact_rating(
|
||
co2_emissions_kg_per_yr=1500.0,
|
||
total_floor_area_m2=100.0,
|
||
)
|
||
|
||
# Assert
|
||
assert result == pytest.approx(86.14, abs=0.05)
|
||
|
||
|
||
def test_environmental_impact_rating_log_branch_above_threshold() -> None:
|
||
# Arrange — When CF ≥ 28.3, EI = 200 − 95 × log10(CF). For a 100 m²
|
||
# home emitting 5000 kg CO2/yr:
|
||
# CF = 5000 / 145 ≈ 34.48
|
||
# log10(34.48) ≈ 1.5377
|
||
# EI = 200 − 95 × 1.5377 ≈ 53.92
|
||
|
||
# Act
|
||
result = environmental_impact_rating(
|
||
co2_emissions_kg_per_yr=5000.0,
|
||
total_floor_area_m2=100.0,
|
||
)
|
||
|
||
# Assert
|
||
assert result == pytest.approx(53.92, abs=0.05)
|