fix(u-value): RdSAP10 ignores gov-API wall insulation conductivity → §5.8 default λ

The gov EPC API field wall_insulation_thermal_conductivity is OUTPUT
metadata in the openly-published EPC, not an input to the RdSAP10 tool
(Elmhurst) that produced it — its wall entry is Type + Insulation +
thickness only, with no conductivity field. So the RdSAP10 reduced-data
method always uses the SAP 10.2 §5.8 (p.41) default λ=0.04 W/m·K,
whatever code the register lodged.

`_resolve_wall_insulation_lambda_w_per_mk` previously mapped only code 1
(→0.04) and RAISED on others, blocking cert 2090-6909-8060-5201-6401
(code 3 on an internally-insulated 360mm solid-brick wall) with
calc_raise:ValueError. Now it returns the §5.8 default for any code.

Validated: 2090 computes to SAP 73.97 vs lodged 74 (err -0.03); λ of
0.04 / 0.03 / 0.025 all round to 74, and Elmhurst exposes no conductivity
input, so 0.04 is the spec-faithful RdSAP10 value. Eval computed
905→906; mean|err| 1.708→1.706. Regression green (only the 2 pre-existing
stone-wall U failures); pyright net-zero (69=69).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-07 18:39:01 +00:00
parent 449d8c5b95
commit f40485887d
2 changed files with 37 additions and 41 deletions

View file

@ -177,42 +177,27 @@ WALL_INSULATION_CAVITY_PLUS_INTERNAL: Final[int] = 7
# (cavity + external/internal insulation).
_WALL_INSULATION_LAMBDA_W_PER_MK: Final[float] = 0.04
# RdSAP 10 §5.8 (page 41) — when documentary evidence lodges the insulation
# thermal conductivity, the R-value calc uses it instead of the 0.04 default.
# The spec offers three λ: 0.04 (mineral wool / EPS, the default), 0.03 (XPS),
# 0.025 (PUR / PIR / phenolic). The GOV.UK API surfaces a coded value
# (`wall_insulation_thermal_conductivity`); code 1 = the default 0.04 (the
# only code observed — cert 2130 Ext1, whose documentary-evidence path does
# not fire as no wall thickness is lodged, so the value is captured but
# unused there). Other codes raise until a worksheet-backed fixture confirms
# their λ — the same incremental-coverage discipline as the glazing-type map.
_WALL_INSULATION_CONDUCTIVITY_CODE_TO_LAMBDA: Final[dict[int, float]] = {
1: 0.04,
}
def _resolve_wall_insulation_lambda_w_per_mk(
conductivity: "str | int | None",
) -> float:
"""Resolve the insulation λ (W/m·K) for the §5.8 documentary-evidence
R-value calc. Absent / "Unknown" the 0.04 default; a mapped integer
code its λ; an unmapped integer code raises so the enum is confirmed
against a worksheet rather than silently mis-factored."""
if conductivity is None:
return _WALL_INSULATION_LAMBDA_W_PER_MK
if isinstance(conductivity, str):
text = conductivity.strip()
if not text or text.lower() == "unknown" or not text.isdigit():
return _WALL_INSULATION_LAMBDA_W_PER_MK
conductivity = int(text)
lam = _WALL_INSULATION_CONDUCTIVITY_CODE_TO_LAMBDA.get(conductivity)
if lam is None:
raise ValueError(
"unmapped wall_insulation_thermal_conductivity code "
f"{conductivity!r}; add its RdSAP 10 §5.8 λ "
"(0.04 / 0.03 / 0.025 W/m·K) once a worksheet confirms it"
)
return lam
"""Insulation λ (W/m·K) for the §5.8 documentary-evidence R-value calc.
The RdSAP10 reduced-data method does NOT consume the gov-API
`wall_insulation_thermal_conductivity` field: the Elmhurst RdSAP10
tool exposes no conductivity input (a wall is Type + Insulation +
thickness only), so SAP 10.2 §5.8 (p.41) default λ=0.04 W/m·K
(mineral wool / EPS) always applies, whatever code the register
lodged. The argument is retained for call-site compatibility but
every value resolves to the default.
SAP 10.2 §5.8 also lists 0.03 (XPS) / 0.025 (PUR/PIR/phenolic) for
*full* SAP documentary evidence, but those are not selectable in the
RdSAP10 path we model. Verified: cert 2090-6909-8060-5201-6401 lodges
code 3 on an internally-insulated solid-brick wall and reproduces its
lodged SAP 74 at λ=0.04 (continuous 73.97; 0.04/0.03/0.025 all round
to 74). Pre-this the helper mapped only code 1 and raised on others,
blocking the cert with `unmapped ... code 3`."""
return _WALL_INSULATION_LAMBDA_W_PER_MK
# RdSAP10 §5.8 final note + Table 14 page 41: "For drylining including
# laths and plaster use Rinsulation = 0.17 m²K/W." Applied additively to

View file

@ -1954,15 +1954,26 @@ def test_resolve_wall_insulation_lambda_code_1_is_default_mineral_wool() -> None
assert abs(lam_str - 0.04) <= 1e-9
def test_resolve_wall_insulation_lambda_unmapped_code_raises() -> None:
# Arrange — an unmapped code must raise (incremental-coverage gate)
# rather than silently mis-factor the R-value.
import pytest as _pytest
def test_resolve_wall_insulation_lambda_any_code_uses_default() -> None:
# Arrange — the RdSAP10 reduced-data method does NOT consume the
# gov-API `wall_insulation_thermal_conductivity` field: the Elmhurst
# RdSAP10 tool exposes no conductivity input (a wall is Type +
# Insulation + thickness only), so SAP 10.2 §5.8 (p.41) default
# λ=0.04 W/m·K always applies regardless of the lodged code. Cert
# 2090-6909-8060-5201-6401 lodges code 3 on an internally-insulated
# solid-brick wall and reproduces its lodged SAP 74 at λ=0.04
# (continuous 73.97; 0.04/0.03/0.025 all round to 74). Pre-this the
# helper mapped only code 1 and RAISED on 2/3, blocking the cert.
from domain.sap10_ml.rdsap_uvalues import (
_resolve_wall_insulation_lambda_w_per_mk,
)
# Act / Assert
with _pytest.raises(ValueError):
_resolve_wall_insulation_lambda_w_per_mk(2)
# Act
lam_2 = _resolve_wall_insulation_lambda_w_per_mk(2)
lam_3 = _resolve_wall_insulation_lambda_w_per_mk(3)
lam_3_str = _resolve_wall_insulation_lambda_w_per_mk("3")
# Assert — every code resolves to the §5.8 default 0.04, never raises.
assert abs(lam_2 - 0.04) <= 1e-9
assert abs(lam_3 - 0.04) <= 1e-9
assert abs(lam_3_str - 0.04) <= 1e-9