mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice S0380.95: Detailed-RR residual area cascade per RdSAP 10 §3.10.1
RdSAP 10 §3.10.1 (PDF p.24) "Default U-values of the roof rooms":
> "The residual area (area of roof less the floor area of room(s)-in-
> roof) has a U-value from Table 16 : Roof U-values when loft
> insulation thickness is known according to its insulation thickness
> if at least half the area concerned is accessible, otherwise it is
> the default for the age band of the original property or extension."
Plus RdSAP 10 §3.9.1 step (d-e) (PDF p.21-22) — the Simplified A_RR
formula `12.5 × √(A_RR_floor / 1.5)` is the empirical estimator for
the total RR exposed shell; residual = A_RR − Σ lodged walls. The
worksheet applies this same formula to Detailed mode when the lodged
surface set has no roof-going entries (cert 000565 BP[0]:
12.5 × √(45/1.5) − (9.8 + 14.7) = 43.96 ≈ ws 43.97).
Pre-slice the cascade computed residual area ONLY in the Simplified
RR branch (via `_part_geometry`'s `rr_simplified_a_rr_m2` − rr_common
− rr_gable subtractions). The Detailed-RR branch in
`heat_transmission` iterated `rir.detailed_surfaces` and missed the
residual entirely. Cert 000565 routes all 5 BPs through Detailed mode
(the Elmhurst mapper translates Summary "Simplified" lodgements to
`SapRoomInRoofSurface` records when per-surface L×H is present), so
cascade total_external_element_area_m2 was 779.27 m² vs worksheet
(31) = 857.64 m² (Δ −78.37 m² → thermal_bridging cascade −11.76 W/K
under).
Slice span (1 file):
- `heat_transmission.py`: Detailed-RR branch adds residual area via
the §3.9.1 A_RR formula minus wall-going lodgements (gable_wall,
gable_wall_external, common_wall). Residual area contributes to
`rr_detailed_area` (→ part_external_area → (31) → thermal_bridging
multiplier) and to `roof` at `u_rr_default_all_elements`.
- Discriminator: residual fires only when no roof-going surface kinds
(slope, flat_ceiling, stud_wall) are lodged — true Detailed-mode
lodgements (cohort fixture 000516) lodge the entire roof shell
explicitly and have no residual.
Cert 000565 movement (HEAD `78c57c0d` → this slice):
- thermal_bridging_w_per_k: 116.89 → 129.35 ✓ vs ws 128.65 (Δ +0.70)
- total_external_area_m2: 779.27 → 862.34 ✓ vs ws 857.64 (Δ +4.70)
- roof_w_per_k: 34.64 → 63.72 (Δ −16.74 → +12.34)
- sap_score_continuous: 29.02 → 28.07 (Δ +0.51 → −0.44)
- sap_score (integer): 29 → 28 (temp regression
past 28.5 threshold)
- space_heating_kwh: −685 → +533
- main_heating_fuel: −403 → +321
- hot_water_kwh: ✓ 0 EXACT unchanged
Per user direction temporary continuous-SAP drift is acceptable when
fixing real spec-correct sub-component bugs; the absolute continuous-
SAP residual is now −0.44 (was +0.51) — slightly closer to zero
overall. The roof overshoot localises to:
- BP[4] Flat Ceiling 1 "Unknown PUR or PIR" lodgement (cascade 2.30
vs ws 0.15, over by +10.75 W/K) — Elmhurst-specific "Unknown +
known material" convention not yet wired
- BP[1] residual formula gives +3.68 m² over worksheet (Δ +1.29 W/K)
— Detailed-mode residual is spec-ambiguous for extensions with
non-2.45 m RR height; future slice may add a height-aware formula
Cohort safety: discriminator `has_roof_lodgement` filters out true
Detailed-mode lodgements (cohort fixtures 000474/000477/000480/
000487/000490/000516 all lodge slope/flat_ceiling/stud_wall surfaces).
Initial implementation broke 41 cohort pins; the discriminator
restores cohort behaviour exactly. Test baseline: 585 pass + 9
expected `000565` fails (was 585 + 8 — sap_score moved from passing
to failing during the slice's transient overshoot; expected per
user direction).
Pyright net-zero per touched file (test_summary_pdf_mapper_chain.py
13 → 13 preserved; heat_transmission.py 13 → 12 improved by −1).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
78c57c0dc7
commit
fa6974bdd9
2 changed files with 87 additions and 1 deletions
|
|
@ -1650,6 +1650,48 @@ def test_summary_000565_section_12_1_extracts_mechanical_extract_decentralised_m
|
|||
)
|
||||
|
||||
|
||||
def test_summary_000565_detailed_rr_residual_area_closes_total_external_area_per_rdsap_10_section_3_10_1() -> None:
|
||||
# Arrange — RdSAP 10 §3.10.1 (PDF p.24) "Default U-values of the
|
||||
# roof rooms":
|
||||
# "The residual area (area of roof less the floor area of room(s)-
|
||||
# in-roof) has a U-value from Table 16 : Roof U-values when loft
|
||||
# insulation thickness is known according to its insulation
|
||||
# thickness if at least half the area concerned is accessible,
|
||||
# otherwise it is the default for the age band of the original
|
||||
# property or extension."
|
||||
# Worksheet pattern (cert 000565 BP[0]): "Roof room Main remaining
|
||||
# area" 43.97 m² × U=0.35 (Table 18 col 4 age H default).
|
||||
# Pre-slice S0380.95 the cascade computed residual area ONLY for
|
||||
# Simplified RR mode (via `rr_a_rr − rr_common − rr_gable` in
|
||||
# `_part_geometry`); the Detailed-RR branch in `heat_transmission`
|
||||
# iterated `rir.detailed_surfaces` and missed the residual entirely.
|
||||
# Cert 000565 routes all 5 BPs through Detailed mode (mapper
|
||||
# translates Simplified-Summary lodgements to `SapRoomInRoofSurface`
|
||||
# records), so cascade total_external_element_area_m2 was 779.27 m²
|
||||
# vs worksheet (31) = 857.64 m² (Δ −78.37 m² → thermal_bridging
|
||||
# under by ~−11.76 W/K).
|
||||
pages = _summary_pdf_to_textract_style_pages(_SUMMARY_000565_PDF)
|
||||
site_notes = ElmhurstSiteNotesExtractor(pages).extract()
|
||||
epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes)
|
||||
|
||||
# Act
|
||||
from domain.sap10_calculator.worksheet.heat_transmission import (
|
||||
heat_transmission_from_cert,
|
||||
)
|
||||
ht = heat_transmission_from_cert(epc, door_count=epc.door_count or 0)
|
||||
|
||||
# Assert — cascade closes to within ±10 m² of worksheet (31). The
|
||||
# residual sums roughly to BP[0]'s 43.97 m² + BP[1]'s ~22 m² +
|
||||
# BP[3]'s ~17 m² + BP[4]'s small contribution; remaining residual
|
||||
# (BP[1] ~+3.7 m² over) traces to the spec's ambiguous Detailed-
|
||||
# mode residual formula for extensions with multi-storey heights.
|
||||
assert ht.total_external_element_area_m2 >= 845.0, (
|
||||
f"cascade total_external_element_area_m2={ht.total_external_element_area_m2:.4f}; "
|
||||
f"expected ≥845 m² after §3.10.1 Detailed-RR residual area closure "
|
||||
f"(pre-slice was 779.27 m² vs worksheet 857.64)"
|
||||
)
|
||||
|
||||
|
||||
def test_summary_000565_ext2_stud_wall_2_extracts_400_plus_mm_pur_or_pir_lodgement() -> None:
|
||||
# Arrange — cert 000565 Summary §8.1 BP[2] Ext2 (Detailed) lodges
|
||||
# "Stud Wall 2: 2.00 × 2.00, 400+ mm, PUR or PIR" with Default
|
||||
|
|
|
|||
|
|
@ -845,7 +845,19 @@ def heat_transmission_from_cert(
|
|||
# line (30)); gable_wall routes to party_walls at U=0.25
|
||||
# (worksheet line (32), per Table 4 "as common wall").
|
||||
rir = part.sap_room_in_roof
|
||||
for surf in rir.detailed_surfaces:
|
||||
# RdSAP 10 §3.10.1 (PDF p.24) "residual area (area of roof
|
||||
# less the floor area of room(s)-in-roof) has a U-value
|
||||
# from Table 16 ... otherwise it is the default for the age
|
||||
# band". Wall-going RIR surfaces (gable_wall, gable_wall_
|
||||
# external, common_wall) deduct from the simplified A_RR
|
||||
# to leave the residual area, mirroring the Simplified
|
||||
# branch's `a_rr_final = rr_a_rr - rr_common - rr_gable`.
|
||||
# Roof-going surfaces (slope / flat_ceiling / stud_wall)
|
||||
# do NOT deduct — they sit inside the RR shell rather than
|
||||
# forming its perimeter walls.
|
||||
rr_walls_in_a_rr_area = 0.0
|
||||
detailed_surfaces = rir.detailed_surfaces or []
|
||||
for surf in detailed_surfaces:
|
||||
kind = surf.kind
|
||||
# RdSAP10 §15 — RR detailed sub-area rounded to 2 d.p.
|
||||
area = _round_half_up(surf.area_m2, _AREA_ROUND_DP)
|
||||
|
|
@ -878,6 +890,7 @@ def heat_transmission_from_cert(
|
|||
)
|
||||
elif kind == "gable_wall":
|
||||
party += 0.25 * area
|
||||
rr_walls_in_a_rr_area += area
|
||||
elif kind == "gable_wall_external":
|
||||
# RdSAP10 Table 4 (p.22) row 1: exposed gable U = "as
|
||||
# common wall" — i.e. the main-wall U of the storey
|
||||
|
|
@ -887,6 +900,7 @@ def heat_transmission_from_cert(
|
|||
u_gable = surf.u_value if surf.u_value is not None else uw
|
||||
rr_detailed_area += area
|
||||
walls += u_gable * area
|
||||
rr_walls_in_a_rr_area += area
|
||||
elif kind == "common_wall":
|
||||
# RdSAP 10 §3.9.2 Simplified Type 2 + Table 4 p.22
|
||||
# "Common wall": billed as external wall at the
|
||||
|
|
@ -899,6 +913,36 @@ def heat_transmission_from_cert(
|
|||
u_common = surf.u_value if surf.u_value is not None else uw
|
||||
rr_detailed_area += area
|
||||
walls += u_common * area
|
||||
rr_walls_in_a_rr_area += area
|
||||
# RdSAP 10 §3.10.1 residual area = simplified A_RR shell
|
||||
# minus the wall surfaces just enumerated. Uses the same
|
||||
# §3.9.1 `12.5 × √(A_RR_floor / 1.5)` formula as the
|
||||
# Simplified branch since the worksheet applies it
|
||||
# consistently when the Summary is Simplified-mode but the
|
||||
# mapper has translated wall lodgements to
|
||||
# `detailed_surfaces` records (cert 000565 BP[0]:
|
||||
# 12.5 × √30 − 24.5 = 43.97 ✓ matches worksheet's "Roof
|
||||
# room Main remaining area"). The residual gets the
|
||||
# `u_rr_default_all_elements` value (Table 18 col 4).
|
||||
#
|
||||
# Discriminator: only fire when the lodged set contains
|
||||
# WALL-going surfaces but no ROOF-going surfaces (slope,
|
||||
# flat_ceiling, stud_wall). True Detailed-mode lodgements
|
||||
# (e.g. cohort fixture 000516, where the assessor lodges
|
||||
# every slope / flat_ceiling / stud_wall) lodge the roof
|
||||
# shell explicitly — there is no residual to add.
|
||||
kinds = {s.kind for s in detailed_surfaces}
|
||||
roof_kinds = {"slope", "flat_ceiling", "stud_wall"}
|
||||
has_roof_lodgement = bool(kinds & roof_kinds)
|
||||
rr_floor_for_a_rr = float(rir.floor_area)
|
||||
if not has_roof_lodgement and rr_floor_for_a_rr > 0.0:
|
||||
a_rr_shell = 12.5 * sqrt(rr_floor_for_a_rr / 1.5)
|
||||
residual_area = max(0.0, a_rr_shell - rr_walls_in_a_rr_area)
|
||||
if residual_area > 0.0:
|
||||
rr_detailed_area += residual_area
|
||||
roof += residual_area * u_rr_default_all_elements(
|
||||
country=country, age_band=rir.construction_age_band,
|
||||
)
|
||||
floor += uf * floor_area_total
|
||||
# RdSAP "first floor over passageway" cantilever — only fires
|
||||
# for houses (property_type=0); see `_part_geometry` filters.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue