diff --git a/domain/sap10_ml/rdsap_uvalues.py b/domain/sap10_ml/rdsap_uvalues.py index 33b9741c..26e4f611 100644 --- a/domain/sap10_ml/rdsap_uvalues.py +++ b/domain/sap10_ml/rdsap_uvalues.py @@ -567,23 +567,25 @@ 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. - # - # 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). + # RdSAP 10 §5.6 (PDF p.40) — uninsulated stone wall formula, age + # bands A-E. Fires only when (a) a documentary wall thickness is lodged + # (per §5.3 documentary-evidence rule) AND (b) the insulation STATE is + # known (`wall_insulation_type` not None — As Built / external / internal). + # When either is absent the cascade falls through to the Table-6 + # typical-thickness default (1.7) below: an "insulation Unknown" lodgement + # is NOT treated as bare stone of the lodged thickness (cert 000565 Ext1: + # granite 50 mm + insulation Unknown → Table-6 1.70 in the worksheet, NOT + # the §5.6 formula's 6.09), and the footnote (a) "use §5.6 if the + # calculated U-value is less than 1.7" clause governs the unknown path. When the thickness IS + # lodged the raw §5.6 U is the spec target — it is NOT capped at 1.7, + # because a thin/standard solid stone wall genuinely loses more than the + # typical-thickness default (sandstone 400 mm → 1.90, granite 120 mm → + # 3.89). §5.8 + Table 14 insulation / dry-line adjustments apply on top + # of the raw §5.6 U₀ (cert 000565 Main alt_wall_1: granite W=120 mm + # dry-lined → U₀=3.88 + dry-line → 2.34, matches worksheet). if ( wall_thickness_mm is not None + and wall_insulation_type is not None and band in _STONE_AGE_A_TO_E and construction in (WALL_STONE_GRANITE, WALL_STONE_SANDSTONE) ): @@ -628,8 +630,15 @@ def u_wall( Decimal("0.01"), rounding=ROUND_HALF_UP ) ) - if u0 >= 1.7: - return 1.7 # Table-6 row cap per footnote (a) + # As-built (uninsulated, not dry-lined) stone wall of KNOWN + # thickness, age A-E: return the raw §5.6 result. The Table-6 + # footnote (a) "< 1.7" clause governs the UNKNOWN-thickness path + # (which falls through to the Table-6 typical 1.7 default below) — + # NOT a documentary-thickness lodgement. A thin solid stone wall + # genuinely has U > 1.7 (e.g. sandstone 400 mm = 1.90, granite + # 120 mm = 3.89); capping it to 1.7 under-counts fabric loss and + # over-rates. Confirmed against Elmhurst (age-B sandstone 400 mm + # → 1.9) and the §5.6 Table-12 formula tests. return u0 known_types = { WALL_STONE_GRANITE, WALL_STONE_SANDSTONE, WALL_SOLID_BRICK, WALL_CAVITY, diff --git a/tests/infrastructure/epc_client/test_sap_accuracy_corpus.py b/tests/infrastructure/epc_client/test_sap_accuracy_corpus.py index 7b3524f5..2f454359 100644 --- a/tests/infrastructure/epc_client/test_sap_accuracy_corpus.py +++ b/tests/infrastructure/epc_client/test_sap_accuracy_corpus.py @@ -150,14 +150,27 @@ _CORPUS = Path( # MAE 0.12 -> 0.08 t/yr (bias +0.04 -> 0.00). A prior session deferred enum 9 # ("direction not understood") while the PE/CO2 lens was confounded by the # climate-cascade bug (fc7c4d2d); the corrected lens shows the over-rate. +# UNINSULATED STONE WALL §5.6 FORMULA (RdSAP 10 §5.6 Table 12, PDF p.40): a +# stone wall of KNOWN thickness whose insulation STATE is known (As Built / +# external / internal) is billed by the §5.6 formula on its lodged thickness +# (sandstone/limestone U = 54.876·W^-0.561, granite/whinstone 45.315·W^-0.513), +# NOT capped at the Table-6 typical-thickness 1.7. The old `if u0>=1.7: 1.7` +# cap nullified the formula for every real-thickness stone wall (it only dips +# below 1.7 past ~488 mm sandstone / ~640 mm granite) and under-counted fabric +# loss → over-rate. Gated on `wall_insulation_type is not None` so an +# "insulation Unknown" wall still falls to the Table-6 default (cert 000565 +# Ext1: granite 50 mm + Unknown → worksheet 1.70, not the formula's 6.09). +# Took within-0.5 70.3% -> 71.6% (MAE 0.833 -> 0.822); fixed the 2 stone-U +# unit tests; worksheet-validated (Elmhurst age-B sandstone 400 mm → 1.90). +# # SAP RATING FLOOR (SAP 10.2 §13 / RdSAP 10 §13): the rating is floored at 1 # ("if the result is less than 1, the rating is 1"). `calculate_sap_from_inputs` # now applies that floor to the CONTINUOUS score too (was integer-only), so a # degenerate dwelling no longer emits a negative SAP. Removed a -12.3 outlier # (cert 422000111926, lodged at the floor of 1, was computing -11.3): within-0.5 # 70.2% -> 70.3%, MAE 0.845 -> 0.833. -_MIN_WITHIN_HALF_SAP = 0.70 -_MAX_SAP_MAE = 0.84 +_MIN_WITHIN_HALF_SAP = 0.71 +_MAX_SAP_MAE = 0.83 _MAX_CO2_MAE_TONNES = 0.09 # t CO2 / yr vs co2_emissions_current _MAX_PE_PER_M2_MAE = 4.0 # kWh / m2 / yr vs energy_consumption_current