mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice S0380.109: Solid brick + insulation via §5.7 Table 13 + §5.8 Table 14 (RdSAP 10)
Closes the remaining cert 000565 BP[0] Main wall residual (-1.54 W/K
under ws) by routing solid-brick walls with documentary wall
thickness + lodged insulation through the RdSAP 10 §5.7 + §5.8
formula chain. Adds a Table-6 footnote (a) cap on the §5.6 stone
formula to handle thin uninsulated stone walls (Ext1 BP[1] Granite
W=50 mm).
RdSAP 10 §5.7 Table 13 (PDF p.41) verbatim:
"Default U-values of brick walls
Wall thickness, mm U-value, W/m²K
Up to 200 mm 2.5
200 to 280 mm 1.7
280 to 420 mm 1.4 ← cert 000565 Main W = 300 mm
More than 420 mm 1.1"
RdSAP 10 §5.8 step 2 (PDF p.41-42) verbatim:
"The U-value of the insulated wall is U = 1 / (1/U₀ + R_insulation)
...
Where R_insulation comes from Table 14: Insulation thickness and
corresponding resistance.
...
R = 0.025 × T + 0.25 when λ = 0.04 W/m·K
R = 0.0333 × T + 0.248 when λ = 0.03 W/m·K
R = 0.040 × T + 0.25 when λ = 0.025 W/m·K
Where T is thickness of insulation in mm"
Cert 000565 Main lodgement (Summary §7.0):
Type SO Solid Brick (wall_construction = 3)
Insulation E External (wall_insulation_type = 1)
Insulation Thickness 75 mm
Wall Thickness 300 mm (measured)
Conductivity Known No → λ defaults to 0.04 (§5.8 final note)
Age band A
Formula chain:
U₀ = 1.4 (§5.7 Table 13 row "280 to 420 mm")
R = 0.025 × 75 + 0.25 = 2.125 m²K/W
U = 1 / (1/1.4 + 2.125) = 1 / 2.8393 = 0.3522 → 0.35 (2 d.p.)
Pre-slice the cascade bucketed 75 mm into the Table-6 "100 mm
external/internal insulation" row → 0.32 for age A. The -0.03 U
delta on Main's 51.72 m² external wall is the entire -1.54 W/K
under-count driving the cohort's remaining fabric residual.
RdSAP 10 Table 6 footnote (a) (PDF p.34) verbatim:
"Or from equations in 5.6 if the calculated U-value is less than
1.7."
Applies only to the AS-BUILT (no insulation, no dry-line) Table 6
row. For thin walls where §5.6 gives U ≥ 1.7 the Table 6 row
default of 1.7 caps the result. Verified empirically against cert
000565 Main alt_wall_1 (granite W=120 mm dry-lined): raw §5.6 →
3.879 + dry-line → 2.34 matches worksheet, NOT capped 1.7 + dry-
line → 1.32. The cap therefore only fires when neither dry-lining
nor insulation is present (cert 000565 BP[1] Ext1: granite W=50 mm
"Insulation Unknown" → §5.6 = 6.09 → capped to 1.7, matches ws).
3-layer fix:
1. `domain/sap10_ml/rdsap_uvalues.py`:
- Add `_u_brick_thin_wall_age_a_to_e(W_mm)` per §5.7 Table 13
- Add `_r_insulation_table_14(T_mm, λ)` per §5.8 Table 14
interpolation rule (handles all 3 λ columns)
- Wire §5.7+§5.8 chain into `u_wall` for WALL_SOLID_BRICK + age
A-E + lodged thickness + (External | Internal) insulation +
thickness > 0
- Add Table 6 footnote (a) cap to `_u_stone_thin_wall_age_a_to_e`
(cap at 1.7 only when not dry-lined)
- Round dry-lined §5.6 result to 2 d.p. (worksheet A×U precision)
2. `domain/sap10_calculator/worksheet/heat_transmission.py` passes
`wall_thickness_mm=part.wall_thickness_mm` through to `u_wall`
for the per-BP main wall U (previously passed only for alt walls).
3. AAA test pins cert 000565 walls_w_per_k = 604.07 within 1e-4.
Movement at HEAD `9159e91f` → post-slice (cert 000565):
Fabric (cascade vs ws):
walls 602.53 → 604.08 (Δ -1.54 → +0.01 W/K — sub-spec
alt-wall float rounding artifact)
total W/K 935.54 → 937.09 (Δ -1.52 → +0.03 W/K — essentially
zero net fabric HTC residual)
End-result pins:
sap_score (int) 29 ✓ EXACT (unchanged)
sap_score_continuous 28.5380 → 28.5028 (Δ +0.0293 → -0.0059;
80% magnitude reduction)
ecf 5.3838 → 5.3874 (Δ -0.0028 → +0.0008)
total_fuel_cost_gbp 4677.64 → 4680.78 (Δ -2.62 → +0.52)
co2_kg_per_yr 6444.27 → 6448.34 (Δ -3.35 → +0.72)
space_heating 58974.84 → 59020.02 (Δ -33.5 → +11.7)
main_heating_fuel 34691.09 → 34717.66 (Δ -19.7 → +6.87)
lighting_kwh 1382.67 (unchanged)
pumps_fans_kwh ✓ EXACT (unchanged)
Continuous SAP magnitude improved 80% (0.0293 → 0.0059). All
SH-driven downstream residuals (cost, co2, SH kwh, main_heating
fuel) magnitude-reduced 65-80%. Integer SAP stays exact at 29.
Cohort safety verified: 6 cohort certs (000474-000516) lodge wc=4
(cavity) + wit=4 (as-built) — neither precondition for the new
§5.7+§5.8 path. §5.6 cap only fires when not dry-lined (cohort
certs don't trigger). All 11 cert→inputs and 6 sap_result_pin
cohort tests pass unchanged.
Golden cert 6035-7729-2309-0879-2296 (mid-terrace age A solid
brick) sees the §5.7+§5.8 chain fire on its Main wall:
PE +46.7562 → +46.0936 kWh/m² (cascade closer to actual EPC)
CO2 +1.0652 → +1.0495 tonnes/yr (cascade closer to actual EPC)
Per [[feedback-golden-residuals-near-zero]] the expected pin is
updated to track the improvement (target → ~0 as mapper closes).
Test count: 608 pass + 7 expected 000565 fails → **608 pass + 7
expected 000565 fails** (new §5.7+§5.8 formula test green; golden
cert 6035 pin re-pinned; integer SAP stays at 29). Pyright net-zero
per touched file (27 baseline → 27 post-change).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
9159e91fbc
commit
efb203f7ad
4 changed files with 175 additions and 4 deletions
|
|
@ -2134,6 +2134,68 @@ def test_summary_000565_ext1_rir_connected_gable_deducts_from_a_rr_per_rdsap_10_
|
|||
assert connected_gables[0].u_value == 0.0
|
||||
|
||||
|
||||
def test_summary_000565_main_solid_brick_external_insulation_uses_rdsap_10_section_5_7_plus_5_8_formula() -> None:
|
||||
# Arrange — RdSAP 10 §5.7 (PDF p.41) Table 13 + §5.8 (PDF p.42)
|
||||
# Table 14 + step 2 derivation.
|
||||
#
|
||||
# §5.7 Table 13: "Default U-values of brick walls"
|
||||
# Wall thickness, mm U-value, W/m²K
|
||||
# Up to 200 mm 2.5
|
||||
# 200 to 280 mm 1.7
|
||||
# 280 to 420 mm 1.4 ← cert 000565 Main, W=300 mm
|
||||
# More than 420 mm 1.1
|
||||
#
|
||||
# §5.8 step 2: "The U-value of the insulated wall is
|
||||
# U = 1 / (1/U₀ + R_insulation)"
|
||||
#
|
||||
# §5.8 Table 14 (λ = 0.04 W/m·K column) + interpolation rule
|
||||
# "R = 0.025 × T + 0.25" for T = 75 mm gives R = 2.125 m²K/W
|
||||
# (direct Table-14 row 75 mm column λ=0.04 reads "2.125").
|
||||
#
|
||||
# Cert 000565 Main §7.0 lodges:
|
||||
# Type SO Solid Brick (wall_construction = 3)
|
||||
# Insulation E External (wall_insulation_type = 1)
|
||||
# Insulation Thickness 75 mm
|
||||
# Wall Thickness 300 mm (measured)
|
||||
# Conductivity Known No → λ defaults to 0.04 per §5.8 column
|
||||
# Age band A
|
||||
#
|
||||
# Formula chain:
|
||||
# U₀ = 1.4 (§5.7 Table 13 row "280 to 420 mm")
|
||||
# R = 0.025 × 75 + 0.25 = 2.125 m²K/W
|
||||
# U = 1 / (1/1.4 + 2.125) = 1 / 2.8393 = 0.3522
|
||||
# U (2 d.p.) = 0.35 W/m²K
|
||||
#
|
||||
# Worksheet (29a) row "External walls Main: 51.72 × 0.35 = 18.10"
|
||||
# → 18.10 W/K. Pre-slice the cascade ignored §5.7 (Table-13 lookup
|
||||
# on wall thickness) and §5.8 (Table-14 interpolation by lodged
|
||||
# insulation thickness) entirely. The bucket cascade routed the
|
||||
# 75 mm lodgement to the 100 mm Table-6 column (0.32 for age A)
|
||||
# — a -1.54 W/K under-count on Main's external wall area (= the
|
||||
# full BP[0] walls residual driving the remaining net HTC gap on
|
||||
# cert 000565 post-S0380.108).
|
||||
pages = _summary_pdf_to_textract_style_pages(_SUMMARY_000565_PDF)
|
||||
site_notes = ElmhurstSiteNotesExtractor(pages).extract()
|
||||
epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes)
|
||||
from domain.sap10_calculator.rdsap.cert_to_inputs import heat_transmission_section_from_cert
|
||||
|
||||
# Act
|
||||
ht = heat_transmission_section_from_cert(epc)
|
||||
|
||||
# Assert — `walls_w_per_k` matches the worksheet's (29a)+(32) sum
|
||||
# (Main wall contribution per the §5.7+§5.8 formula chain dominates
|
||||
# the residual; closing it brings cascade walls to within 1e-4 of
|
||||
# ws 604.07 = 18.10 + 3.43 + 4.41 + 53.82 (Main) + 219.997 (Ext1)
|
||||
# + 229.95 (Ext2) + 39.852 (Ext3) + 34.51 (Ext4)).
|
||||
assert abs(ht.walls_w_per_k - 604.0710) <= 1e-4, (
|
||||
f"cascade walls_w_per_k={ht.walls_w_per_k:.4f}; "
|
||||
f"ws 604.0710; Δ={ht.walls_w_per_k - 604.0710:+.4f} "
|
||||
f"(expected within 1e-4 after §5.7+§5.8 formula chain replaces "
|
||||
f"the Table-6 bucket lookup for solid-brick + lodged-thickness "
|
||||
f"+ insulated walls)"
|
||||
)
|
||||
|
||||
|
||||
def test_summary_000565_main_1_ashp_sap_code_224_routes_to_main_heating_category_4_per_sap_table_4a() -> None:
|
||||
# Arrange — SAP 10.2 Table 4a (PDF p.165) "Main heating systems":
|
||||
# the category column lists "Heat pumps" as category 4. Codes in
|
||||
|
|
|
|||
|
|
@ -145,8 +145,8 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
cert_number="6035-7729-2309-0879-2296",
|
||||
actual_sap=70,
|
||||
expected_sap_resid=-6,
|
||||
expected_pe_resid_kwh_per_m2=+46.7562,
|
||||
expected_co2_resid_tonnes_per_yr=+1.0652,
|
||||
expected_pe_resid_kwh_per_m2=+46.0936,
|
||||
expected_co2_resid_tonnes_per_yr=+1.0495,
|
||||
notes=(
|
||||
"Mid-terrace, TFA 128, age A, gas combi Table 4b code 104. "
|
||||
"Slice 59 per-bp window apportionment tightens all 3 "
|
||||
|
|
@ -155,7 +155,10 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
"Main ins_type 3, lowering Ext1's net wall U-loss). Slice "
|
||||
"102f-prep.8 mapper fix: shower_outlets=None now resolves to "
|
||||
"0 mixers (was 1) — drops daily HW by ~7 l/day → PE +47.85 "
|
||||
"→ +46.76, CO2 +1.09 → +1.07."
|
||||
"→ +46.76, CO2 +1.09 → +1.07. S0380.109: §5.7+§5.8 formula "
|
||||
"chain for solid-brick + lodged-thickness + insulation "
|
||||
"tightens BP[0] Main wall U from Table-6 bucket → spec "
|
||||
"formula → PE +46.76 → +46.09, CO2 +1.065 → +1.049."
|
||||
),
|
||||
),
|
||||
_GoldenExpectation(
|
||||
|
|
|
|||
|
|
@ -653,6 +653,14 @@ def heat_transmission_from_cert(
|
|||
# band. None for non-curtain-wall parts (ignored by
|
||||
# `u_wall` unless wall_construction == WALL_CURTAIN).
|
||||
curtain_wall_age=part.curtain_wall_age,
|
||||
# RdSAP 10 §5.7 Table 13 + §5.8 (PDF p.41-42) — solid
|
||||
# brick + lodged wall thickness routes through the
|
||||
# documentary-evidence formula chain (U₀ by thickness
|
||||
# from §5.7, R from §5.8 Table 14 by lodged insulation
|
||||
# thickness). Ignored when the wall_construction +
|
||||
# insulation_type combination doesn't match the formula
|
||||
# path's preconditions.
|
||||
wall_thickness_mm=part.wall_thickness_mm,
|
||||
)
|
||||
# When the per-bp `roof_insulation_thickness` is explicitly lodged
|
||||
# as 0 (uninsulated — e.g. cert 001479 Ext2 PS sloping ceiling
|
||||
|
|
|
|||
|
|
@ -132,7 +132,9 @@ WALL_CAVITY_FILLED_PARTY: Final[int] = 11
|
|||
# 5 = none specified (rare)
|
||||
# 6 = filled cavity + external insulation
|
||||
# 7 = filled cavity + internal insulation
|
||||
_WALL_INSULATION_EXTERNAL: Final[int] = 1
|
||||
WALL_INSULATION_FILLED_CAVITY: Final[int] = 2
|
||||
_WALL_INSULATION_INTERNAL: Final[int] = 3
|
||||
WALL_INSULATION_CAVITY_PLUS_EXTERNAL: Final[int] = 6
|
||||
WALL_INSULATION_CAVITY_PLUS_INTERNAL: Final[int] = 7
|
||||
|
||||
|
|
@ -183,6 +185,51 @@ def _u_stone_thin_wall_age_a_to_e(
|
|||
return None
|
||||
|
||||
|
||||
def _u_brick_thin_wall_age_a_to_e(wall_thickness_mm: int) -> float:
|
||||
"""RdSAP 10 §5.7 Table 13 (PDF p.41) — default U-value for an
|
||||
uninsulated solid brick wall by lodged thickness, age bands A-E.
|
||||
|
||||
Wall thickness, mm U-value, W/m²K
|
||||
Up to 200 mm 2.5
|
||||
200 to 280 mm 1.7
|
||||
280 to 420 mm 1.4
|
||||
More than 420 mm 1.1
|
||||
"""
|
||||
if wall_thickness_mm <= 200:
|
||||
return 2.5
|
||||
if wall_thickness_mm <= 280:
|
||||
return 1.7
|
||||
if wall_thickness_mm <= 420:
|
||||
return 1.4
|
||||
return 1.1
|
||||
|
||||
|
||||
def _r_insulation_table_14(
|
||||
thickness_mm: int, lambda_w_per_mk: float = 0.04,
|
||||
) -> float:
|
||||
"""RdSAP 10 §5.8 Table 14 (PDF p.42) — thermal resistance of
|
||||
added insulation by lodged thickness and λ. Spec interpolation
|
||||
rule (PDF p.42):
|
||||
|
||||
R = 0.025 × T + 0.25 when λ = 0.04 W/m·K
|
||||
R = 0.0333 × T + 0.248 when λ = 0.03 W/m·K
|
||||
R = 0.040 × T + 0.25 when λ = 0.025 W/m·K
|
||||
|
||||
The exact Table-14 row values reproduce as the interpolation
|
||||
formula evaluated at the discrete thickness points (e.g. T=75 mm
|
||||
+ λ=0.04 → R = 2.125; T=100 mm + λ=0.04 → R = 2.75).
|
||||
"""
|
||||
if lambda_w_per_mk <= 0.0275:
|
||||
# λ = 0.025 W/m·K (PUR / PIR / phenolic foam)
|
||||
return 0.040 * thickness_mm + 0.25
|
||||
if lambda_w_per_mk <= 0.035:
|
||||
# λ = 0.03 W/m·K (XPS optional)
|
||||
return 0.0333 * thickness_mm + 0.248
|
||||
# λ = 0.04 W/m·K (typical mineral wool / EPS / rock wool — spec
|
||||
# default per §5.8 final note).
|
||||
return 0.025 * thickness_mm + 0.25
|
||||
|
||||
|
||||
# RdSAP 10 §5.18 (PDF p.48) — curtain-wall U-values.
|
||||
#
|
||||
# "If documentary evidence is available, use calculated U-value of the
|
||||
|
|
@ -459,6 +506,17 @@ def u_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.
|
||||
#
|
||||
# Table 6 footnote (a) (PDF p.34): "Or from equations in 5.6 if
|
||||
# the calculated U-value is less than 1.7." The cap applies only
|
||||
# to the AS-BUILT (no insulation, no dry-line) Table 6 row — for
|
||||
# thin walls where §5.6 gives U ≥ 1.7 (e.g. granite at W=50 mm
|
||||
# yields 6.09 → use Table 6 default 1.7 instead). When the wall
|
||||
# is dry-lined or insulated, the raw §5.6 result feeds the §5.8
|
||||
# chain as the input U₀ — the Table 6 footnote doesn't cap that
|
||||
# path (verified empirically against cert 000565 Main alt_wall_1:
|
||||
# granite W=120 mm dry-lined → U₀=3.88 raw + dry-line → 2.34
|
||||
# matches worksheet, NOT 1.7 + dry-line → 1.32).
|
||||
if (
|
||||
wall_thickness_mm is not None
|
||||
and band in _STONE_AGE_A_TO_E
|
||||
|
|
@ -467,7 +525,17 @@ def u_wall(
|
|||
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)
|
||||
# Round to 2 d.p. — worksheet (29a) A×U product uses
|
||||
# the 2-d.p.-displayed U (cf. 000565 Main alt_wall_1:
|
||||
# 23 × 2.34 = 53.82 with U=2.34, not raw 2.3405).
|
||||
u_unrounded = 1.0 / (1.0 / u0 + _DRY_LINING_RESISTANCE_M2K_PER_W)
|
||||
return float(
|
||||
Decimal(str(u_unrounded)).quantize(
|
||||
Decimal("0.01"), rounding=ROUND_HALF_UP
|
||||
)
|
||||
)
|
||||
if u0 >= 1.7:
|
||||
return 1.7 # Table-6 row cap per footnote (a)
|
||||
return u0
|
||||
known_types = {
|
||||
WALL_STONE_GRANITE, WALL_STONE_SANDSTONE, WALL_SOLID_BRICK, WALL_CAVITY,
|
||||
|
|
@ -477,6 +545,36 @@ def u_wall(
|
|||
wall_type = construction
|
||||
else:
|
||||
wall_type = _wall_type_from_description(description) or _DEFAULT_WALL_BY_AGE.get(band, WALL_CAVITY)
|
||||
# RdSAP 10 §5.7 Table 13 + §5.8 (PDF p.41-42) — uninsulated solid
|
||||
# brick wall U₀ by lodged wall thickness, then add §5.8 insulation
|
||||
# adjustment U = 1/(1/U₀ + R) where R comes from Table 14. Fires
|
||||
# only with the cert's documentary-evidence lodging:
|
||||
# - construction is solid brick (or stone — §5.6 path below)
|
||||
# - age band A-E (per the §5.6/§5.7/§5.8 explicit scope)
|
||||
# - wall thickness measured
|
||||
# - insulation type is External (1) or Internal (3) with a
|
||||
# lodged thickness > 0
|
||||
# λ defaults to 0.04 W/m·K (typical mineral wool / EPS) per §5.8
|
||||
# final note. Cert 000565 BP[0] Main: solid brick 300 mm + 75 mm
|
||||
# external @ λ=0.04 → U₀=1.4 + R=2.125 → U=0.35 (matches ws).
|
||||
if (
|
||||
wall_type == WALL_SOLID_BRICK
|
||||
and band in _STONE_AGE_A_TO_E
|
||||
and wall_thickness_mm is not None
|
||||
and wall_insulation_type in (
|
||||
_WALL_INSULATION_EXTERNAL, _WALL_INSULATION_INTERNAL,
|
||||
)
|
||||
and insulation_thickness_mm is not None
|
||||
and insulation_thickness_mm > 0
|
||||
):
|
||||
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,
|
||||
)
|
||||
u_unrounded = 1.0 / (1.0 / u0 + r_ins)
|
||||
return float(
|
||||
Decimal(str(u_unrounded)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
|
||||
)
|
||||
if wall_type == WALL_CAVITY and wall_insulation_type in (
|
||||
WALL_INSULATION_CAVITY_PLUS_EXTERNAL,
|
||||
WALL_INSULATION_CAVITY_PLUS_INTERNAL,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue