mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Cohort residual slice 11: Simplified Type 1 RR geometry — _part_geometry + heat_transmission
Implements RdSAP10 §3.9.1 Simplified Type 1 (True Room-in-Roof, no
common walls):
A_RR = 12.5 × √(A_RR_floor / 1.5)
When the cert lodges only a `SapRoomInRoof(floor_area, construction_
age_band)` (no gable / party / sheltered / connected wall lengths),
ΣA_RR_gable/other = 0 → A_RR_final = A_RR, treated as timber-framed
roof structure with U from Table 18 col (4) "Room-in-roof, all elements".
The storey-below roof area (§3.8) is deducted by A_RR_floor per §3.9.
Changes:
- `_part_geometry`: returns new keys `rr_floor_area_m2` and
`rr_simplified_a_rr_m2`; existing `top_floor_area_m2` now subtracts
`rr_floor_area_m2` (the §3.9 deduction).
- Main loop: `roof += U_RR × A_RR` where U_RR is from
`u_rr_default_all_elements(country, rir.construction_age_band)`.
A_RR also joins the (31) external-area total for thermal-bridging.
Test: synthetic 2-storey + RR (15 m² floor) at age B → roof_w_per_k
math closes at abs=0.001 vs hand-computed 100.92 W/K.
Cohort impact (post-slice-11 vs post-slice-8):
- 000474, 000490 unchanged at Δ=0 ✓
- 000480: Δ=+12 → +4 (RR Simplified resolved most of the gap)
- 000487: Δ=+11 → +3 (same)
- 000516: Δ=+12 → +4 (same)
- 000477: Δ=+2 → −6 (overshoot — the U985 PDF uses detailed §3.10
per-surface RR lodgement; Simplified Type 1 at U=2.30 is too high
for an RR with measured retrofit insulation. Closes once Detailed
lands + 000477 fixture upgrades to detailed lodgement, slice 14.)
Reference: RdSAP 10 (10-06-2025) §3.9.1 page 21-22; Table 18 page 45.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
0ff814451f
commit
4df056859e
2 changed files with 103 additions and 3 deletions
|
|
@ -56,9 +56,11 @@ from domain.ml.rdsap_uvalues import (
|
|||
u_floor,
|
||||
u_party_wall,
|
||||
u_roof,
|
||||
u_rr_default_all_elements,
|
||||
u_wall,
|
||||
u_window,
|
||||
)
|
||||
from math import sqrt
|
||||
|
||||
|
||||
_WALL_INSULATION_NONE: Final[int] = 4
|
||||
|
|
@ -147,6 +149,8 @@ def _part_geometry(part: SapBuildingPart) -> dict[str, float]:
|
|||
"top_floor_area_m2": 0.0,
|
||||
"gross_wall_area_m2": 0.0,
|
||||
"party_wall_area_m2": 0.0,
|
||||
"rr_floor_area_m2": 0.0,
|
||||
"rr_simplified_a_rr_m2": 0.0,
|
||||
}
|
||||
fds = list(part.sap_floor_dimensions)
|
||||
ground = next((fd for fd in fds if fd.floor == 0), fds[0])
|
||||
|
|
@ -165,11 +169,28 @@ def _part_geometry(part: SapBuildingPart) -> dict[str, float]:
|
|||
(fd.party_wall_length_m or 0.0) * (fd.room_height_m or _DEFAULT_STOREY_HEIGHT_M)
|
||||
for fd in fds
|
||||
)
|
||||
# RdSAP10 §3.9.1 Simplified Type 1 (True Room-in-Roof): when an RR is
|
||||
# lodged with only its floor area (no gable/party/sheltered/connected
|
||||
# wall lengths), the spec's empirical formula treats it as one chunk
|
||||
# of timber-framed roof structure of area A_RR = 12.5 × √(A_RR_floor
|
||||
# / 1.5). The storey-below roof area (§3.8) is deducted by A_RR_floor
|
||||
# — the regular roof "becomes" the residual footprint not under the
|
||||
# RR. Type 2 (common walls < 1.8m) and the Detailed §3.10 path are
|
||||
# additive on top — they decompose A_RR into per-surface areas with
|
||||
# their own U-values, deducted from A_RR_final per step e.
|
||||
rr_floor_area = 0.0
|
||||
rr_a_rr = 0.0
|
||||
rir = part.sap_room_in_roof
|
||||
if rir is not None and rir.floor_area > 0:
|
||||
rr_floor_area = float(rir.floor_area)
|
||||
rr_a_rr = 12.5 * sqrt(rr_floor_area / 1.5)
|
||||
return {
|
||||
"ground_floor_area_m2": ground.total_floor_area_m2 or 0.0,
|
||||
"top_floor_area_m2": top.total_floor_area_m2 or 0.0,
|
||||
"top_floor_area_m2": max(0.0, (top.total_floor_area_m2 or 0.0) - rr_floor_area),
|
||||
"gross_wall_area_m2": gross_wall,
|
||||
"party_wall_area_m2": party_wall,
|
||||
"rr_floor_area_m2": rr_floor_area,
|
||||
"rr_simplified_a_rr_m2": rr_a_rr,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -328,6 +349,21 @@ def heat_transmission_from_cert(
|
|||
|
||||
walls += uw * main_wall_area + alt_walls_contribution
|
||||
roof += ur * roof_area
|
||||
# RdSAP10 §3.9.1 Simplified RR contribution. A_RR is treated as
|
||||
# timber-framed roof structure with U from Table 18 column (4)
|
||||
# "Room-in-roof, all elements" (the as-built / unknown default
|
||||
# per footnote (1)). The age band lives on the SapRoomInRoof, not
|
||||
# the SapBuildingPart, so we read it from there. Lines under (30)
|
||||
# in the U985 worksheet — same line ref as the regular roof, so
|
||||
# we fold the contribution into roof_w_per_k.
|
||||
rr_a_rr = geom["rr_simplified_a_rr_m2"]
|
||||
if rr_a_rr > 0:
|
||||
rir = part.sap_room_in_roof
|
||||
assert rir is not None # rr_a_rr > 0 ⇒ rir present per _part_geometry
|
||||
u_rr = u_rr_default_all_elements(
|
||||
country=country, age_band=rir.construction_age_band,
|
||||
)
|
||||
roof += u_rr * rr_a_rr
|
||||
floor += uf * floor_area_total
|
||||
party += upw * party_area
|
||||
windows += window_u * w_area
|
||||
|
|
@ -335,9 +371,11 @@ def heat_transmission_from_cert(
|
|||
# (31) — total external element area used by both the worksheet
|
||||
# readout and the (36) thermal-bridging multiplier. Excludes the
|
||||
# party wall (party walls have their own line (32)) per RdSAP
|
||||
# §5.15: bridging applies to *exposed* area only.
|
||||
# §5.15: bridging applies to *exposed* area only. RR area joins
|
||||
# the external surfaces per the spec — A_RR contributes to (31)
|
||||
# alongside walls + roof + floor + openings.
|
||||
part_external_area = (
|
||||
main_wall_area + alt_walls_total_area + roof_area + floor_area_total + w_area + d_area
|
||||
main_wall_area + alt_walls_total_area + roof_area + floor_area_total + w_area + d_area + rr_a_rr
|
||||
)
|
||||
total_external_area += part_external_area
|
||||
bridging += y * part_external_area
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ from datatypes.epc.domain.epc_property_data import (
|
|||
BuildingPartIdentifier,
|
||||
EnergyElement,
|
||||
SapAlternativeWall,
|
||||
SapRoomInRoof,
|
||||
)
|
||||
|
||||
from domain.ml.tests._fixtures import (
|
||||
|
|
@ -1306,3 +1307,64 @@ def test_section_3_partial_match_against_elmhurst_worksheet(fixture: ModuleType)
|
|||
# (gap #1). Non-RR fixtures get exact (31) + (36) asserted by the
|
||||
# dedicated `test_section_3_non_rr_line_31_and_36_match_elmhurst_worksheet`.
|
||||
assert result.total_external_element_area_m2 > 0
|
||||
|
||||
|
||||
def test_room_in_roof_simplified_type_1_adds_a_rr_timber_framed_area_to_roof_w_per_k() -> None:
|
||||
"""RdSAP10 §3.9.1 Simplified Type 1 (True Room-in-Roof, no common
|
||||
walls). Formula (page 22 step d): A_RR = 12.5 × √(A_RR_floor / 1.5);
|
||||
this area is treated as timber-framed construction and assigned a
|
||||
U-value from Table 18 column (4) "Room-in-roof, all elements" by age
|
||||
band when no gable / party / sheltered / connected wall lengths are
|
||||
lodged (ΣA_RR_gable/other = 0 → A_RR_final = A_RR).
|
||||
|
||||
Also (§3.9 page 21): A_RR_floor is deducted from the storey-below
|
||||
roof area determined by §3.8 (which takes the greatest floor area
|
||||
across levels).
|
||||
|
||||
Synthetic dwelling for arithmetic clarity:
|
||||
- 2 storeys × 40 m² each → §3.8 roof area = 40
|
||||
- RR floor area = 15 m²
|
||||
- §3.9 deducted top-floor roof area = 40 − 15 = 25 m²
|
||||
- A_RR = 12.5 × √(15 / 1.5) = 12.5 × √10 ≈ 39.5285 m²
|
||||
- Age band B → U_roof (Table 18 col 1) = 0.40; U_RR (Table 18
|
||||
col 4 "as built") = 2.30.
|
||||
- roof_w_per_k = 25 × 0.40 + 39.5285 × 2.30 = 10.00 + 90.9156
|
||||
= 100.9156 W/K.
|
||||
"""
|
||||
# Arrange
|
||||
import math
|
||||
main = make_building_part(
|
||||
identifier=BuildingPartIdentifier.MAIN,
|
||||
construction_age_band="B",
|
||||
wall_construction=3,
|
||||
wall_insulation_type=4,
|
||||
party_wall_construction=1,
|
||||
roof_construction=4,
|
||||
floor_dimensions=[
|
||||
make_floor_dimension(
|
||||
total_floor_area_m2=40.0, room_height_m=2.5,
|
||||
heat_loss_perimeter_m=20.0, party_wall_length_m=0.0, floor=0,
|
||||
),
|
||||
make_floor_dimension(
|
||||
total_floor_area_m2=40.0, room_height_m=2.5,
|
||||
heat_loss_perimeter_m=20.0, party_wall_length_m=0.0, floor=1,
|
||||
),
|
||||
],
|
||||
sap_room_in_roof=SapRoomInRoof(floor_area=15.0, construction_age_band="B"),
|
||||
)
|
||||
epc = make_minimal_sap10_epc(
|
||||
total_floor_area_m2=95.0, # 40 + 40 + 15
|
||||
habitable_rooms_count=5,
|
||||
country_code="ENG",
|
||||
sap_building_parts=[main],
|
||||
)
|
||||
|
||||
# Act
|
||||
result = heat_transmission_from_cert(
|
||||
epc, window_total_area_m2=0.0, window_avg_u_value=None, door_count=0,
|
||||
)
|
||||
|
||||
# Assert
|
||||
a_rr = 12.5 * math.sqrt(15.0 / 1.5)
|
||||
expected_roof_w_per_k = (40.0 - 15.0) * 0.40 + a_rr * 2.30
|
||||
assert result.roof_w_per_k == pytest.approx(expected_roof_w_per_k, abs=0.001)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue