mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
feat(modelling): solid-wall generator offers EWI+IWI for solid brick
Slice 2a. New recommend_solid_wall emits one Main-wall Recommendation carrying External + Internal wall-insulation Options for an uninsulated (wall_insulation _type=4) solid-brick (wall_construction=3) main wall, each priced at the heat- loss wall area. Cascade pin: the generator's EWI and IWI Options reproduce their respective re-lodged afters at abs(diff) <= 1e-4. Detection keys on wall_construction code, not description (ADR-0019 note corrected): the Elmhurst ingestion path leaves walls[].description empty, so the code is the only cross-path signal; codes 1-5 are consistent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
da69dc27fd
commit
1c7997c471
3 changed files with 148 additions and 1 deletions
|
|
@ -4,7 +4,7 @@ The solid-wall Recommendation Generator must decide, per Property, which wall-in
|
|||
|
||||
## Decision
|
||||
|
||||
**By construction (detected from the wall *description* string, not the numeric `wall_construction` code — see Consequences):**
|
||||
**By construction** (keyed on the `wall_construction` code, which is consistent across the API and Elmhurst paths for codes 1-5; the wall *description* is empty on the Elmhurst ingestion path so it can't be the primary signal — it's a fallback for the ambiguous higher codes (system-built 6-vs-8, cob 7) and for refining the as-built trigger on the API path):
|
||||
|
||||
| Construction | Cavity fill | IWI | EWI |
|
||||
|---|---|---|---|
|
||||
|
|
|
|||
113
domain/modelling/generators/solid_wall_recommendation.py
Normal file
113
domain/modelling/generators/solid_wall_recommendation.py
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
"""The solid-wall Recommendation Generator (IWI / EWI).
|
||||
|
||||
Detects an uninsulated *solid* (non-cavity) main wall and emits one "Main wall"
|
||||
Recommendation carrying the constructable solid-wall insulation Options —
|
||||
External (EWI) and/or Internal (IWI) — as mutually-exclusive Measure Options
|
||||
the Optimiser chooses between (ADR-0019). A cavity wall is handled by
|
||||
`recommend_cavity_wall`, never here.
|
||||
|
||||
Wall material is keyed on the RdSAP `wall_construction` code (codes 1-5 are
|
||||
consistent across the API and Elmhurst ingestion paths; the wall *description*
|
||||
is empty on the Elmhurst path, so it can't be the primary signal — it is a
|
||||
fallback for the ambiguous higher codes, handled in a later slice). The trigger
|
||||
is the as-built/uninsulated `wall_insulation_type`, mirroring the cavity
|
||||
generator. Detection + pricing only; impact is produced later by scoring
|
||||
(ADR-0016).
|
||||
"""
|
||||
|
||||
from typing import Final, Optional
|
||||
|
||||
from datatypes.epc.domain.epc_property_data import (
|
||||
BuildingPartIdentifier,
|
||||
EpcPropertyData,
|
||||
)
|
||||
from domain.building_geometry import gross_heat_loss_wall_area
|
||||
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"
|
||||
|
||||
# RdSAP `wall_construction` code for solid brick (consistent across paths).
|
||||
_WALL_SOLID_BRICK: Final[int] = 3
|
||||
# `wall_insulation_type`: 4 = as-built / assumed (uninsulated) — the trigger.
|
||||
_WALL_AS_BUILT: Final[int] = 4
|
||||
# `wall_insulation_type` the overlay lodges: 1 = external, 3 = internal.
|
||||
_WALL_INSULATION_EXTERNAL: Final[int] = 1
|
||||
_WALL_INSULATION_INTERNAL: Final[int] = 3
|
||||
# Recommended solid-wall insulation depth (mm); the calculator's λ default
|
||||
# (0.04 W/m·K) matches Elmhurst's lodged thermal conductivity.
|
||||
_SOLID_WALL_INSULATION_MM: Final[int] = 100
|
||||
|
||||
# Which solid-wall Options each construction can take (ADR-0019). Solid brick
|
||||
# takes both; timber-frame (IWI only), system-built, and the breathable
|
||||
# cob/stone exclusions land in later slices.
|
||||
_CONSTRUCTABLE_OPTIONS: Final[dict[int, tuple[str, ...]]] = {
|
||||
_WALL_SOLID_BRICK: (_EXTERNAL_MEASURE_TYPE, _INTERNAL_MEASURE_TYPE),
|
||||
}
|
||||
|
||||
_INSULATION_TYPE: Final[dict[str, int]] = {
|
||||
_EXTERNAL_MEASURE_TYPE: _WALL_INSULATION_EXTERNAL,
|
||||
_INTERNAL_MEASURE_TYPE: _WALL_INSULATION_INTERNAL,
|
||||
}
|
||||
|
||||
_DESCRIPTION: Final[dict[str, str]] = {
|
||||
_EXTERNAL_MEASURE_TYPE: "External wall insulation (insulate the wall externally)",
|
||||
_INTERNAL_MEASURE_TYPE: "Internal wall insulation (insulate the wall internally)",
|
||||
}
|
||||
|
||||
|
||||
def _solid_wall_option(
|
||||
epc: EpcPropertyData, products: ProductRepository, measure_type: str
|
||||
) -> MeasureOption:
|
||||
"""Build one solid-wall Measure Option: its insulation overlay (100 mm at the
|
||||
External/Internal `wall_insulation_type`) priced at the heat-loss wall area."""
|
||||
product = products.get(measure_type)
|
||||
wall_area: float = gross_heat_loss_wall_area(epc, BuildingPartIdentifier.MAIN)
|
||||
cost = Cost(
|
||||
total=wall_area * product.unit_cost_per_m2,
|
||||
contingency_rate=product.contingency_rate,
|
||||
)
|
||||
return MeasureOption(
|
||||
measure_type=measure_type,
|
||||
description=_DESCRIPTION[measure_type],
|
||||
overlay=EpcSimulation(
|
||||
building_parts={
|
||||
BuildingPartIdentifier.MAIN: BuildingPartOverlay(
|
||||
wall_insulation_type=_INSULATION_TYPE[measure_type],
|
||||
wall_insulation_thickness=_SOLID_WALL_INSULATION_MM,
|
||||
)
|
||||
}
|
||||
),
|
||||
cost=cost,
|
||||
material_id=product.id,
|
||||
)
|
||||
|
||||
|
||||
def recommend_solid_wall(
|
||||
epc: EpcPropertyData, products: ProductRepository
|
||||
) -> Optional[Recommendation]:
|
||||
"""Return a solid-wall insulation Recommendation for an uninsulated, suitable
|
||||
main wall — its constructable EWI/IWI Options — else None."""
|
||||
main = next(
|
||||
part
|
||||
for part in epc.sap_building_parts
|
||||
if part.identifier is BuildingPartIdentifier.MAIN
|
||||
)
|
||||
|
||||
if main.wall_insulation_type != _WALL_AS_BUILT:
|
||||
return None
|
||||
|
||||
construction: object = main.wall_construction
|
||||
if not isinstance(construction, int):
|
||||
return None # a free-text site-notes construction is not a code we key on
|
||||
measure_types = _CONSTRUCTABLE_OPTIONS.get(construction)
|
||||
if not measure_types:
|
||||
return None
|
||||
|
||||
options = tuple(
|
||||
_solid_wall_option(epc, products, measure_type)
|
||||
for measure_type in measure_types
|
||||
)
|
||||
return Recommendation(surface="Main wall", options=options)
|
||||
|
|
@ -27,6 +27,10 @@ from domain.modelling.generators.floor_recommendation import recommend_floor_ins
|
|||
from domain.modelling.generators.roof_recommendation import recommend_loft_insulation
|
||||
from domain.modelling.simulation import BuildingPartOverlay, EpcSimulation
|
||||
from domain.modelling.generators.wall_recommendation import recommend_cavity_wall
|
||||
from domain.modelling.generators.solid_wall_recommendation import (
|
||||
recommend_solid_wall,
|
||||
)
|
||||
from domain.modelling.recommendation import MeasureOption
|
||||
from domain.sap10_calculator.calculator import Sap10Calculator, SapResult
|
||||
from repositories.product.product_repository import ProductRepository
|
||||
from tests.domain.modelling._elmhurst_recommendation import (
|
||||
|
|
@ -137,6 +141,36 @@ def test_solid_brick_iwi_overlay_reproduces_the_relodged_after() -> None:
|
|||
_assert_overlay_reproduces_after(before, after, overlay)
|
||||
|
||||
|
||||
def test_solid_brick_generator_offers_ewi_and_iwi_each_pinning_its_after() -> None:
|
||||
# Arrange — one uninsulated solid-brick before, two re-lodged afters.
|
||||
before: EpcPropertyData = parse_recommendation_summary(
|
||||
"solid_brick_ewi_001431_before.pdf"
|
||||
)
|
||||
ewi_after: EpcPropertyData = parse_recommendation_summary(
|
||||
"solid_brick_ewi_001431_after.pdf"
|
||||
)
|
||||
iwi_after: EpcPropertyData = parse_recommendation_summary(
|
||||
"solid_brick_iwi_001431_after.pdf"
|
||||
)
|
||||
|
||||
# Act — solid brick is suitable for both, unrestricted.
|
||||
recommendation: Recommendation | None = recommend_solid_wall(before, _AnyProduct())
|
||||
assert recommendation is not None
|
||||
options: dict[str, MeasureOption] = {
|
||||
option.measure_type: option for option in recommendation.options
|
||||
}
|
||||
|
||||
# Assert — both Options offered, and each Option's overlay reproduces its
|
||||
# own re-lodged after at the pin tolerance.
|
||||
assert set(options) == {"external_wall_insulation", "internal_wall_insulation"}
|
||||
_assert_overlay_reproduces_after(
|
||||
before, ewi_after, options["external_wall_insulation"].overlay
|
||||
)
|
||||
_assert_overlay_reproduces_after(
|
||||
before, iwi_after, options["internal_wall_insulation"].overlay
|
||||
)
|
||||
|
||||
|
||||
def test_loft_overlay_reproduces_the_relodged_after() -> None:
|
||||
# Arrange
|
||||
before: EpcPropertyData = parse_recommendation_summary(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue