From 504f592a27991d9a057cdc72b6aa8a17f4be7769 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 3 Jun 2026 12:50:40 +0000 Subject: [PATCH] =?UTF-8?q?feat(modelling):=20Epc.sap=5Flower=5Fbound()=20?= =?UTF-8?q?=E2=80=94=20band=20=E2=86=92=20minimum=20SAP=20(#1160)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slice 3a. The inverse of Epc.from_sap_score: the minimum SAP rating in a band (C → 69, B → 81, …), used as the Optimiser's repair target for an INCREASING_EPC goal (goal_value "C" → target SAP 69). Keeps the band-target derivation in the domain rather than re-coupling to backend.app.utils.epc_to_sap_lower_bound. 8 tests incl. round-trip through from_sap_score; pyright strict clean. Co-Authored-By: Claude Opus 4.8 --- datatypes/epc/domain/epc.py | 15 +++++++++++++++ tests/datatypes/__init__.py | 0 tests/datatypes/epc/__init__.py | 0 tests/datatypes/epc/domain/__init__.py | 0 tests/datatypes/epc/domain/test_epc.py | 26 ++++++++++++++++++++++++++ 5 files changed, 41 insertions(+) create mode 100644 tests/datatypes/__init__.py create mode 100644 tests/datatypes/epc/__init__.py create mode 100644 tests/datatypes/epc/domain/__init__.py create mode 100644 tests/datatypes/epc/domain/test_epc.py diff --git a/datatypes/epc/domain/epc.py b/datatypes/epc/domain/epc.py index b715be82..ae4fd824 100644 --- a/datatypes/epc/domain/epc.py +++ b/datatypes/epc/domain/epc.py @@ -31,3 +31,18 @@ class Epc(Enum): if score >= 21: return cls.F return cls.G + + def sap_lower_bound(self) -> int: + """The minimum SAP rating in this band — the inverse of + `from_sap_score` (A → 92, B → 81, C → 69, D → 55, E → 39, F → 21, + G → 1). Used as an optimisation target, e.g. "reach band C" → 69.""" + bounds: dict["Epc", int] = { + Epc.A: 92, + Epc.B: 81, + Epc.C: 69, + Epc.D: 55, + Epc.E: 39, + Epc.F: 21, + Epc.G: 1, + } + return bounds[self] diff --git a/tests/datatypes/__init__.py b/tests/datatypes/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/datatypes/epc/__init__.py b/tests/datatypes/epc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/datatypes/epc/domain/__init__.py b/tests/datatypes/epc/domain/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/datatypes/epc/domain/test_epc.py b/tests/datatypes/epc/domain/test_epc.py new file mode 100644 index 00000000..474c5e89 --- /dev/null +++ b/tests/datatypes/epc/domain/test_epc.py @@ -0,0 +1,26 @@ +"""Behaviour of the Epc band enum's SAP mapping — the band a SAP rating falls +in, and the minimum SAP rating of a band (the inverse, used as an optimisation +target).""" + +from __future__ import annotations + +import pytest + +from datatypes.epc.domain.epc import Epc + + +def test_sap_lower_bound_returns_the_band_floor() -> None: + # Act / Assert — the standard SAP10 band floors. + assert Epc.A.sap_lower_bound() == 92 + assert Epc.B.sap_lower_bound() == 81 + assert Epc.C.sap_lower_bound() == 69 + assert Epc.D.sap_lower_bound() == 55 + assert Epc.E.sap_lower_bound() == 39 + assert Epc.F.sap_lower_bound() == 21 + assert Epc.G.sap_lower_bound() == 1 + + +@pytest.mark.parametrize("band", list(Epc)) +def test_band_floor_round_trips_through_from_sap_score(band: Epc) -> None: + # Act / Assert — a band's floor scores back to that band. + assert Epc.from_sap_score(band.sap_lower_bound()) is band