mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +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_floor,
|
||||||
u_party_wall,
|
u_party_wall,
|
||||||
u_roof,
|
u_roof,
|
||||||
|
u_rr_default_all_elements,
|
||||||
u_wall,
|
u_wall,
|
||||||
u_window,
|
u_window,
|
||||||
)
|
)
|
||||||
|
from math import sqrt
|
||||||
|
|
||||||
|
|
||||||
_WALL_INSULATION_NONE: Final[int] = 4
|
_WALL_INSULATION_NONE: Final[int] = 4
|
||||||
|
|
@ -147,6 +149,8 @@ def _part_geometry(part: SapBuildingPart) -> dict[str, float]:
|
||||||
"top_floor_area_m2": 0.0,
|
"top_floor_area_m2": 0.0,
|
||||||
"gross_wall_area_m2": 0.0,
|
"gross_wall_area_m2": 0.0,
|
||||||
"party_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)
|
fds = list(part.sap_floor_dimensions)
|
||||||
ground = next((fd for fd in fds if fd.floor == 0), fds[0])
|
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)
|
(fd.party_wall_length_m or 0.0) * (fd.room_height_m or _DEFAULT_STOREY_HEIGHT_M)
|
||||||
for fd in fds
|
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 {
|
return {
|
||||||
"ground_floor_area_m2": ground.total_floor_area_m2 or 0.0,
|
"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,
|
"gross_wall_area_m2": gross_wall,
|
||||||
"party_wall_area_m2": party_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
|
walls += uw * main_wall_area + alt_walls_contribution
|
||||||
roof += ur * roof_area
|
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
|
floor += uf * floor_area_total
|
||||||
party += upw * party_area
|
party += upw * party_area
|
||||||
windows += window_u * w_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
|
# (31) — total external element area used by both the worksheet
|
||||||
# readout and the (36) thermal-bridging multiplier. Excludes the
|
# readout and the (36) thermal-bridging multiplier. Excludes the
|
||||||
# party wall (party walls have their own line (32)) per RdSAP
|
# 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 = (
|
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
|
total_external_area += part_external_area
|
||||||
bridging += y * part_external_area
|
bridging += y * part_external_area
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ from datatypes.epc.domain.epc_property_data import (
|
||||||
BuildingPartIdentifier,
|
BuildingPartIdentifier,
|
||||||
EnergyElement,
|
EnergyElement,
|
||||||
SapAlternativeWall,
|
SapAlternativeWall,
|
||||||
|
SapRoomInRoof,
|
||||||
)
|
)
|
||||||
|
|
||||||
from domain.ml.tests._fixtures import (
|
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
|
# (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`.
|
# dedicated `test_section_3_non_rr_line_31_and_36_match_elmhurst_worksheet`.
|
||||||
assert result.total_external_element_area_m2 > 0
|
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