mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
feat(modelling): MeasureType StrEnum as the canonical measure vocabulary
Introduce domain/modelling/measure_type.py — a StrEnum with one member per modelled measure (the 15 the generators emit). A StrEnum so each member *is* its string value: it persists straight into the `recommendation` varchar column, is the optimiser's group-by key, and compares equal to the catalogue / EPC strings — so it replaces the per-generator string constants with no persistence or optimiser change. Repoint every generator's measure-type constant/literal to a MeasureType member (wall, solid_wall, roof, floor, glazing, lighting, ventilation, heating, solar). Field annotations stay `str` for now; tightening them to MeasureType is the next slice. This is the enum the historical engine deferred (engine.py:970 "TODO - formalise property measure types into an enum") and the vocabulary the forthcoming `considered_measures` allowlist will speak (mirroring the legacy `inclusions`). Suite green: tests/domain/modelling + orchestration + harness 253 pass + 3 xfail; pyright clean on the enum + generators. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
1b4806f8e4
commit
d58ac60d29
11 changed files with 111 additions and 15 deletions
|
|
@ -15,6 +15,7 @@ from datatypes.epc.domain.epc_property_data import (
|
|||
SapBuildingPart,
|
||||
)
|
||||
from domain.building_geometry import ground_floor_area
|
||||
from domain.modelling.measure_type import MeasureType
|
||||
from domain.modelling.recommendation import Cost, MeasureOption, Recommendation
|
||||
from domain.modelling.simulation import BuildingPartOverlay, EpcSimulation
|
||||
from repositories.product.product_repository import ProductRepository
|
||||
|
|
@ -44,9 +45,9 @@ def _floor_measure_type(construction_type: Optional[str]) -> Optional[str]:
|
|||
None when the construction is not a treatable suspended/solid floor."""
|
||||
text = (construction_type or "").lower()
|
||||
if "suspended" in text:
|
||||
return "suspended_floor_insulation"
|
||||
return MeasureType.SUSPENDED_FLOOR_INSULATION
|
||||
if "solid" in text:
|
||||
return "solid_floor_insulation"
|
||||
return MeasureType.SOLID_FLOOR_INSULATION
|
||||
return None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ from typing import Final, Optional
|
|||
|
||||
from datatypes.epc.domain.epc_property_data import EpcPropertyData
|
||||
from domain.geospatial.planning_restrictions import PlanningRestrictions
|
||||
from domain.modelling.measure_type import MeasureType
|
||||
from domain.modelling.recommendation import Cost, MeasureOption, Recommendation
|
||||
from domain.modelling.simulation import EpcSimulation, WindowOverlay
|
||||
from repositories.product.product_repository import ProductRepository
|
||||
|
|
@ -52,7 +53,7 @@ class _GlazingTarget:
|
|||
# Unrestricted: replace the units with double glazing (gt=5 "Double post 2022";
|
||||
# U 4.80→1.40, g 0.85→0.72).
|
||||
_DOUBLE: Final[_GlazingTarget] = _GlazingTarget(
|
||||
measure_type="double_glazing",
|
||||
measure_type=MeasureType.DOUBLE_GLAZING,
|
||||
description="Replace the single-glazed windows with double glazing",
|
||||
glazing_type=5,
|
||||
u_value=1.40,
|
||||
|
|
@ -64,7 +65,7 @@ _DOUBLE: Final[_GlazingTarget] = _GlazingTarget(
|
|||
# solar gain). The external units can't be replaced on a protected/over-looked
|
||||
# building, so this is the planning-picked Measure.
|
||||
_SECONDARY: Final[_GlazingTarget] = _GlazingTarget(
|
||||
measure_type="secondary_glazing",
|
||||
measure_type=MeasureType.SECONDARY_GLAZING,
|
||||
description="Fit secondary glazing to the single-glazed windows",
|
||||
glazing_type=11,
|
||||
u_value=2.90,
|
||||
|
|
|
|||
|
|
@ -18,14 +18,15 @@ from datatypes.epc.domain.epc_property_data import EpcPropertyData, MainHeatingD
|
|||
from datatypes.epc.domain.field_mappings import PROPERTY_TYPE_LOOKUP
|
||||
from domain.geospatial.planning_restrictions import PlanningRestrictions
|
||||
from domain.modelling.products import AshpCostInputs, AshpExistingSystem, Products
|
||||
from domain.modelling.measure_type import MeasureType
|
||||
from domain.modelling.recommendation import Cost, MeasureOption, Recommendation
|
||||
from domain.modelling.simulation import EpcSimulation, HeatingOverlay
|
||||
from repositories.product.product_repository import ProductRepository
|
||||
|
||||
_HEATING_SURFACE = "Heating & Hot Water"
|
||||
|
||||
_HHR_STORAGE_MEASURE_TYPE = "high_heat_retention_storage_heaters"
|
||||
_ASHP_MEASURE_TYPE = "air_source_heat_pump"
|
||||
_HHR_STORAGE_MEASURE_TYPE = MeasureType.HIGH_HEAT_RETENTION_STORAGE_HEATERS
|
||||
_ASHP_MEASURE_TYPE = MeasureType.AIR_SOURCE_HEAT_PUMP
|
||||
|
||||
# Electricity main-fuel code (Elmhurst → SAP10 Table 12).
|
||||
_ELECTRICITY_FUEL = 30
|
||||
|
|
|
|||
|
|
@ -18,11 +18,12 @@ scoring (ADR-0016).
|
|||
from typing import Final, Optional
|
||||
|
||||
from datatypes.epc.domain.epc_property_data import EpcPropertyData
|
||||
from domain.modelling.measure_type import MeasureType
|
||||
from domain.modelling.recommendation import Cost, MeasureOption, Recommendation
|
||||
from domain.modelling.simulation import EpcSimulation, LightingOverlay
|
||||
from repositories.product.product_repository import ProductRepository
|
||||
|
||||
_LIGHTING_MEASURE_TYPE: Final[str] = "low_energy_lighting"
|
||||
_LIGHTING_MEASURE_TYPE: Final[MeasureType] = MeasureType.LOW_ENERGY_LIGHTING
|
||||
|
||||
|
||||
def recommend_lighting(
|
||||
|
|
|
|||
|
|
@ -17,12 +17,13 @@ from datatypes.epc.domain.epc_property_data import (
|
|||
EpcPropertyData,
|
||||
)
|
||||
from domain.building_geometry import roof_area
|
||||
from domain.modelling.measure_type import MeasureType
|
||||
from domain.modelling.recommendation import Cost, MeasureOption, Recommendation
|
||||
from domain.modelling.simulation import BuildingPartOverlay, EpcSimulation
|
||||
from repositories.product.product_repository import ProductRepository
|
||||
|
||||
_LOFT_MEASURE_TYPE = "loft_insulation"
|
||||
_SLOPING_CEILING_MEASURE_TYPE = "sloping_ceiling_insulation"
|
||||
_LOFT_MEASURE_TYPE = MeasureType.LOFT_INSULATION
|
||||
_SLOPING_CEILING_MEASURE_TYPE = MeasureType.SLOPING_CEILING_INSULATION
|
||||
# RdSAP 10 Table 16: 0 mm lodged roof insulation is an uninsulated loft. The
|
||||
# Elmhurst mapper resolves "As Built" to 0 for pitched/sloping/loft roofs.
|
||||
_ROOF_UNINSULATED_MM = 0
|
||||
|
|
@ -32,7 +33,7 @@ _ROOF_UNINSULATED_MM = 0
|
|||
_RECOMMENDED_LOFT_THICKNESS_MM = 300
|
||||
# Recommended sloping-ceiling depth (mm); Elmhurst re-lodges 100 mm (ADR-0021).
|
||||
_RECOMMENDED_SLOPING_CEILING_THICKNESS_MM = 100
|
||||
_FLAT_ROOF_MEASURE_TYPE = "flat_roof_insulation"
|
||||
_FLAT_ROOF_MEASURE_TYPE = MeasureType.FLAT_ROOF_INSULATION
|
||||
# Recommended flat-roof depth (mm); Elmhurst re-lodges 200 mm (ADR-0021).
|
||||
_RECOMMENDED_FLAT_ROOF_THICKNESS_MM = 200
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ from datatypes.epc.domain.epc_property_data import (
|
|||
from datatypes.epc.domain.field_mappings import PROPERTY_TYPE_LOOKUP
|
||||
from domain.geospatial.planning_restrictions import PlanningRestrictions
|
||||
from domain.modelling.products import Products, SolarCostInputs
|
||||
from domain.modelling.measure_type import MeasureType
|
||||
from domain.modelling.recommendation import Cost, MeasureOption, Recommendation
|
||||
from domain.modelling.simulation import EpcSimulation, SolarOverlay
|
||||
from domain.modelling.solar_potential import (
|
||||
|
|
@ -37,7 +38,7 @@ from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
|||
from repositories.product.product_repository import ProductRepository
|
||||
|
||||
_SOLAR_SURFACE = "Solar PV"
|
||||
_SOLAR_MEASURE_TYPE = "solar_pv"
|
||||
_SOLAR_MEASURE_TYPE = MeasureType.SOLAR_PV
|
||||
|
||||
# The fixed, representative battery capacity for the with-battery variant
|
||||
# (ADR-0026) — a flagged estimate (see the rate sheet), 5 kWh.
|
||||
|
|
|
|||
|
|
@ -24,12 +24,13 @@ from datatypes.epc.domain.epc_property_data import (
|
|||
from datatypes.epc.domain.field_mappings import PROPERTY_TYPE_LOOKUP
|
||||
from domain.building_geometry import gross_heat_loss_wall_area
|
||||
from domain.geospatial.planning_restrictions import PlanningRestrictions
|
||||
from domain.modelling.measure_type import MeasureType
|
||||
from domain.modelling.recommendation import Cost, MeasureOption, Recommendation
|
||||
from domain.modelling.simulation import BuildingPartOverlay, EpcSimulation
|
||||
from repositories.product.product_repository import ProductRepository
|
||||
|
||||
_EXTERNAL_MEASURE_TYPE: Final[str] = "external_wall_insulation"
|
||||
_INTERNAL_MEASURE_TYPE: Final[str] = "internal_wall_insulation"
|
||||
_EXTERNAL_MEASURE_TYPE: Final[MeasureType] = MeasureType.EXTERNAL_WALL_INSULATION
|
||||
_INTERNAL_MEASURE_TYPE: Final[MeasureType] = MeasureType.INTERNAL_WALL_INSULATION
|
||||
|
||||
# RdSAP `wall_construction` codes (consistent across paths for 1-5).
|
||||
_WALL_SOLID_BRICK: Final[int] = 3
|
||||
|
|
|
|||
|
|
@ -16,11 +16,12 @@ mirrors legacy ``Property.has_ventilation``.
|
|||
from typing import Optional
|
||||
|
||||
from datatypes.epc.domain.epc_property_data import EpcPropertyData
|
||||
from domain.modelling.measure_type import MeasureType
|
||||
from domain.modelling.recommendation import Cost, MeasureOption, Recommendation
|
||||
from domain.modelling.simulation import EpcSimulation, VentilationOverlay
|
||||
from repositories.product.product_repository import ProductRepository
|
||||
|
||||
_VENTILATION_MEASURE_TYPE = "mechanical_ventilation"
|
||||
_VENTILATION_MEASURE_TYPE = MeasureType.MECHANICAL_VENTILATION
|
||||
|
||||
# The SAP10.2 §2 mechanical-ventilation kind installed: decentralised MEV
|
||||
# ("mechanical extract, decentralised (MEV dc)" → MechanicalVentilationKind
|
||||
|
|
|
|||
|
|
@ -12,11 +12,12 @@ from datatypes.epc.domain.epc_property_data import (
|
|||
EpcPropertyData,
|
||||
)
|
||||
from domain.building_geometry import gross_heat_loss_wall_area
|
||||
from domain.modelling.measure_type import MeasureType
|
||||
from domain.modelling.recommendation import Cost, MeasureOption, Recommendation
|
||||
from domain.modelling.simulation import BuildingPartOverlay, EpcSimulation
|
||||
from repositories.product.product_repository import ProductRepository
|
||||
|
||||
_CAVITY_MEASURE_TYPE = "cavity_wall_insulation"
|
||||
_CAVITY_MEASURE_TYPE = MeasureType.CAVITY_WALL_INSULATION
|
||||
|
||||
# RdSAP 10 Table 5 wall_construction: 4 = "Cavity". Table 6
|
||||
# wall_insulation_type: 4 = "as-built / assumed" (uninsulated), 2 = "Filled
|
||||
|
|
|
|||
35
domain/modelling/measure_type.py
Normal file
35
domain/modelling/measure_type.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
"""MeasureType — the canonical vocabulary of the measures the Modelling stage
|
||||
models.
|
||||
|
||||
One member per Recommendation Generator option. A ``StrEnum`` so each member
|
||||
*is* its string value: it persists straight into the ``recommendation`` varchar
|
||||
column, is the optimiser's group-by key, and compares equal to the raw strings
|
||||
the catalogue and EPC carry — so it can replace the per-generator string
|
||||
constants as the single source of truth without a persistence or optimiser
|
||||
change. It is also the vocabulary the ``considered_measures`` allowlist speaks
|
||||
(mirroring the legacy engine's ``inclusions``).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class MeasureType(StrEnum):
|
||||
"""A measure the Modelling stage can recommend (CONTEXT.md)."""
|
||||
|
||||
CAVITY_WALL_INSULATION = "cavity_wall_insulation"
|
||||
EXTERNAL_WALL_INSULATION = "external_wall_insulation"
|
||||
INTERNAL_WALL_INSULATION = "internal_wall_insulation"
|
||||
LOFT_INSULATION = "loft_insulation"
|
||||
SLOPING_CEILING_INSULATION = "sloping_ceiling_insulation"
|
||||
FLAT_ROOF_INSULATION = "flat_roof_insulation"
|
||||
SUSPENDED_FLOOR_INSULATION = "suspended_floor_insulation"
|
||||
SOLID_FLOOR_INSULATION = "solid_floor_insulation"
|
||||
DOUBLE_GLAZING = "double_glazing"
|
||||
SECONDARY_GLAZING = "secondary_glazing"
|
||||
LOW_ENERGY_LIGHTING = "low_energy_lighting"
|
||||
MECHANICAL_VENTILATION = "mechanical_ventilation"
|
||||
HIGH_HEAT_RETENTION_STORAGE_HEATERS = "high_heat_retention_storage_heaters"
|
||||
AIR_SOURCE_HEAT_PUMP = "air_source_heat_pump"
|
||||
SOLAR_PV = "solar_pv"
|
||||
52
tests/domain/modelling/test_measure_type.py
Normal file
52
tests/domain/modelling/test_measure_type.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
"""Slice 1 — `MeasureType`, the canonical enum of the measures the Modelling
|
||||
stage actually models.
|
||||
|
||||
A `StrEnum` so each member *is* its persisted/optimiser string value (e.g.
|
||||
`MeasureType.SOLAR_PV == "solar_pv"`), letting it flow through the `recommendation`
|
||||
varchar column and the optimiser's group-by-type key unchanged. Replaces the
|
||||
per-generator string constants as the single source of truth, and is the
|
||||
vocabulary the `considered_measures` allowlist speaks.
|
||||
"""
|
||||
|
||||
from domain.modelling.measure_type import MeasureType
|
||||
|
||||
# The full set of measures the generators emit today (one per Generator option).
|
||||
_EXPECTED_VALUES = {
|
||||
"cavity_wall_insulation",
|
||||
"external_wall_insulation",
|
||||
"internal_wall_insulation",
|
||||
"loft_insulation",
|
||||
"sloping_ceiling_insulation",
|
||||
"flat_roof_insulation",
|
||||
"suspended_floor_insulation",
|
||||
"solid_floor_insulation",
|
||||
"double_glazing",
|
||||
"secondary_glazing",
|
||||
"low_energy_lighting",
|
||||
"mechanical_ventilation",
|
||||
"high_heat_retention_storage_heaters",
|
||||
"air_source_heat_pump",
|
||||
"solar_pv",
|
||||
}
|
||||
|
||||
|
||||
def test_members_cover_exactly_the_modelled_measures() -> None:
|
||||
# Arrange / Act
|
||||
values = {member.value for member in MeasureType}
|
||||
|
||||
# Assert
|
||||
assert values == _EXPECTED_VALUES
|
||||
|
||||
|
||||
def test_member_is_its_string_value() -> None:
|
||||
# Arrange / Act / Assert — a StrEnum member compares and persists as its str.
|
||||
assert MeasureType.SOLAR_PV == "solar_pv"
|
||||
assert MeasureType.HIGH_HEAT_RETENTION_STORAGE_HEATERS == (
|
||||
"high_heat_retention_storage_heaters"
|
||||
)
|
||||
assert isinstance(MeasureType.SOLAR_PV, str)
|
||||
|
||||
|
||||
def test_round_trips_through_its_value() -> None:
|
||||
# Arrange / Act / Assert — a raw catalogue/DB string maps back to the member.
|
||||
assert MeasureType("cavity_wall_insulation") is MeasureType.CAVITY_WALL_INSULATION
|
||||
Loading…
Add table
Reference in a new issue