mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
S0380.217: capture wall_insulation_thermal_conductivity (was dropped)
Second silently-dropped field from the 2130 audit: the schema-21 SapBuildingPart never declared `wall_insulation_thermal_conductivity`, so `from_dict` discarded it. Captured it through schema 21.0.0/21.0.1 → domain SapBuildingPart → API mapper, and wired it into u_wall's RdSAP 10 §5.8 documentary-evidence R-value calc (both the solid-brick §5.7/§5.8 path and the cavity-composite path), replacing the bare 0.04 λ constant with a resolved λ. Resolver: absent / "Unknown" → the §5.8 default 0.04 W/m·K (mineral wool / EPS); a mapped code → its λ; an unmapped integer code RAISES so the enum is confirmed against a worksheet rather than silently mis-factored (same incremental-coverage discipline as the glazing-type map). Only code 1 (= the default 0.04) is mapped — the sole observed value (cert 2130 Ext1). Zero cascade effect today: the λ path fires only for solid-brick/cavity walls with a *measured* wall thickness, and 2130 Ext1 lodges no wall thickness, so its conductivity is captured-but-unused; all existing §5.8 certs lodge no conductivity → 0.04 default unchanged. The point is to stop dropping lodged data and make λ correct when a future cert exercises it. Suite: 2523 passed (1 pre-existing TFA fail); sap10_ml 237 passed (2 pre-existing stone-formula fails). Zero new pyright errors (46=46). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
6b4f1aec44
commit
f895dd3ab7
7 changed files with 140 additions and 2 deletions
|
|
@ -477,6 +477,10 @@ class SapBuildingPart:
|
|||
# separate measured field), else the lodged string ("NI", a numeric
|
||||
# string, etc.). Mirrors `roof_insulation_thickness`.
|
||||
wall_insulation_thickness: Optional[Union[str, int]] = None
|
||||
# RdSAP 10 §5.8 thermal-conductivity code for measured wall insulation
|
||||
# (λ = 0.04 / 0.03 / 0.025 W/m·K). Used by the documentary-evidence
|
||||
# R-value path when a measured wall thickness is lodged alongside it.
|
||||
wall_insulation_thermal_conductivity: Optional[Union[str, int]] = None
|
||||
sap_alternative_wall_1: Optional[SapAlternativeWall] = None
|
||||
sap_alternative_wall_2: Optional[SapAlternativeWall] = None
|
||||
|
||||
|
|
|
|||
|
|
@ -564,6 +564,9 @@ class EpcPropertyDataMapper:
|
|||
bp.wall_insulation_thickness,
|
||||
getattr(bp, "wall_insulation_thickness_measured", None),
|
||||
),
|
||||
wall_insulation_thermal_conductivity=getattr(
|
||||
bp, "wall_insulation_thermal_conductivity", None
|
||||
),
|
||||
floor_heat_loss=bp.floor_heat_loss,
|
||||
floor_insulation_thickness=None,
|
||||
roof_construction=bp.roof_construction,
|
||||
|
|
@ -700,6 +703,9 @@ class EpcPropertyDataMapper:
|
|||
bp.wall_insulation_thickness,
|
||||
getattr(bp, "wall_insulation_thickness_measured", None),
|
||||
),
|
||||
wall_insulation_thermal_conductivity=getattr(
|
||||
bp, "wall_insulation_thermal_conductivity", None
|
||||
),
|
||||
floor_heat_loss=bp.floor_heat_loss,
|
||||
floor_insulation_thickness=None,
|
||||
roof_construction=bp.roof_construction,
|
||||
|
|
@ -836,6 +842,9 @@ class EpcPropertyDataMapper:
|
|||
bp.wall_insulation_thickness,
|
||||
getattr(bp, "wall_insulation_thickness_measured", None),
|
||||
),
|
||||
wall_insulation_thermal_conductivity=getattr(
|
||||
bp, "wall_insulation_thermal_conductivity", None
|
||||
),
|
||||
floor_heat_loss=bp.floor_heat_loss,
|
||||
# API certs commonly lodge "NI" (no measured
|
||||
# thickness) on floors that aren't actually
|
||||
|
|
@ -998,6 +1007,9 @@ class EpcPropertyDataMapper:
|
|||
bp.wall_insulation_thickness,
|
||||
getattr(bp, "wall_insulation_thickness_measured", None),
|
||||
),
|
||||
wall_insulation_thermal_conductivity=getattr(
|
||||
bp, "wall_insulation_thermal_conductivity", None
|
||||
),
|
||||
floor_heat_loss=bp.floor_heat_loss,
|
||||
# API certs commonly lodge "NI" (no measured
|
||||
# thickness) on floors that aren't actually
|
||||
|
|
@ -1177,6 +1189,9 @@ class EpcPropertyDataMapper:
|
|||
bp.wall_insulation_thickness,
|
||||
getattr(bp, "wall_insulation_thickness_measured", None),
|
||||
),
|
||||
wall_insulation_thermal_conductivity=getattr(
|
||||
bp, "wall_insulation_thermal_conductivity", None
|
||||
),
|
||||
floor_heat_loss=bp.floor_heat_loss,
|
||||
# API certs commonly lodge "NI" (no measured
|
||||
# thickness) on floors that aren't actually
|
||||
|
|
@ -1397,6 +1412,9 @@ class EpcPropertyDataMapper:
|
|||
bp.wall_insulation_thickness,
|
||||
getattr(bp, "wall_insulation_thickness_measured", None),
|
||||
),
|
||||
wall_insulation_thermal_conductivity=getattr(
|
||||
bp, "wall_insulation_thermal_conductivity", None
|
||||
),
|
||||
floor_heat_loss=bp.floor_heat_loss,
|
||||
# API certs commonly lodge "NI" (no measured
|
||||
# thickness) on floors that aren't actually
|
||||
|
|
@ -1662,6 +1680,9 @@ class EpcPropertyDataMapper:
|
|||
bp.wall_insulation_thickness,
|
||||
getattr(bp, "wall_insulation_thickness_measured", None),
|
||||
),
|
||||
wall_insulation_thermal_conductivity=getattr(
|
||||
bp, "wall_insulation_thermal_conductivity", None
|
||||
),
|
||||
floor_heat_loss=bp.floor_heat_loss,
|
||||
# API certs commonly lodge "NI" (no measured
|
||||
# thickness) on floors that aren't actually
|
||||
|
|
|
|||
|
|
@ -246,6 +246,11 @@ class SapBuildingPart:
|
|||
# undeclared, so `from_dict` silently dropped it and the cascade fell
|
||||
# back to the 50 mm "insulation present, unknown thickness" default.
|
||||
wall_insulation_thickness_measured: Optional[Union[str, int]] = None
|
||||
# Lodged thermal-conductivity code for measured wall insulation
|
||||
# (RdSAP 10 §5.8: λ = 0.04 / 0.03 / 0.025 W/m·K). Previously undeclared
|
||||
# → dropped by `from_dict`. Consumed by `u_wall`'s documentary-evidence
|
||||
# R-value path when a measured wall thickness is also lodged.
|
||||
wall_insulation_thermal_conductivity: Optional[Union[str, int]] = None
|
||||
floor_insulation_thickness: Optional[str] = None
|
||||
flat_roof_insulation_thickness: Optional[Union[str, int]] = None
|
||||
|
||||
|
|
|
|||
|
|
@ -284,6 +284,11 @@ class SapBuildingPart:
|
|||
# undeclared, so `from_dict` silently dropped it and the cascade fell
|
||||
# back to the 50 mm "insulation present, unknown thickness" default.
|
||||
wall_insulation_thickness_measured: Optional[Union[str, int]] = None
|
||||
# Lodged thermal-conductivity code for measured wall insulation
|
||||
# (RdSAP 10 §5.8: λ = 0.04 / 0.03 / 0.025 W/m·K). Previously undeclared
|
||||
# → dropped by `from_dict`. Consumed by `u_wall`'s documentary-evidence
|
||||
# R-value path when a measured wall thickness is also lodged.
|
||||
wall_insulation_thermal_conductivity: Optional[Union[str, int]] = None
|
||||
floor_insulation_thickness: Optional[str] = None
|
||||
flat_roof_insulation_thickness: Optional[Union[str, int]] = None
|
||||
|
||||
|
|
|
|||
|
|
@ -750,6 +750,10 @@ def heat_transmission_from_cert(
|
|||
# insulation_type combination doesn't match the formula
|
||||
# path's preconditions.
|
||||
wall_thickness_mm=part.wall_thickness_mm,
|
||||
# RdSAP 10 §5.8 — lodged insulation thermal-conductivity
|
||||
# code feeds the documentary-evidence R-value calc when a
|
||||
# measured wall thickness is also present (else ignored).
|
||||
wall_insulation_thermal_conductivity=part.wall_insulation_thermal_conductivity,
|
||||
)
|
||||
# When the per-bp `roof_insulation_thickness` is explicitly lodged
|
||||
# as 0 (uninsulated — e.g. cert 001479 Ext2 PS sloping ceiling
|
||||
|
|
|
|||
|
|
@ -177,6 +177,43 @@ 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
|
||||
|
||||
# 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
|
||||
# the base U-value of an otherwise-uninsulated wall when the cert lodges
|
||||
|
|
@ -489,6 +526,7 @@ def u_wall(
|
|||
dry_lined: bool = False,
|
||||
curtain_wall_age: Optional[str] = None,
|
||||
wall_thickness_mm: Optional[int] = None,
|
||||
wall_insulation_thermal_conductivity: "str | int | None" = None,
|
||||
) -> float:
|
||||
"""RdSAP10 wall U-value in W/m^2K, never null.
|
||||
|
||||
|
|
@ -601,7 +639,10 @@ def u_wall(
|
|||
):
|
||||
u0 = _u_brick_thin_wall_age_a_to_e(wall_thickness_mm)
|
||||
r_ins = _r_insulation_table_14(
|
||||
insulation_thickness_mm, _WALL_INSULATION_LAMBDA_W_PER_MK,
|
||||
insulation_thickness_mm,
|
||||
_resolve_wall_insulation_lambda_w_per_mk(
|
||||
wall_insulation_thermal_conductivity
|
||||
),
|
||||
)
|
||||
u_unrounded = 1.0 / (1.0 / u0 + r_ins)
|
||||
return float(
|
||||
|
|
@ -623,7 +664,9 @@ def u_wall(
|
|||
# for column alignment). Cascade-internal HLC then uses the
|
||||
# rounded U so net wall HLC matches `A × U_2dp` exactly.
|
||||
u_filled = _CAVITY_FILLED_ENG[age_idx]
|
||||
r_ins = (insulation_thickness_mm / 1000.0) / _WALL_INSULATION_LAMBDA_W_PER_MK
|
||||
r_ins = (insulation_thickness_mm / 1000.0) / _resolve_wall_insulation_lambda_w_per_mk(
|
||||
wall_insulation_thermal_conductivity
|
||||
)
|
||||
u_unrounded = 1.0 / (1.0 / u_filled + r_ins)
|
||||
# Half-up 2-d.p. round so 0.2545 → 0.25, matching the dr87
|
||||
# worksheet's column-display behaviour (used downstream in A×U).
|
||||
|
|
|
|||
|
|
@ -1870,3 +1870,59 @@ def test_u_floor_matches_section_5_12_formula_for_cohort_geometry(
|
|||
|
||||
# Assert
|
||||
assert abs(u - expected_u) < 1e-4
|
||||
|
||||
|
||||
def test_resolve_wall_insulation_lambda_absent_uses_default() -> None:
|
||||
# Arrange — no lodged conductivity → RdSAP 10 §5.8 default 0.04 W/m·K.
|
||||
from domain.sap10_ml.rdsap_uvalues import (
|
||||
_resolve_wall_insulation_lambda_w_per_mk,
|
||||
)
|
||||
|
||||
# Act
|
||||
lam = _resolve_wall_insulation_lambda_w_per_mk(None)
|
||||
|
||||
# Assert
|
||||
assert abs(lam - 0.04) <= 1e-9
|
||||
|
||||
|
||||
def test_resolve_wall_insulation_lambda_unknown_string_uses_default() -> None:
|
||||
# Arrange — a non-numeric "Unknown" lodgement defers to the default.
|
||||
from domain.sap10_ml.rdsap_uvalues import (
|
||||
_resolve_wall_insulation_lambda_w_per_mk,
|
||||
)
|
||||
|
||||
# Act
|
||||
lam = _resolve_wall_insulation_lambda_w_per_mk("Unknown")
|
||||
|
||||
# Assert
|
||||
assert abs(lam - 0.04) <= 1e-9
|
||||
|
||||
|
||||
def test_resolve_wall_insulation_lambda_code_1_is_default_mineral_wool() -> None:
|
||||
# Arrange — code 1 = the §5.8 default λ=0.04 (mineral wool / EPS);
|
||||
# cert 2130 Ext1 lodges this. Numeric-string form resolves identically.
|
||||
from domain.sap10_ml.rdsap_uvalues import (
|
||||
_resolve_wall_insulation_lambda_w_per_mk,
|
||||
)
|
||||
|
||||
# Act
|
||||
lam_int = _resolve_wall_insulation_lambda_w_per_mk(1)
|
||||
lam_str = _resolve_wall_insulation_lambda_w_per_mk("1")
|
||||
|
||||
# Assert
|
||||
assert abs(lam_int - 0.04) <= 1e-9
|
||||
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
|
||||
|
||||
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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue