mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice S0380.32: route bare \"Extension\" window location to BP[1] per RdSAP10 §3 — closes cert 9380 +0.027 residual
RdSAP10 §3 p.17:
"When specifying windows and doors, for each building part
assessor allocates windows and doors to the corresponding
wall (the appropriate main wall or each alternative wall).
For each building part, software will deduct window/door
areas contained in the relevant wall areas."
SAP 10.2 §3 p.16:
"Wall area is the net area of walls after subtracting the
area of windows and doors."
Cert 9380's Summary PDF lodges 2 windows on its single extension,
but pdftotext wraps "1st" onto a preceding layout line while
"Extension" lands on a separate line — the Elmhurst extractor
captures only the second token. `_window_bp_index` previously
matched "main" / "1st"-"4th" prefixes but fell through bare
"Extension" to BP[0] (main), causing the cascade to deduct ext1
windows from the main wall:
Worksheet (29a): main 60.60 × 0.70 + ext1 18.25 × 0.53 = 52.0925
Pre-fix cascade: main 59.01 × 0.70 + ext1 19.84 × 0.53 = 51.8222
Δ -0.27 W/K → SAP +0.027
This slice adds bare "extension" (when num_parts >= 2) as a sibling
to the ordinal-prefix matches. Closes cert 9380 +0.027 → -4.8e-6.
Cohort-2 distribution after S0380.31 + S0380.32:
34 exact + 4 ≤0.07 (was 33 exact + 5 ≤0.07).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
86226ebdb6
commit
396907f46a
2 changed files with 50 additions and 0 deletions
|
|
@ -212,6 +212,17 @@ def _window_bp_index(window_location: Any, num_parts: int) -> int:
|
|||
for prefix, idx in (("1st", 1), ("2nd", 2), ("3rd", 3), ("4th", 4)):
|
||||
if s.startswith(prefix):
|
||||
return idx if idx < num_parts else 0
|
||||
# Bare "Extension" — PDF wrap artifact: cert 9380's Summary lodges
|
||||
# "1st Extension" but pdftotext wraps "1st" onto a preceding layout
|
||||
# line, so the extractor only captures "Extension". RdSAP10 §3 p.17
|
||||
# requires window/door areas to deduct from the building part the
|
||||
# opening pierces ("for each building part, software will deduct
|
||||
# window/door areas contained in the relevant wall areas"); without
|
||||
# this route the ext1 windows deduct from main, over-counting the
|
||||
# main wall's contribution and under-counting the ext1's by the
|
||||
# U-value difference × opening area.
|
||||
if s == "extension" and num_parts >= 2:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ from domain.sap10_calculator.worksheet.heat_transmission import (
|
|||
HeatTransmission,
|
||||
heat_transmission_from_cert,
|
||||
)
|
||||
from domain.sap10_calculator.worksheet.heat_transmission import (
|
||||
_window_bp_index, # pyright: ignore[reportPrivateUsage]
|
||||
)
|
||||
|
||||
|
||||
def test_roof_insulated_assumed_with_ni_thickness_uses_50mm_per_section_5_11_4() -> None:
|
||||
|
|
@ -593,6 +596,42 @@ def test_walls_w_per_k_uses_sum_of_per_storey_perimeter_times_height_not_ground_
|
|||
assert result.walls_w_per_k == pytest.approx(24.0, abs=0.5)
|
||||
|
||||
|
||||
def test_window_bp_index_routes_bare_extension_to_first_extension_per_rdsap10_section_3() -> None:
|
||||
# Arrange — RdSAP10 §3 p.17: "for each building part, software will
|
||||
# deduct window/door areas contained in the relevant wall areas".
|
||||
# Cert 9380's Summary PDF lodges 2 windows on its single extension,
|
||||
# but pdftotext wraps "1st" onto a preceding layout line while
|
||||
# "Extension" lands on a separate line — the Elmhurst extractor
|
||||
# captures only the second token ("Extension") and the dwelling has
|
||||
# exactly 2 building parts (Main + Ext1). Without this route the
|
||||
# extension windows deduct from the main wall and the U-weighted
|
||||
# net-area sum drifts by the U-difference × opening area (cert
|
||||
# 9380: 1.59 × (0.70 − 0.53) = 0.27 W/K on (29a)).
|
||||
|
||||
# Act
|
||||
bare_extension_idx = _window_bp_index("Extension", num_parts=2)
|
||||
ordinal_idx = _window_bp_index("1st Extension", num_parts=2)
|
||||
main_idx = _window_bp_index("Main", num_parts=2)
|
||||
|
||||
# Assert — bare "Extension" routes to BP[1] just like "1st Extension".
|
||||
assert bare_extension_idx == 1
|
||||
assert ordinal_idx == 1
|
||||
assert main_idx == 0
|
||||
|
||||
|
||||
def test_window_bp_index_bare_extension_with_only_main_bp_falls_back_to_main() -> None:
|
||||
# Arrange — when the dwelling has no extension BP at all, the
|
||||
# "Extension" string is unresolvable; the function must fall back
|
||||
# to BP[0] (main) rather than raise. Mirrors the existing ordinal-
|
||||
# prefix out-of-range behaviour.
|
||||
|
||||
# Act
|
||||
idx = _window_bp_index("Extension", num_parts=1)
|
||||
|
||||
# Assert
|
||||
assert idx == 0
|
||||
|
||||
|
||||
def test_main_plus_extension_sums_per_element_contributions() -> None:
|
||||
# Arrange — Main + single-storey age L extension. Each contributes to the
|
||||
# element totals. With_extension > main_only on every populated field
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue