mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice S0380.13: widen cantilever gate to accept "House" descriptive form
Closes cert 2636 to spec floor (Δ +0.5167 → +0.0323) by accepting
both the EPC schema enum-as-string ("0") AND the Elmhurst Summary
mapper's descriptive form ("House") for the cantilever-detection
property-type gate at `heat_transmission.py:768`.
Root cause: slice 102f-prep.9 (commit 06b4ef3d) added cantilever
detection gated on `epc.property_type == _PROPERTY_TYPE_HOUSE` where
`_PROPERTY_TYPE_HOUSE = "0"`. That matches the API mapper's encoding
(schema enum), but the Summary mapper produces "House" (descriptive)
and the hand-built worksheet fixtures also use "House" — so neither
triggers the gate and the cantilever path silently no-ops on the
Summary path. Cert 2636's worksheet (28b) "Exposed floor Main 3.74
× 1.20 = 4.4880" is the cantilever — without surfacing it the
cascade missed 4.488 W/K of floor heat loss.
Three-encoding origins:
- API mapper: property_type='0' (schema enum-as-string)
- Summary mapper: property_type='House' (descriptive from §1)
- Hand-built fixtures: property_type='House' (legacy convention)
Fix: replace the equality check with a `_is_house()` helper that
accepts the {"0", "House"} frozenset. Centralised so future
property-type sensitive gates can call the same helper.
Forcing function: cert 2636 first-attempt Summary SAP closes from
Δ +0.5167 (after S0380.12 walls fix) to Δ **+0.0323** — within the
±0.07 ASHP-cohort spec floor. `floor_w_per_k` moves from 19.1982
(ground floor only) to 23.6862 (ground 19.20 + cantilever 4.49 =
worksheet (28a) + (28b) exact match).
Cohort closure status (6 of 7 ASHP certs at spec floor):
cert Δ vs worksheet spec floor?
0380 +0.0594 ✓
0350 +0.0458 ✓
2225 +0.0441 ✓
2636 +0.0323 ✓ ← this slice
3800 +0.0442 ✓
9285 +0.0502 ✓
9418 +2.5973 ✗ (Daikin EDLQ05CAV3 — final cert)
Boiler hand-built parity verified intact: 5 hand-built cohort certs
(000474, 000477, 000480, 000490, 000516) all use property_type=
"House" and now also fire the cantilever gate, but none have
floor1_area > floor0_area + 1m² (the cantilever-area trigger) so
their cascade output is unchanged. Regression suite 683 pass + 10
fail (= handover baseline 669 + 10 + 17 new GREEN tests across
S0380.2..S0380.13).
Pyright net-zero on edited files:
domain/sap10_calculator/worksheet/heat_transmission.py: 13
(baseline; no new errors)
backend/documents_parser/tests/test_summary_pdf_mapper_chain.py: 0
Spec / precedent refs:
- Slice 102f-prep.9 (commit 06b4ef3d) — RdSAP cantilever-exposed-
floor detection (originally API-only via `property_type=="0"` gate).
- SAP 10.2 Table 20 — U_exposed_floor (age D + no insulation →
1.20 W/m²K, the cohort's cantilever U-value).
- Cert 2636 worksheet `dr87-0001-000898.pdf` line refs (28a)+(28b)
sum 23.6862 W/K (exact cascade match after this slice).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
03c4ea4921
commit
395ad30c48
2 changed files with 45 additions and 5 deletions
|
|
@ -789,6 +789,33 @@ def test_summary_2225_full_chain_sap_within_spec_floor_of_worksheet() -> None:
|
|||
assert abs(result.sap_score_continuous - worksheet_unrounded_sap) < _ASHP_COHORT_CHAIN_TOLERANCE
|
||||
|
||||
|
||||
def test_summary_2636_full_chain_sap_within_spec_floor_of_worksheet() -> None:
|
||||
# Arrange — cert 2636-0525-2600-0401-2296 (Summary_000898.pdf):
|
||||
# Mitsubishi PUZ-WM50VHA, mid-terrace house with **alt-wall +
|
||||
# cantilever** — the most complex geometry in the ASHP cohort.
|
||||
# Worksheet "SAP value" lodges 86.2641.
|
||||
#
|
||||
# Closed by two combined slices:
|
||||
# - S0380.12: alt-wall window-location parser fix (walls W/K
|
||||
# 20.5595 → 20.0240 = worksheet exact).
|
||||
# - S0380.13: cantilever gate accepts "House" descriptive form
|
||||
# in addition to the schema enum "0" (allowing the Summary
|
||||
# mapper's descriptive property_type to trigger the cantilever
|
||||
# detection that slice 102f-prep.9 added on the API path).
|
||||
pages = _summary_pdf_to_textract_style_pages(_SUMMARY_000898_PDF)
|
||||
site_notes = ElmhurstSiteNotesExtractor(pages).extract()
|
||||
epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes)
|
||||
|
||||
# Act
|
||||
result = calculate_sap_from_inputs(
|
||||
cert_to_inputs(epc, prices=SAP_10_2_SPEC_PRICES)
|
||||
)
|
||||
|
||||
# Assert — ±0.07 ASHP-cohort spec-floor tolerance.
|
||||
worksheet_unrounded_sap = 86.2641
|
||||
assert abs(result.sap_score_continuous - worksheet_unrounded_sap) < _ASHP_COHORT_CHAIN_TOLERANCE
|
||||
|
||||
|
||||
def test_summary_3800_full_chain_sap_within_spec_floor_of_worksheet() -> None:
|
||||
# Arrange — cert 3800-8515-0922-3398-3563 (Summary_000901.pdf /
|
||||
# dr87-0001-000901.pdf) is the third ASHP cohort cert to close on
|
||||
|
|
|
|||
|
|
@ -109,10 +109,23 @@ _COS_30_DEG: Final[float] = cos(radians(30.0))
|
|||
# BP0: 195%), neither of which the worksheet treats as cantilever.
|
||||
_CANTILEVER_MIN_AREA_M2: Final[float] = 1.0
|
||||
_CANTILEVER_MAX_RATIO: Final[float] = 0.25
|
||||
# EPC API `property_type` strings that flag a dwelling as a house (not
|
||||
# flat). Cantilever detection only fires for houses — flats with very
|
||||
# small floor=0 areas (stairwell access) would otherwise over-count.
|
||||
_PROPERTY_TYPE_HOUSE: Final[str] = "0"
|
||||
# `property_type` values that flag a dwelling as a house (not flat).
|
||||
# Cantilever detection only fires for houses — flats with very small
|
||||
# floor=0 areas (stairwell access) would otherwise over-count.
|
||||
# The API mapper produces the EPC schema enum-as-string ("0"), the
|
||||
# Elmhurst Summary mapper produces the descriptive form ("House"), and
|
||||
# the hand-built worksheet fixtures use the descriptive form too. The
|
||||
# canonical check below accepts both so the cantilever path fires
|
||||
# uniformly regardless of source-mapper encoding.
|
||||
_PROPERTY_TYPES_HOUSE: Final[frozenset[str]] = frozenset({"0", "House"})
|
||||
|
||||
|
||||
def _is_house(property_type: Optional[str]) -> bool:
|
||||
"""True when `epc.property_type` encodes a house (not flat / maisonette
|
||||
/ park home). Tolerant of both the API schema's enum-as-string ("0")
|
||||
and the Summary mapper / hand-built fixture descriptive form
|
||||
("House")."""
|
||||
return property_type in _PROPERTY_TYPES_HOUSE
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
@ -765,7 +778,7 @@ def heat_transmission_from_cert(
|
|||
# thermal bridges via its area on (31).
|
||||
cantilever_area = (
|
||||
_round_half_up(geom.get("cantilever_floor_area_m2", 0.0), _AREA_ROUND_DP)
|
||||
if epc.property_type == _PROPERTY_TYPE_HOUSE
|
||||
if _is_house(epc.property_type)
|
||||
else 0.0
|
||||
)
|
||||
if cantilever_area > 0:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue