mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
feat(modelling): detect single-glazing code 15 + glazing before/after pins
With the mapper now in main, cert 001431 parses: it lodges four single-glazed
windows — codes 1 ("Single") and 15 ("single glazing, known data", a single
pane with manufacturer U/g). The generator only detected code 1, so it missed
two panes. Detect {1, 15}; set the secondary target to code 11 ("Secondary
glazing - Normal emissivity", what the cert re-lodges; score-neutral vs 7 but
exact).
A deterministic green pin proves the overlay reproduces the after's 14 windows
exactly. The full-SAP before->after pins are xfail(strict) tripwires: the
overlay nails the windows, but the measure also re-lodges percent_draughtproofed
84->100 (sealed units draught-proof the replaced openings) plus a ~0.4 SAP
fabric residual the overlay doesn't model yet — a glazing-measure coupling to
close later.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
2c36a8e1d6
commit
07dbaa5361
3 changed files with 110 additions and 12 deletions
|
|
@ -27,10 +27,12 @@ from domain.modelling.recommendation import Cost, MeasureOption, Recommendation
|
|||
from domain.modelling.simulation import EpcSimulation, WindowOverlay
|
||||
from repositories.product.product_repository import ProductRepository
|
||||
|
||||
# SAP10.2 Table U2 code 1 = single glazing — the only windows this generator
|
||||
# upgrades (the Elmhurst mapper sends "Single"/"Single glazing" → 1 and the API
|
||||
# passes int 1, so the trigger is path-consistent).
|
||||
_SINGLE_GLAZED: Final[int] = 1
|
||||
# Single-glazing codes — the only windows this generator upgrades. Code 1 is
|
||||
# bare "Single"/"Single glazing"; code 15 is "single glazing, known data" (a
|
||||
# single pane with manufacturer U/g lodged — same g_L=0.90 as code 1, per
|
||||
# RdSAP-21). Both are single-glazed and must be detected, or a cert that lodges
|
||||
# manufacturer data on its single panes (e.g. 001431 windows 12-13) is missed.
|
||||
_SINGLE_GLAZED_CODES: Final[frozenset[int]] = frozenset({1, 15})
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
@ -57,13 +59,14 @@ _DOUBLE: Final[_GlazingTarget] = _GlazingTarget(
|
|||
solar_transmittance=0.72,
|
||||
)
|
||||
# Protected (conservation/listed/heritage): fit an internal secondary pane
|
||||
# (gt=7 "Secondary glazing"; U→2.90, g unchanged at 0.85 — the existing outer
|
||||
# single pane still drives solar gain). The external units can't be replaced on
|
||||
# a protected/over-looked building, so this is the planning-picked Measure.
|
||||
# (gt=11 "Secondary glazing - Normal emissivity", what cert 001431 re-lodges;
|
||||
# U→2.90, g unchanged at 0.85 — the existing outer single pane still drives
|
||||
# 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",
|
||||
description="Fit secondary glazing to the single-glazed windows",
|
||||
glazing_type=7,
|
||||
glazing_type=11,
|
||||
u_value=2.90,
|
||||
solar_transmittance=0.85,
|
||||
)
|
||||
|
|
@ -81,7 +84,7 @@ def recommend_glazing(
|
|||
single_indices = tuple(
|
||||
index
|
||||
for index, window in enumerate(epc.sap_windows)
|
||||
if window.glazing_type == _SINGLE_GLAZED
|
||||
if window.glazing_type in _SINGLE_GLAZED_CODES
|
||||
)
|
||||
if not single_indices:
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ from __future__ import annotations
|
|||
from dataclasses import replace
|
||||
from typing import Final
|
||||
|
||||
import pytest
|
||||
|
||||
from datatypes.epc.domain.epc_property_data import (
|
||||
BuildingPartIdentifier,
|
||||
EpcPropertyData,
|
||||
|
|
@ -34,6 +36,8 @@ from domain.geospatial.planning_restrictions import PlanningRestrictions
|
|||
from domain.modelling.generators.solid_wall_recommendation import (
|
||||
recommend_solid_wall,
|
||||
)
|
||||
from domain.modelling.generators.glazing_recommendation import recommend_glazing
|
||||
from domain.modelling.scoring.overlay_applicator import apply_simulations
|
||||
from domain.modelling.recommendation import MeasureOption
|
||||
from domain.sap10_calculator.calculator import Sap10Calculator, SapResult
|
||||
from repositories.product.product_repository import ProductRepository
|
||||
|
|
@ -442,3 +446,94 @@ def test_suspended_floor_overlay_reproduces_the_relodged_after() -> None:
|
|||
_assert_overlay_reproduces_after(
|
||||
before, after, recommendation.options[0].overlay
|
||||
)
|
||||
|
||||
|
||||
def test_double_glazing_overlay_reproduces_the_relodged_after_windows() -> None:
|
||||
# The full-SAP pin below is xfail (draught-proofing coupling), but the
|
||||
# overlay's actual job — turning every single-glazed window into the
|
||||
# relodged spec — is deterministic and must hold exactly: it proves the
|
||||
# generator detects BOTH single-glazing codes (1 and 15) on the real cert.
|
||||
# Arrange
|
||||
before: EpcPropertyData = parse_recommendation_summary(
|
||||
"double_glazing_001431_before.pdf"
|
||||
)
|
||||
after: EpcPropertyData = parse_recommendation_summary(
|
||||
"double_glazing_001431_after.pdf"
|
||||
)
|
||||
recommendation: Recommendation | None = recommend_glazing(before, _AnyProduct())
|
||||
assert recommendation is not None
|
||||
|
||||
# Act — apply the overlay to the parsed before.
|
||||
applied: EpcPropertyData = apply_simulations(
|
||||
before, [recommendation.options[0].overlay]
|
||||
)
|
||||
|
||||
# Assert — every window's glazing_type + lodged U/g matches the after.
|
||||
def _window_spec(epc: EpcPropertyData) -> list[tuple[object, object, object]]:
|
||||
specs: list[tuple[object, object, object]] = []
|
||||
for window in epc.sap_windows:
|
||||
details = window.window_transmission_details
|
||||
specs.append(
|
||||
(
|
||||
window.glazing_type,
|
||||
details.u_value if details is not None else None,
|
||||
details.solar_transmittance if details is not None else None,
|
||||
)
|
||||
)
|
||||
return specs
|
||||
|
||||
assert _window_spec(applied) == _window_spec(after)
|
||||
|
||||
|
||||
_GLAZING_DRAUGHT_COUPLING_REASON: Final[str] = (
|
||||
"Blocked on the glazing measure's draught-proofing coupling. The window "
|
||||
"U/g overlay reproduces the after's 14 windows EXACTLY (all four single-"
|
||||
"glazed panes — codes 1 and 15 — become the relodged double/secondary "
|
||||
"spec). The residual ~0.7 SAP is a secondary effect the overlay does not "
|
||||
"model: replacing the single-glazed (lodged draught_proofed=No) windows "
|
||||
"with sealed units re-lodges percent_draughtproofed 84->100 (~0.3 SAP) and "
|
||||
"lowers fabric heat loss by ~+150 kWh space heating (~0.4 SAP) not yet "
|
||||
"isolated. Flips green once the glazing overlay propagates draught-proofing "
|
||||
"(and the residual fabric coupling is modelled)."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.xfail(strict=True, reason=_GLAZING_DRAUGHT_COUPLING_REASON)
|
||||
def test_double_glazing_overlay_reproduces_the_relodged_after() -> None:
|
||||
# Arrange — cert 001431 lodges four single-glazed windows (codes 1 and 15,
|
||||
# "single glazing, known data"); the after re-lodges every one as double
|
||||
# (gt=5, U=1.40, g=0.72).
|
||||
before: EpcPropertyData = parse_recommendation_summary(
|
||||
"double_glazing_001431_before.pdf"
|
||||
)
|
||||
after: EpcPropertyData = parse_recommendation_summary(
|
||||
"double_glazing_001431_after.pdf"
|
||||
)
|
||||
recommendation: Recommendation | None = recommend_glazing(before, _AnyProduct())
|
||||
assert recommendation is not None
|
||||
|
||||
# Act / Assert
|
||||
_assert_overlay_reproduces_after(
|
||||
before, after, recommendation.options[0].overlay
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.xfail(strict=True, reason=_GLAZING_DRAUGHT_COUPLING_REASON)
|
||||
def test_secondary_glazing_overlay_reproduces_the_relodged_after() -> None:
|
||||
# Arrange — a planning protection forces secondary glazing; the after
|
||||
# re-lodges every single-glazed window as secondary (gt=11, U=2.90, g=0.85).
|
||||
before: EpcPropertyData = parse_recommendation_summary(
|
||||
"secondary_glazing_001431_before.pdf"
|
||||
)
|
||||
after: EpcPropertyData = parse_recommendation_summary(
|
||||
"secondary_glazing_001431_after.pdf"
|
||||
)
|
||||
recommendation: Recommendation | None = recommend_glazing(
|
||||
before, _AnyProduct(), PlanningRestrictions(in_conservation_area=True)
|
||||
)
|
||||
assert recommendation is not None
|
||||
|
||||
# Act / Assert
|
||||
_assert_overlay_reproduces_after(
|
||||
before, after, recommendation.options[0].overlay
|
||||
)
|
||||
|
|
|
|||
|
|
@ -116,11 +116,11 @@ def test_planning_protection_picks_secondary_glazing_over_double() -> None:
|
|||
)
|
||||
|
||||
# Assert — one secondary-glazing Option; each single window upgraded to the
|
||||
# pinned secondary target (gt=7, U=2.90, g unchanged at 0.85).
|
||||
# pinned secondary target (gt=11, U=2.90, g unchanged at 0.85).
|
||||
assert recommendation is not None
|
||||
option = recommendation.options[0]
|
||||
assert option.measure_type == "secondary_glazing"
|
||||
assert dict(option.overlay.windows) == {
|
||||
0: WindowOverlay(glazing_type=7, u_value=2.90, solar_transmittance=0.85),
|
||||
2: WindowOverlay(glazing_type=7, u_value=2.90, solar_transmittance=0.85),
|
||||
0: WindowOverlay(glazing_type=11, u_value=2.90, solar_transmittance=0.85),
|
||||
2: WindowOverlay(glazing_type=11, u_value=2.90, solar_transmittance=0.85),
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue