mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice S0380.22: per-BP roof exposure — closes cert 0036 Ext1 flat roof
For multi-BP dwellings the dwelling-level `exposure.has_exposed_roof`
flag (derived from `dwelling_type` via `_dwelling_exposure`) zeroed
out ALL BPs' roof contributions uniformly. That's wrong when a flat
has an extension with its own external roof — e.g. ground-floor flat
with a single-storey extension whose flat roof is exposed.
Replace the global suppression with a per-BP signal:
- Per-BP `roof_construction_type` containing "another dwelling
above" → that BP's roof is party → suppress.
- Otherwise BP 0 (Main) falls back to the dwelling-level flag
(covers flat lodgements that don't explicitly mark the Main
roof type).
- Extensions (i > 0) expose their roof by default unless their
own roof_construction_type lodges as party.
Cohort cert 0036-6325-1100-0063-1226 (ground-floor flat, age D):
- Main lodges roof_construction_type = "Another dwelling above"
→ contributes 0 W/K (matches worksheet line (30) "External roof
Main 57.93 m² × U=0 = 0.0").
- Ext1 lodges roof_construction_type = "Flat" → contributes
1.09 m² × U=2.30 = 2.507 W/K (matches worksheet "External roof
Ext1 1.09 m² × U=2.30 = 2.507", spec line (30)).
- Cascade SAP closes from +0.2987 → -6e-6 vs worksheet 62.7471.
Houses + bungalows are unaffected: dwelling-level flag stays True
and the per-BP guard only activates on explicit party-roof lodgement.
Single-BP flat tests stay correct: the per-BP guard is a no-op when
no roof_construction_type is lodged (i==0 → falls back to dwelling-
level flag).
Spec citation:
- RdSAP 10 §3 / §5.11 — heat-loss surfaces and party-roof
treatment. SAP 10.2 spec line (30) sums external roofs only;
party roofs sit in the (32) party-element channel with U=0.
Cohort-2 distribution (38 certs, Summary path) shifts:
exact (<1e-4): 19 → **20** (+1: 0036)
0.07..0.5: 2 → **1** (-1: 0036 → exact)
Pyright net-zero (heat_transmission.py 13→13, test file 71→71).
Test counts: 702 → 703 pass (+1 new test), 10 expected fails unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
f7d863a9fa
commit
7136edf2fb
2 changed files with 77 additions and 1 deletions
|
|
@ -686,8 +686,26 @@ def heat_transmission_from_cert(
|
|||
# floor areas on each level. For a pitched roof with a sloping
|
||||
# ceiling, divide that area by cos(30°) — the worksheet enters
|
||||
# the inclined surface area, not the horizontal projection.
|
||||
top_floor_area = geom["top_floor_area_m2"] if exposure.has_exposed_roof else 0.0
|
||||
roof_type = (part.roof_construction_type or "").lower()
|
||||
# Per-BP roof exposure: an extension on a flat can have its own
|
||||
# external roof even when the dwelling-level position says the
|
||||
# primary building's roof is party (cohort cert 0036: ground-
|
||||
# floor flat with single-storey Ext1 flat roof, worksheet (30)
|
||||
# = 1.09 m² × U=2.30 = 2.51 W/K). The per-BP signal is the
|
||||
# explicit `roof_construction_type` lodgement of "another
|
||||
# dwelling above" — when present, suppress that BP's roof; when
|
||||
# absent, the dwelling-level `exposure.has_exposed_roof` flag
|
||||
# applies to the primary BP (i==0) and extensions (i>0) expose
|
||||
# by default. Houses + bungalows pass through unchanged because
|
||||
# their dwelling-level flag stays True.
|
||||
part_roof_is_party = "another dwelling above" in roof_type
|
||||
if part_roof_is_party:
|
||||
part_has_exposed_roof = False
|
||||
elif i == 0:
|
||||
part_has_exposed_roof = exposure.has_exposed_roof
|
||||
else:
|
||||
part_has_exposed_roof = True
|
||||
top_floor_area = geom["top_floor_area_m2"] if part_has_exposed_roof else 0.0
|
||||
if "sloping ceiling" in roof_type:
|
||||
top_floor_area = top_floor_area / _COS_30_DEG
|
||||
gross_roof_area = _round_half_up(top_floor_area, _AREA_ROUND_DP)
|
||||
|
|
|
|||
|
|
@ -762,6 +762,64 @@ def test_ground_floor_flat_exposure_keeps_floor_drops_roof() -> None:
|
|||
assert ground.roof_w_per_k == 0.0
|
||||
|
||||
|
||||
def test_ground_floor_flat_extension_with_flat_roof_exposes_extension_roof_only() -> None:
|
||||
"""Per-BP roof exposure: an extension on a ground-floor flat can have
|
||||
its own external (e.g. single-storey) roof even though the dwelling-
|
||||
level position says the main building's roof is party. Cohort cert
|
||||
0036-6325-1100-0063-1226: ground-floor flat, Main lodges roof type
|
||||
"Another dwelling above" (party); Ext1 lodges roof type "Flat" with
|
||||
its own external surface. Worksheet (30) sums Ext1's 1.09 m² × U=2.30
|
||||
= 2.507 W/K; Main contributes 0 W/K. Without the per-BP signal the
|
||||
dwelling-level `has_exposed_roof=False` zeroes both → -0.30 SAP
|
||||
over-prediction.
|
||||
"""
|
||||
# Arrange
|
||||
main = make_building_part(
|
||||
identifier=BuildingPartIdentifier.MAIN,
|
||||
construction_age_band="D",
|
||||
wall_construction=4, wall_insulation_type=4,
|
||||
party_wall_construction=1, roof_construction=4,
|
||||
floor_dimensions=[
|
||||
make_floor_dimension(
|
||||
total_floor_area_m2=60.0, room_height_m=2.5,
|
||||
party_wall_length_m=0.0, heat_loss_perimeter_m=30.0, floor=0,
|
||||
),
|
||||
],
|
||||
)
|
||||
main.roof_construction_type = "Another dwelling above"
|
||||
ext1 = make_building_part(
|
||||
identifier=BuildingPartIdentifier.EXTENSION_1,
|
||||
construction_age_band="D",
|
||||
wall_construction=4, wall_insulation_type=4,
|
||||
party_wall_construction=1, roof_construction=4,
|
||||
floor_dimensions=[
|
||||
make_floor_dimension(
|
||||
total_floor_area_m2=1.09, room_height_m=2.0,
|
||||
party_wall_length_m=0.0, heat_loss_perimeter_m=3.0, floor=0,
|
||||
),
|
||||
],
|
||||
)
|
||||
ext1.roof_construction_type = "Flat"
|
||||
epc = make_minimal_sap10_epc(
|
||||
total_floor_area_m2=61.09,
|
||||
country_code="ENG",
|
||||
sap_building_parts=[main, ext1],
|
||||
)
|
||||
|
||||
# Act
|
||||
result = heat_transmission_from_cert(
|
||||
epc,
|
||||
exposure=DwellingExposure(has_exposed_floor=True, has_exposed_roof=False),
|
||||
)
|
||||
|
||||
# Assert — only Ext1's 1.09 m² flat roof contributes; Main's roof is
|
||||
# party. Age D flat-roof default per Table 18 col (3) = 2.30 W/m²K.
|
||||
expected_ext1_roof = 1.09 * 2.30
|
||||
assert abs(result.roof_w_per_k - expected_ext1_roof) <= 0.01, (
|
||||
f"got {result.roof_w_per_k:.4f}, want {expected_ext1_roof:.4f}"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# New §3 worksheet-line-mapped tests: alternative walls, effective window U,
|
||||
# and the (31)/(33) line-ref fields. Reference: SAP10.2 §3.2, RdSAP10 §1.4.2.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue