mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice S0380.86: §5.6 thin-wall stone + §5.8 dry-line closes BP[0] alt1 cascade gap
RdSAP 10 §5.6 (PDF p.40) "U-values of uninsulated stone walls, age
bands A to E":
Table 12 — Default U-values of stone walls
Sandstone or limestone: U = 54.876 × W^(-0.561)
Granite or whinstone: U = 45.315 × W^(-0.513)
Where W is wall thickness in mm.
"Apply the adjustment according to Table 14: Insulation thickness
and corresponding resistance if wall is insulated or dry-lined
including lath and plaster."
Combined with §5.8 (PDF p.40) + Table 14 (PDF p.41) dry-line R = 0.17
m²K/W: U = 1 / (1/U₀ + 0.17).
Cert 000565 BP[0] Main alt1 is the cohort fixture: Stone Granite, age
band A (inherited from Main), 120 mm wall thickness, dry-lined.
§5.6 formula: U₀ = 45.315 × 120^(-0.513) ≈ 3.8871.
§5.8 + Table 14 dry-line: U = 1/(1/3.8871 + 0.17) ≈ **2.3405**.
→ matches worksheet U985-0001-000565 line (29a) "External walls Main
alt.1 ... SolidWallDensePlasterInsul, Solid, 0.0, 2.34" EXACT.
Pre-S0380.86 two coupled bugs blocked this path:
1. Mapper mis-name per [[feedback-no-misleading-insulation-type]]:
`_map_elmhurst_alternative_wall` routed the Elmhurst Summary §7
"Alternative Wall N Thickness" lodging (the WALL thickness)
onto `SapAlternativeWall.wall_insulation_thickness="120"`. The
cascade then mis-bucketed it as 100 mm insulation (bucket=100
→ _BRICK_INS_100 row at age A → U=0.32). The Elmhurst Summary
schema has no "Alternative Wall N Insulation Thickness" line at
all — `wall_insulation_thickness` on alts was always
semantically the wall thickness, never insulation.
2. `u_wall` had no §5.6 thin-wall stone branch. Stone constructions
fell through to Table 6 row values (designed for typical-
thickness ~300mm+ walls), which dramatically under-state heat
loss for sub-200mm stone.
Fix span:
- datatypes/epc/domain/epc_property_data.py:SapAlternativeWall:
new `wall_thickness_mm: Optional[int] = None` field, mirroring
`SapBuildingPart.wall_thickness_mm`.
- datatypes/epc/domain/mapper.py:_map_elmhurst_alternative_wall:
routes Elmhurst `a.thickness_mm` (Wall thickness) onto
`wall_thickness_mm`; leaves `wall_insulation_thickness=None`
on this path (no Elmhurst Summary alt-wall insulation-thickness
line exists).
- domain/sap10_ml/rdsap_uvalues.py:
new `_u_stone_thin_wall_age_a_to_e(construction, W)` helper
implements §5.6 Table 12 formulas. `u_wall` accepts a new
`wall_thickness_mm: Optional[int] = None` param; dispatches
§5.6 formula when (a) wall thickness lodged, (b) age band ∈
A-E, (c) construction ∈ {STONE_GRANITE, STONE_SANDSTONE}.
§5.8 + Table 14 R=0.17 applied on top when dry_lined=True.
- domain/sap10_calculator/worksheet/heat_transmission.py:
`_alt_wall_contribution_w_per_k` passes
`wall_thickness_mm=alt_wall.wall_thickness_mm` to `u_wall`.
Tests (7 new, AAA-structure):
- 5 in domain/sap10_ml/tests/test_rdsap_uvalues.py — granite at
120 mm with dry-line (U=2.34); granite raw formula (U=3.89);
sandstone (U=3.74); age-G gate (Table 6 row, NOT formula); no
wall_thickness fallback (Table 6 row 1.7).
- 2 in backend/documents_parser/tests/test_summary_pdf_mapper_chain
.py — mapper pin (wall_thickness_mm=120 on BP[0] alt1;
wall_insulation_thickness=None) and cascade pin (walls_w_per_k
≥ 595, post-S0380.85 was 555.93).
**Cert 000565 cascade walls: 555.93 → 602.40 W/K (worksheet 604.07;
0.27% residual).** BP[0] alt1 cascade U: 0.32 → 2.34. Cascade walls
within 2 W/K of worksheet target across S0380.85+.86 closure cycle.
Test baseline: 560 pass (was 558 + 7 new − 5 already passing pins
that moved) + 9 expected `test_sap_result_pin[000565-*]` fails
unchanged. Cohort + golden + cert 9501 unaffected: of the 6 cohort
fixtures only cert 000565 alt1 lodged a `wall_insulation_thickness`
value on `SapAlternativeWall` (audit confirmed) — and that value was
always semantically the wall thickness, so the rename is a fix not
a behaviour change. The API mapper path defaults `wall_thickness_mm`
to None (API schema doesn't yet surface alt-wall thickness; safe
forward-compat).
Per [[feedback-verify-handover-claims]]: the post-S0380.84 handover
predicted SH residual would close after the wall fixes. Empirically
SH grew +2591 → +6348 → +7924 across S0380.84/.85/.86 — confirming a
SEPARATE SH-channel over-count that's independent of fabric (each
+1 W/K of spec-correct walls adds ~33.5 kWh of cascade SH, vs the
worksheet's ~38.96 kWh/W/K rate). The walls fixes are spec-correct;
the SH over-count is now a single isolated open work-item for the
next slice (~+8 k kWh structural).
Pyright net-zero per touched file (test_rdsap_uvalues.py error count
actually decreased by 1).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
647c1aad0e
commit
6c8bbbc9e2
6 changed files with 298 additions and 6 deletions
|
|
@ -1460,6 +1460,89 @@ def test_summary_000565_ext2_curtain_wall_routes_to_u_value_1p4_per_rdsap_10_sec
|
|||
)
|
||||
|
||||
|
||||
def test_summary_000565_mapper_routes_alt_wall_thickness_120mm_to_wall_thickness_mm_field() -> None:
|
||||
"""The Summary §7 "Alternative Wall N Thickness" line is the WALL
|
||||
thickness, NOT an insulation thickness. Cert 000565 BP[0] Main
|
||||
alt1 lodges
|
||||
|
||||
Alternative Wall 1 Type SG Stone: granite or whinstone
|
||||
Alternative Wall 1 Insulation A As Built
|
||||
Alternative Wall 1 Dry-lining Yes
|
||||
Alternative Wall 1 Thickness 120 mm
|
||||
|
||||
Pre-S0380.86 `_map_elmhurst_alternative_wall` routed this 120 mm
|
||||
onto `SapAlternativeWall.wall_insulation_thickness="120"`, a
|
||||
semantic mis-name flagged in `[[feedback-no-misleading-insulation-
|
||||
type]]`. The cascade then mis-bucketed it as insulation (bucket
|
||||
100 → _BRICK_INS_100 → U=0.32 at age A) instead of routing to the
|
||||
RdSAP 10 §5.6 thin-wall stone formula (U₀=3.89 → §5.8 dry-line
|
||||
adjustment → U=2.34, matching worksheet line (29a)).
|
||||
|
||||
This pin asserts the mapper now lodges the wall thickness on the
|
||||
new `SapAlternativeWall.wall_thickness_mm` field, leaving
|
||||
`wall_insulation_thickness=None` (the As-Built lodging carries
|
||||
no insulation thickness).
|
||||
"""
|
||||
# Arrange
|
||||
pages = _summary_pdf_to_textract_style_pages(_SUMMARY_000565_PDF)
|
||||
site_notes = ElmhurstSiteNotesExtractor(pages).extract()
|
||||
|
||||
# Act
|
||||
epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes)
|
||||
|
||||
# Assert
|
||||
alt1 = epc.sap_building_parts[0].sap_alternative_wall_1
|
||||
assert alt1 is not None
|
||||
assert alt1.wall_construction == 1, (
|
||||
f"BP[0] alt1 wall_construction = {alt1.wall_construction!r}; "
|
||||
f"expected 1 (WALL_STONE_GRANITE)"
|
||||
)
|
||||
assert alt1.wall_thickness_mm == 120, (
|
||||
f"BP[0] alt1 wall_thickness_mm = {alt1.wall_thickness_mm!r}; "
|
||||
f"expected 120 (the lodged wall thickness, not insulation)"
|
||||
)
|
||||
assert alt1.wall_insulation_thickness is None, (
|
||||
f"BP[0] alt1 wall_insulation_thickness = "
|
||||
f"{alt1.wall_insulation_thickness!r}; expected None (As-Built "
|
||||
f"lodging carries no insulation thickness)"
|
||||
)
|
||||
assert alt1.wall_dry_lined == "Y"
|
||||
|
||||
|
||||
def test_summary_000565_bp0_alt1_stone_granite_thin_wall_routes_to_u_value_2p34_per_rdsap_10_section_5_6() -> None:
|
||||
"""End-to-end cascade pin: with `wall_thickness_mm=120` plumbed
|
||||
through extractor + mapper + `u_wall` §5.6 thin-wall formula +
|
||||
§5.8 dry-line adjustment, cert 000565 BP[0] Main alt1 cascade
|
||||
U-value moves from 0.32 → 2.34 (worksheet line (29a) pin).
|
||||
|
||||
Δ U=2.02 × area=23 m² → +46.5 W/K of cascade walls heat loss.
|
||||
Combined with S0380.85's Curtain Wall closure (+112 W/K), the
|
||||
cascade walls subtotal closes from 443 W/K (pre-S0380.84
|
||||
baseline) → ~602 W/K (worksheet 604.07; <0.5% residual).
|
||||
|
||||
Asserts the cascade walls subtotal is now within 2% of worksheet
|
||||
(post-S0380.85 was 555.93; this slice should bring it to ~602).
|
||||
"""
|
||||
# Arrange
|
||||
from domain.sap10_calculator.worksheet.heat_transmission import (
|
||||
heat_transmission_from_cert,
|
||||
)
|
||||
|
||||
pages = _summary_pdf_to_textract_style_pages(_SUMMARY_000565_PDF)
|
||||
site_notes = ElmhurstSiteNotesExtractor(pages).extract()
|
||||
epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes)
|
||||
|
||||
# Act
|
||||
ht = heat_transmission_from_cert(epc)
|
||||
|
||||
# Assert — worksheet target 604.07; lower-bound 595 is a robust
|
||||
# gate that admits ≤2% residual against the worksheet pin.
|
||||
assert ht.walls_w_per_k >= 595.0, (
|
||||
f"walls_w_per_k = {ht.walls_w_per_k:.2f}; expected ≥595 after "
|
||||
f"§5.6 thin-wall + §5.8 dry-line dispatch (post-S0380.85 was 555.93)"
|
||||
)
|
||||
|
||||
|
||||
def test_summary_000565_ext1_party_wall_routes_to_cavity_filled_code_4() -> None:
|
||||
# Arrange — RdSAP 10 Table 15 row 3 "Cavity masonry filled":
|
||||
# cert 000565 Ext1 lodges "CF Cavity masonry filled". Routes
|
||||
|
|
|
|||
|
|
@ -375,6 +375,14 @@ class SapAlternativeWall:
|
|||
# at U=1.90, where the 9-mm-thick single-layer timber wall doesn't
|
||||
# fit the Table 6 buckets cleanly).
|
||||
u_value: Optional[float] = None
|
||||
# WALL thickness in mm (not insulation thickness — separately
|
||||
# surfaced as `wall_insulation_thickness`). Lodged by Elmhurst
|
||||
# Summary §7 "Alternative Wall N Thickness" when `Thickness
|
||||
# Unknown: No`. Drives the RdSAP 10 §5.6 thin-wall stone formula
|
||||
# (PDF p.40) when construction is stone and age band is A-E.
|
||||
# Mirrors `SapBuildingPart.wall_thickness_mm` per the
|
||||
# [[feedback-no-misleading-insulation-type]] convention.
|
||||
wall_thickness_mm: Optional[int] = None
|
||||
|
||||
@property
|
||||
def is_basement_wall(self) -> bool:
|
||||
|
|
|
|||
|
|
@ -3139,18 +3139,29 @@ def _map_elmhurst_alternative_wall(
|
|||
measurement); we route through the cascade with thickness=None so
|
||||
`u_wall` falls through to the age-band-and-construction default
|
||||
(e.g. Timber Frame age B → U=1.9 for the 000487 9-mm-thin-wall
|
||||
case, matching the full-cert-text "TimberWallOneLayer" lodgement)."""
|
||||
case, matching the full-cert-text "TimberWallOneLayer" lodgement).
|
||||
|
||||
The Elmhurst Summary §7 "Alternative Wall N Thickness" line is the
|
||||
WALL thickness (drives the RdSAP 10 §5.6 thin-wall stone formula
|
||||
per PDF p.40 for age A-E stone constructions), NOT an insulation
|
||||
thickness. Pre-S0380.86 the mapper mis-routed it to
|
||||
`wall_insulation_thickness` per [[feedback-no-misleading-
|
||||
insulation-type]] semantic mismatch; now lodged on the new
|
||||
`wall_thickness_mm` field. There is no separate "Alternative Wall
|
||||
N Insulation Thickness" line in the Elmhurst Summary schema —
|
||||
alt-wall insulation thickness is always None on this path.
|
||||
"""
|
||||
measured_thickness_mm = (
|
||||
a.thickness_mm if (not a.thickness_unknown and a.thickness_mm is not None) else None
|
||||
)
|
||||
return SapAlternativeWall(
|
||||
wall_area=a.area_m2,
|
||||
wall_dry_lined="Y" if a.dry_lined else "N",
|
||||
wall_construction=_elmhurst_wall_construction_int(a.wall_type) or 0,
|
||||
wall_insulation_type=_elmhurst_wall_insulation_int(a.insulation) or 4,
|
||||
wall_thickness_measured="Y" if not a.thickness_unknown else "N",
|
||||
wall_insulation_thickness=(
|
||||
None
|
||||
if a.thickness_unknown
|
||||
else str(a.thickness_mm) if a.thickness_mm is not None else None
|
||||
),
|
||||
wall_insulation_thickness=None,
|
||||
wall_thickness_mm=measured_thickness_mm,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1008,5 +1008,10 @@ def _alt_wall_w_per_k(
|
|||
# when no insulation thickness is lodged. Cohort fixture: cert
|
||||
# 7700 Alt 1 (Cavity, As-Built, Dry-lined) → 1.50 → 1.20.
|
||||
dry_lined=alt_wall.wall_dry_lined == "Y",
|
||||
# RdSAP 10 §5.6 (PDF p.40) — uninsulated stone wall formula
|
||||
# for age bands A-E, keyed on lodged wall thickness. Cohort
|
||||
# fixture: cert 000565 BP[0] alt1 (Stone Granite age A, 120mm,
|
||||
# dry-lined) → U=2.34 via §5.6 + §5.8 chain.
|
||||
wall_thickness_mm=alt_wall.wall_thickness_mm,
|
||||
)
|
||||
return alt_u * net_alt_area
|
||||
|
|
|
|||
|
|
@ -144,6 +144,39 @@ _WALL_INSULATION_LAMBDA_W_PER_MK: Final[float] = 0.04
|
|||
_DRY_LINING_RESISTANCE_M2K_PER_W: Final[float] = 0.17
|
||||
|
||||
|
||||
# RdSAP 10 §5.6 (PDF p.40) — uninsulated stone wall formula, age bands
|
||||
# A to E (Table 12):
|
||||
#
|
||||
# Sandstone or limestone: U = 54.876 × W^(-0.561)
|
||||
# Granite or whinstone: U = 45.315 × W^(-0.513)
|
||||
#
|
||||
# Where W is wall thickness in mm. Apply §5.8 + Table 14 (PDF p.41) on
|
||||
# top for dry-lining / lath-and-plaster: U_adj = 1/(1/U₀ + 0.17). The
|
||||
# formula only applies for age bands A-E per the §5.6 heading; for age
|
||||
# F+ Table 6 row values represent typical-thickness stone walls and
|
||||
# are the spec target.
|
||||
#
|
||||
# Empirical pin: cert 000565 BP[0] Main alt1 lodges Stone Granite,
|
||||
# age A, 120 mm wall, dry-lined → §5.6 + §5.8 → U=2.34 (worksheet
|
||||
# line (29a)). The §5.6 formula keys on a documentary wall-thickness
|
||||
# lodgement (RdSAP 10 §5.3 / §3.5); without it, fall back to Table 6.
|
||||
_STONE_AGE_A_TO_E: Final[frozenset[str]] = frozenset({"A", "B", "C", "D", "E"})
|
||||
|
||||
|
||||
def _u_stone_thin_wall_age_a_to_e(
|
||||
construction: int, wall_thickness_mm: int,
|
||||
) -> Optional[float]:
|
||||
"""RdSAP 10 §5.6 Table 12 (PDF p.40) — formula U-value for an
|
||||
uninsulated stone wall of known thickness in age bands A-E.
|
||||
Returns None when the construction is not stone (granite /
|
||||
sandstone) — caller must fall through to the Table 6 cascade."""
|
||||
if construction == WALL_STONE_GRANITE:
|
||||
return 45.315 * (wall_thickness_mm ** -0.513)
|
||||
if construction == WALL_STONE_SANDSTONE:
|
||||
return 54.876 * (wall_thickness_mm ** -0.561)
|
||||
return None
|
||||
|
||||
|
||||
# RdSAP 10 §5.18 (PDF p.48) — curtain-wall U-values.
|
||||
#
|
||||
# "If documentary evidence is available, use calculated U-value of the
|
||||
|
|
@ -370,6 +403,7 @@ def u_wall(
|
|||
wall_insulation_type: Optional[int] = None,
|
||||
dry_lined: bool = False,
|
||||
curtain_wall_age: Optional[str] = None,
|
||||
wall_thickness_mm: Optional[int] = None,
|
||||
) -> float:
|
||||
"""RdSAP10 wall U-value in W/m^2K, never null.
|
||||
|
||||
|
|
@ -415,6 +449,20 @@ def u_wall(
|
|||
ctry = country if country is not None else Country.ENG
|
||||
age_idx = _age_index(age_band)
|
||||
band = _AGE_BANDS[age_idx]
|
||||
# RdSAP 10 §5.6 (PDF p.40) — uninsulated stone wall thin-wall
|
||||
# formula, age bands A-E. Fires only when a documentary wall
|
||||
# thickness is lodged (per §5.3 documentary-evidence rule).
|
||||
# §5.8 + Table 14 dry-line adjustment applies on top.
|
||||
if (
|
||||
wall_thickness_mm is not None
|
||||
and band in _STONE_AGE_A_TO_E
|
||||
and construction in (WALL_STONE_GRANITE, WALL_STONE_SANDSTONE)
|
||||
):
|
||||
u0 = _u_stone_thin_wall_age_a_to_e(construction, wall_thickness_mm)
|
||||
if u0 is not None:
|
||||
if dry_lined:
|
||||
return 1.0 / (1.0 / u0 + _DRY_LINING_RESISTANCE_M2K_PER_W)
|
||||
return u0
|
||||
known_types = {
|
||||
WALL_STONE_GRANITE, WALL_STONE_SANDSTONE, WALL_SOLID_BRICK, WALL_CAVITY,
|
||||
WALL_TIMBER_FRAME, WALL_SYSTEM_BUILT, WALL_COB,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ from domain.sap10_ml.rdsap_uvalues import (
|
|||
WALL_INSULATION_FILLED_CAVITY,
|
||||
WALL_SOLID_BRICK,
|
||||
WALL_STONE_GRANITE,
|
||||
WALL_STONE_SANDSTONE,
|
||||
WALL_SYSTEM_BUILT,
|
||||
WALL_TIMBER_FRAME,
|
||||
thermal_bridging_y,
|
||||
|
|
@ -586,6 +587,142 @@ def test_u_wall_curtain_wall_pre_2023_uses_rdsap_5_18_default_u_2p0() -> None:
|
|||
assert abs(result - 2.0) <= 1e-9
|
||||
|
||||
|
||||
def test_u_wall_stone_granite_thin_wall_age_a_120mm_dry_lined_applies_5_6_formula_with_5_8_adjustment() -> None:
|
||||
# Arrange — RdSAP 10 §5.6 (PDF p.40) "U-values of uninsulated stone
|
||||
# walls, age bands A to E":
|
||||
#
|
||||
# Table 12: Default U-values of stone walls
|
||||
# Granite or whinstone: U = 45.315 × W^(-0.513)
|
||||
# Where W is wall thickness in mm.
|
||||
#
|
||||
# Then RdSAP 10 §5.8 (PDF p.40) + Table 14 (PDF p.41) — for
|
||||
# dry-lining (including laths and plaster) apply R = 0.17 m²K/W
|
||||
# additively to U₀:
|
||||
#
|
||||
# U = 1 / (1/U₀ + R_insulation)
|
||||
#
|
||||
# Cert 000565 BP[0] Main alt1 is the cohort fixture: stone granite,
|
||||
# age band A (inherited from Main), wall thickness 120 mm, dry-lined.
|
||||
# §5.6 formula: U₀ = 45.315 × 120^(-0.513) ≈ 3.8871
|
||||
# §5.8 + Table 14 dry-line: U = 1/(1/3.8871 + 0.17) ≈ 2.3405
|
||||
# → matches worksheet U985-0001-000565 line (29a) pin U=2.34.
|
||||
#
|
||||
# Pre-S0380.86: the cert lodged its alt-wall thickness via the
|
||||
# misnamed `wall_insulation_thickness="120"` field, which routed
|
||||
# through `_insulation_bucket(120, ins_present=False)` → 100 →
|
||||
# _BRICK_INS_100 (the stone-insulated-100mm row) → 0.32 W/m²K at
|
||||
# age A. Δ contribution −46.5 W/K on the 23 m² alt area.
|
||||
|
||||
# Act
|
||||
result = u_wall(
|
||||
country=Country.ENG,
|
||||
age_band="A",
|
||||
construction=WALL_STONE_GRANITE,
|
||||
insulation_thickness_mm=None,
|
||||
insulation_present=False,
|
||||
wall_insulation_type=4,
|
||||
dry_lined=True,
|
||||
wall_thickness_mm=120,
|
||||
)
|
||||
|
||||
# Assert — worksheet 2.34 (4 d.p. tolerance for round-half-up)
|
||||
assert abs(result - 2.34) <= 1e-2
|
||||
|
||||
|
||||
def test_u_wall_stone_granite_thin_wall_age_a_120mm_no_dry_line_returns_raw_5_6_formula() -> None:
|
||||
# Arrange — same wall + thickness as above but without dry-lining.
|
||||
# §5.6 formula returns U₀ directly (no §5.8 adjustment applied).
|
||||
# U₀ = 45.315 × 120^(-0.513) ≈ 3.8871
|
||||
|
||||
# Act
|
||||
result = u_wall(
|
||||
country=Country.ENG,
|
||||
age_band="A",
|
||||
construction=WALL_STONE_GRANITE,
|
||||
insulation_thickness_mm=None,
|
||||
insulation_present=False,
|
||||
wall_insulation_type=4,
|
||||
dry_lined=False,
|
||||
wall_thickness_mm=120,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert abs(result - 3.8871) <= 1e-3
|
||||
|
||||
|
||||
def test_u_wall_stone_sandstone_thin_wall_age_a_120mm_uses_5_6_sandstone_formula() -> None:
|
||||
# Arrange — §5.6 (PDF p.40) Table 12: sandstone/limestone formula
|
||||
# is distinct from granite/whinstone:
|
||||
# Sandstone or limestone: U = 54.876 × W^(-0.561)
|
||||
# At W=120 mm: U₀ ≈ 3.7408. The dispatch must pick the formula
|
||||
# by construction code (WALL_STONE_SANDSTONE vs WALL_STONE_GRANITE).
|
||||
|
||||
# Act
|
||||
result = u_wall(
|
||||
country=Country.ENG,
|
||||
age_band="A",
|
||||
construction=WALL_STONE_SANDSTONE,
|
||||
insulation_thickness_mm=None,
|
||||
insulation_present=False,
|
||||
wall_insulation_type=4,
|
||||
dry_lined=False,
|
||||
wall_thickness_mm=120,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert abs(result - 3.7408) <= 1e-3
|
||||
|
||||
|
||||
def test_u_wall_stone_granite_age_g_with_wall_thickness_ignores_5_6_formula_per_age_a_to_e_gate() -> None:
|
||||
# Arrange — §5.6 (PDF p.40) heading explicitly scopes the formula
|
||||
# to "age bands A to E". For age F onwards Table 6 gives literal
|
||||
# U-values that already encode typical-thickness stone wall heat
|
||||
# loss — applying §5.6 outside the A-E gate would over-estimate U
|
||||
# for modern stone walls. Cert 000565 alt1 happens to be age A,
|
||||
# but this test guards against §5.6 leaking into post-1976 stone
|
||||
# constructions.
|
||||
#
|
||||
# At age G stone granite, Table 6 gives U=0.60 (cohort-typical row).
|
||||
# The §5.6 formula at 120 mm would return 3.89 — wildly over.
|
||||
|
||||
# Act
|
||||
result = u_wall(
|
||||
country=Country.ENG,
|
||||
age_band="G",
|
||||
construction=WALL_STONE_GRANITE,
|
||||
insulation_thickness_mm=None,
|
||||
insulation_present=False,
|
||||
wall_insulation_type=4,
|
||||
wall_thickness_mm=120,
|
||||
)
|
||||
|
||||
# Assert — Table 6 row at age G, NOT §5.6 formula.
|
||||
assert abs(result - 0.60) <= 1e-3
|
||||
|
||||
|
||||
def test_u_wall_stone_granite_age_a_without_wall_thickness_returns_table_6_age_a_default() -> None:
|
||||
# Arrange — §5.6 formula only fires when a wall thickness is
|
||||
# lodged. Without documentary wall-thickness evidence, fall back
|
||||
# to the Table 6 row (which represents typical thickness). For
|
||||
# age A stone granite without thickness, the cascade preserves
|
||||
# its existing "as-built typical" U value rather than the formula
|
||||
# extrapolation.
|
||||
|
||||
# Act
|
||||
result = u_wall(
|
||||
country=Country.ENG,
|
||||
age_band="A",
|
||||
construction=WALL_STONE_GRANITE,
|
||||
insulation_thickness_mm=None,
|
||||
insulation_present=False,
|
||||
wall_insulation_type=4,
|
||||
wall_thickness_mm=None,
|
||||
)
|
||||
|
||||
# Assert — _TYPICAL_STONE_UNINSULATED at age A = 1.7 (cohort default).
|
||||
assert abs(result - 1.7) <= 1e-3
|
||||
|
||||
|
||||
def test_u_wall_curtain_wall_missing_age_lodgement_defaults_to_pre_2023_u_2p0_per_rdsap_5_18() -> None:
|
||||
# Arrange — when the cert lodges `Type: CW Curtain Wall` but no
|
||||
# `Curtain Wall Age` line (older Elmhurst Summary PDFs, or API EPCs
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue