diff --git a/docs/sap-spec/HANDOVER_NEXT.md b/docs/sap-spec/HANDOVER_NEXT.md index d3382ebb..f16b6bb2 100644 --- a/docs/sap-spec/HANDOVER_NEXT.md +++ b/docs/sap-spec/HANDOVER_NEXT.md @@ -159,24 +159,29 @@ pumps_fans_kwh_per_yr | ✓ | ✓ | ✓ | ✓ | ✓ | is still off by sub-SAP-point amounts on every fixture — none of `sap_score_ continuous` is closed at abs=1e-4. -### B.3 §3 residuals after slice 24 (rooflight 27a for 000516) +### B.3 §3 residuals after slice 27 (floor U §5.12 rounding) ``` fixture | LINE_31 Δ | LINE_33 Δ | LINE_36 Δ | LINE_37 Δ -000474 | 0.0014 | 0.0296 | 0.0002 | 0.0294 -000477 | 0.0004 | 0.1246 | ✓ | 0.1244 -000480 | 0.0060 | 0.0168 | 0.0009 | 0.0177 +000474 | 0.0014 | 0.0032 | 0.0002 | 0.0030 +000477 | 0.0004 | 0.0013 | ✓ | 0.0013 +000480 | 0.0060 | 0.0075 | 0.0009 | 0.0084 000487 | 8.82 | 37.88 | 1.32 | 39.21 -000490 | 0.0010 | 0.0282 | 0.0002 | 0.0284 +000490 | 0.0010 | 0.0013 | 0.0002 | 0.0014 000516 | 0.0025 | 0.0038 | 0.0004 | 0.0042 ``` -5 of 6 fixtures have §3 residuals under 0.2 W/K. 000516's 0.82 W/K rooflight -gap was closed by slice 24 — line (27a) is now in the cascade. The residual -0.0038 W/K on 000516 LINE_33 is the same pre-existing wall-perimeter + -per-window curtain precision that's biting 000474/477/480/490 (slice 27 -territory). 000487's huge gaps are the RR fixture defect + the U=0.86 -external-gable variant our `gable_wall` enum doesn't handle. +5 of 6 fixtures now have §3 LINE_33 residuals under 0.01 W/K. Slice 27 +applied the §5.12 mandated 2-d.p. rounding to BS EN ISO 13370 floor +U-values, closing 90–95% of the residual on 000474/477/490 (000516 is +exposed-floor only; 000487 still has the RR defect). + +Remaining 0.0013–0.0075 W/K comes from wall + party-wall area precision +(my calc carries `36.4492 m²` where the PDF stores `36.4500` — clearly +2-d.p. rounded). The spec page for §3 element-area rounding hasn't been +read; if a "round to 0.01 m²" rule exists for §3 areas, applying it +would close these. 000487's huge gaps are the RR fixture defect + the +U=0.86 external-gable variant our `gable_wall` enum doesn't handle. ### B.4 §4 residuals @@ -193,6 +198,7 @@ fixture | section §4 pin status ### B.5 Recent slices (in reverse order — newest first) ``` +Slice 27: BS EN ISO 13370 floor U rounded to 2 d.p. per RdSAP10 §5.12 Slice 24: rooflight (line 27a) — SapRoofWindow datatype + 000516 cascade closure ac68cf88 Slice 23: 000516 detailed RR + exposed_floor + door_count fixture lodgement 6be8fdb7 Slice 22: per-window curtain resistance fix (mixed glazing) @@ -272,17 +278,30 @@ extraction commands (sample for §9a): awk '/^9a\. Energy requirements/,/^10a\./' "sap worksheets/U985-0001-NNNNNN.txt" ``` -### C.4 Slice 27 — Floor-U precision (≈0.04 W/m²K drift on 4 fixtures) +### C.4 Slice 27 — ~~Floor-U precision~~ DONE (mostly) -After slices 22 + 23 closed window + RR contributions, 4 fixtures -(000474/477/480/490) have residual ~0.03-0.13 W/K on LINE_33 traced to floor U -drift: -- 000477: calc 0.5261 vs PDF 0.5300 for suspended timber + age B + 31.26 m² - ground floor. -- Likely the BS EN ISO 13370 ground-contact formula vs PDF Table 19 lookup. +Done. The §5.12 spec mandates "rounded to two decimal places" for BS EN ISO +13370 floor U-values, which my calc was skipping. Applied `round(U, 2)` to +both suspended-timber and solid-floor branches in `u_floor` — closed +000474/477/490 from ~0.03–0.13 W/K residual to under 0.002 W/K on each. -Diagnose what the PDF uses for these (probably a tabulated value, not a -formula) and align the calc. +Remaining 0.0013–0.0075 W/K residual is wall + party-wall area precision — +PDF stores 2-d.p.-rounded element areas (e.g. `36.4500 m²` for a wall I +compute as `36.4492 m²`). Closing these needs the §3 area-rounding spec +rule — see slice 27b below. + +### C.4b Slice 27b — Wall + party-wall area precision (PDF rounds to 2 d.p.) + +The 0.0013–0.0075 W/K LINE_33 residual on 000474/477/480/490/516 is +consistently traceable to gross wall-area and party-wall-area values: +- 000474 Main wall area: my 36.4492 vs PDF 36.4500 (Δ × 1.5 U = 0.0012 W/K) +- 000516 wall area: my 45.3675 vs PDF 45.3700 (Δ × 1.5 U = 0.00375 W/K) +- per-window U_eff aggregation: my per-window curtain transform diverges + from PDF's aggregate by ~0.0001 per fixture (slice 22 trade-off) + +If §3 mandates area rounding to 0.01 m² (or 4 d.p.) at the element level, +applying it would close LINE_33 to ≤ 1e-4. Need SAP 10.2 §3 page reference +from the user. ### C.5 Slice 28 — Continuous SAP / fuel cost / CO2 closure diff --git a/packages/domain/src/domain/ml/rdsap_uvalues.py b/packages/domain/src/domain/ml/rdsap_uvalues.py index 3716a30b..c4559c55 100644 --- a/packages/domain/src/domain/ml/rdsap_uvalues.py +++ b/packages/domain/src/domain/ml/rdsap_uvalues.py @@ -681,10 +681,11 @@ def u_floor( wall_thickness_mm: Optional[int], description: Optional[str] = None, ) -> float: - """RdSAP10 ground-floor U-value via BS EN ISO 13370 solid-floor branch. - - Suspended-floor branch is approximated as solid since the difference at - the feature-engineering granularity is < 0.1 W/m^2K for typical UK floors. + """RdSAP10 ground-floor U-value via BS EN ISO 13370 (suspended or solid + branch, per Table 19 footnote 1). Result is rounded to 2 d.p. per spec + §5.12 ("Unless provided by the assessor the floor U-value is calculated + according to BS EN ISO 13370 using its area (A) and exposed perimeter + (P) and rounded to two decimal places."). `description` is the joined surveyor text from `floors[i].description`. When it asserts retrofit insulation ("Solid, insulated (assumed)" / @@ -698,8 +699,7 @@ def u_floor( Full-SAP assessments lodge a measured floor U-value directly in the description ("Average thermal transmittance X W/m²K"); when present this supersedes the BS EN ISO 13370 calculation per spec - §5.12 opening clause ("Unless provided by the assessor the floor - U-value is calculated according to BS EN ISO 13370"). + §5.12 opening clause. """ measured = _measured_u_from_description(description) if measured is not None: @@ -735,18 +735,18 @@ def u_floor( else (construction is None and band_upper in _SUSPENDED_TIMBER_DEFAULT_BANDS) ) if use_suspended_branch: - return _u_floor_suspended( + return round(_u_floor_suspended( area_m2=area_m2, perimeter_m=perimeter_m, wall_thickness_mm=wall_thickness_mm, insulation_thickness_mm=ins_mm or 0, - ) + ), 2) r_f = ((ins_mm or 0) / 1000.0) / 0.035 d_t = w + soil_g * (r_si + r_f + r_se) b = 2.0 * area_m2 / perimeter_m if d_t < b: - return 2.0 * soil_g * log(pi * b / d_t + 1.0) / (pi * b + d_t) - return soil_g / (0.457 * b + d_t) + return round(2.0 * soil_g * log(pi * b / d_t + 1.0) / (pi * b + d_t), 2) + return round(soil_g / (0.457 * b + d_t), 2) # ---------------------------------------------------------------------------