From f40485887d12416013d75fcf70bfe3c20b8bc1a8 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sun, 7 Jun 2026 18:39:01 +0000 Subject: [PATCH] =?UTF-8?q?fix(u-value):=20RdSAP10=20ignores=20gov-API=20w?= =?UTF-8?q?all=20insulation=20conductivity=20=E2=86=92=20=C2=A75.8=20defau?= =?UTF-8?q?lt=20=CE=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- domain/sap10_ml/rdsap_uvalues.py | 51 ++++++++------------- domain/sap10_ml/tests/test_rdsap_uvalues.py | 27 +++++++---- 2 files changed, 37 insertions(+), 41 deletions(-) diff --git a/domain/sap10_ml/rdsap_uvalues.py b/domain/sap10_ml/rdsap_uvalues.py index 4dc8a40e..2ef935ce 100644 --- a/domain/sap10_ml/rdsap_uvalues.py +++ b/domain/sap10_ml/rdsap_uvalues.py @@ -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 diff --git a/domain/sap10_ml/tests/test_rdsap_uvalues.py b/domain/sap10_ml/tests/test_rdsap_uvalues.py index bc06a68c..f7e31c70 100644 --- a/domain/sap10_ml/tests/test_rdsap_uvalues.py +++ b/domain/sap10_ml/tests/test_rdsap_uvalues.py @@ -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