mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
feat(heat-transmission): door to unheated corridor uses Table 26 U=1.4 on the sheltered wall
A door opening to an unheated corridor/stairwell takes U=1.4 W/m²K (RdSAP 10 Table 26, p.51 — any age band) instead of the 3.0 external-door default, and its area deducts from the SHELTERED wall, not the main wall (RdSAP §3.7, p.18: "the door of a flat/maisonette to an unheated stairwell or corridor ... is deducted from the sheltered wall area"). The cascade previously billed every door at the external U on the main wall. Signal: a SHELTERED alternative wall (`is_sheltered`, the RdSAP §5.9 wall-to-unheated-corridor surface, already modelled) is the evidence that the dwelling is accessed via an unheated corridor, so one lodged door opens to it. `_corridor_door_count` returns 1 when a sheltered alt wall is present and >=1 door is lodged, else 0 — so the door channel is unchanged for every non-corridor dwelling (houses, exposed-gable flats). `heat_transmission_ from_cert` gains a `corridor_door_count` param (default 0): it splits the door area into external (main wall, age-default U) + corridor (sheltered alt wall, U=1.4), threading the corridor door's area into that wall's opening deduction and billing it at 1.4. Validated on TWO faithful worksheets: simulated case 34 (cert 001431 storage flat — doors 8.14 exact, fabric 207.47 ≈ ws 207.48) and the long-standing worksheet-harness diverger cert 2474 (−0.87 → −0.32, the "space-demand thread" was the dropped corridor door). The worksheet harness is now 47/47 with ZERO divergers. API SAP gauge: 57.6% → 60.0% within 0.5; mean|err| 1.185 → 1.167; signed −0.165 → −0.115 — ~22 sheltered-corridor flats were a systematic gap. Regression gate green (3 pre-existing fails unrelated); pyright net-zero. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
06989d6b0f
commit
c10881ae7a
3 changed files with 103 additions and 2 deletions
|
|
@ -4011,9 +4011,31 @@ def heat_transmission_section_from_cert(epc: EpcPropertyData) -> HeatTransmissio
|
|||
insulated_door_count=epc.insulated_door_count,
|
||||
insulated_door_u_value=epc.insulated_door_u_value,
|
||||
exposure=exposure,
|
||||
corridor_door_count=_corridor_door_count(epc),
|
||||
)
|
||||
|
||||
|
||||
def _corridor_door_count(epc: EpcPropertyData) -> int:
|
||||
"""RdSAP §3.7 + Table 26 — number of doors opening to an unheated
|
||||
corridor/stairwell (each billed at U=1.4 on the sheltered wall).
|
||||
|
||||
The presence of a SHELTERED alternative wall (`is_sheltered`, the
|
||||
RdSAP §5.9 wall-to-unheated-corridor surface) is the evidence that the
|
||||
dwelling is accessed via an unheated corridor, so its entrance door
|
||||
opens to that corridor. RdSAP convention assumes one such access door
|
||||
when the sheltered wall is present and the cert lodges at least one
|
||||
door; the remainder are external. Returns 0 when no sheltered alt wall
|
||||
is lodged (houses, exposed-gable flats) so the door channel is
|
||||
unchanged for every non-corridor dwelling.
|
||||
"""
|
||||
has_sheltered_alt = any(
|
||||
(bp.sap_alternative_wall_1 is not None and bp.sap_alternative_wall_1.is_sheltered)
|
||||
or (bp.sap_alternative_wall_2 is not None and bp.sap_alternative_wall_2.is_sheltered)
|
||||
for bp in (epc.sap_building_parts or [])
|
||||
)
|
||||
return 1 if has_sheltered_alt and epc.door_count > 0 else 0
|
||||
|
||||
|
||||
def _rooflight_total_area_m2_from_cert(epc: EpcPropertyData) -> float:
|
||||
"""Σ area of `epc.sap_roof_windows` for §5 daylight-factor L2a +
|
||||
§6 horizontal solar gain. Returns 0.0 when none are lodged.
|
||||
|
|
|
|||
|
|
@ -117,6 +117,11 @@ def _decimal_round_half_up_product(a: float, b: float, dp: int) -> float:
|
|||
|
||||
_WALL_INSULATION_NONE: Final[int] = 4
|
||||
_DEFAULT_DOOR_AREA_M2: Final[float] = 1.85
|
||||
# RdSAP 10 Table 26 (PDF p.51): a door opening to an unheated corridor or
|
||||
# stairwell takes U=1.4 W/m²K for any age band (vs 3.0 for an external door
|
||||
# A-J). The door sits on the sheltered wall (RdSAP §3.7 p.18) and its area
|
||||
# deducts from that wall, not the main wall.
|
||||
_CORRIDOR_DOOR_U_W_PER_M2K: Final[float] = 1.4
|
||||
_DEFAULT_STOREY_HEIGHT_M: Final[float] = 2.5
|
||||
# SAP10.2 §3.2 curtain/blind thermal resistance applied to windows (and
|
||||
# roof windows) — turns raw window U into the worksheet's (27) effective U.
|
||||
|
|
@ -569,6 +574,7 @@ def heat_transmission_from_cert(
|
|||
insulated_door_count: int = 0,
|
||||
insulated_door_u_value: Optional[float] = None,
|
||||
exposure: Optional[DwellingExposure] = None,
|
||||
corridor_door_count: int = 0,
|
||||
) -> HeatTransmission:
|
||||
"""Conduction HLC + thermal-bridging contribution, summed across every
|
||||
sap_building_part in the cert. Windows and doors are apportioned to the
|
||||
|
|
@ -590,7 +596,18 @@ def heat_transmission_from_cert(
|
|||
floor_description = _joined_descriptions(epc.floors)
|
||||
|
||||
# RdSAP10 §15 — door area rounds to 2 d.p. before entering the calc.
|
||||
door_area = _round_half_up(max(0, door_count) * _DEFAULT_DOOR_AREA_M2, _AREA_ROUND_DP)
|
||||
# A door to an unheated corridor (RdSAP Table 26 / §3.7) is billed at
|
||||
# U=1.4 and its area deducts from the sheltered wall, not the main wall.
|
||||
# Only the remaining EXTERNAL doors stay on the main wall at the
|
||||
# age-default U; the corridor door area is tracked separately and
|
||||
# assigned to the first sheltered alt wall in the BP loop below.
|
||||
corridor_door_count = max(0, min(corridor_door_count, door_count))
|
||||
external_door_count = max(0, door_count - corridor_door_count)
|
||||
door_area = _round_half_up(external_door_count * _DEFAULT_DOOR_AREA_M2, _AREA_ROUND_DP)
|
||||
corridor_door_area = _round_half_up(
|
||||
corridor_door_count * _DEFAULT_DOOR_AREA_M2, _AREA_ROUND_DP,
|
||||
)
|
||||
corridor_door_area_remaining = corridor_door_area
|
||||
# SAP10.2 §3.2: effective window U includes the 0.04 m²K/W curtain
|
||||
# resistance — `(27)` worksheet column applies it per-window. When
|
||||
# sap_windows have per-window U lodgements (mixed glazing types in
|
||||
|
|
@ -1009,12 +1026,20 @@ def heat_transmission_from_cert(
|
|||
continue
|
||||
# RdSAP10 §15 — alt wall area rounded to 2 d.p.
|
||||
alt_walls_total_area += _round_half_up(alt_wall.wall_area, _AREA_ROUND_DP)
|
||||
alt_opening = alt_window_area if idx == 0 else 0.0
|
||||
# RdSAP §3.7 (p.18): the door to an unheated corridor deducts
|
||||
# from the SHELTERED wall area. Attach the corridor door to the
|
||||
# first sheltered alt wall (its U=1.4 contribution is billed in
|
||||
# the door channel below, not here).
|
||||
if alt_wall.is_sheltered and corridor_door_area_remaining > 0.0:
|
||||
alt_opening += corridor_door_area_remaining
|
||||
corridor_door_area_remaining = 0.0
|
||||
alt_walls_contribution += _alt_wall_w_per_k(
|
||||
alt_wall=alt_wall,
|
||||
country=country,
|
||||
age_band=age_band,
|
||||
wall_description=wall_description,
|
||||
opening_area_m2=alt_window_area if idx == 0 else 0.0,
|
||||
opening_area_m2=alt_opening,
|
||||
)
|
||||
# Main wall net adds back the alt-wall windows that were initially
|
||||
# deducted from the BP's total gross — those openings should have
|
||||
|
|
@ -1263,6 +1288,13 @@ def heat_transmission_from_cert(
|
|||
total_external_area += part_external_area
|
||||
bridging += y * part_external_area
|
||||
|
||||
# RdSAP Table 26 — the unheated-corridor door's heat loss (U=1.4). Its
|
||||
# area was deducted from the sheltered alt wall above, so the alt wall
|
||||
# net (and hence its W/K) already excludes it; bill it here at the
|
||||
# corridor U. (31) is unaffected: the door area stays counted in the
|
||||
# alt-wall gross contribution, equivalent to the worksheet's separate
|
||||
# door line.
|
||||
doors += _CORRIDOR_DOOR_U_W_PER_M2K * corridor_door_area
|
||||
roof_windows_w_per_k = roof_windows_w_per_k_total
|
||||
fabric_heat_loss = (
|
||||
walls + roof + floor + party + windows + roof_windows_w_per_k + doors # (33)
|
||||
|
|
|
|||
|
|
@ -1228,6 +1228,53 @@ def test_sheltered_alternative_wall_applies_table4_0p5_resistance() -> None:
|
|||
assert sheltered_wpk < exposed_wpk
|
||||
|
||||
|
||||
def test_corridor_door_on_sheltered_alt_wall_uses_table26_u_1p4() -> None:
|
||||
# Arrange — RdSAP 10 Table 26 (PDF p.51): a door opening to an unheated
|
||||
# corridor/stairwell has U=1.4 (any age), versus 3.0 for an external
|
||||
# door (age A-J). RdSAP §3.7 (p.18): "the door of a flat/maisonette to
|
||||
# an unheated stairwell or corridor ... is deducted from the sheltered
|
||||
# wall area" — i.e. the corridor door sits on the sheltered alt wall,
|
||||
# not the main wall. Simulated case 34: a flat with a sheltered alt
|
||||
# (corridor) wall + 2 doors → 1 corridor door (U=1.4 on the alt wall) +
|
||||
# 1 external door (U=3.0 on the main wall).
|
||||
from dataclasses import replace
|
||||
|
||||
main = replace(
|
||||
make_building_part(
|
||||
construction_age_band="B",
|
||||
wall_construction=4, wall_insulation_type=4,
|
||||
party_wall_construction=1, roof_construction=4,
|
||||
floor_dimensions=[
|
||||
make_floor_dimension(
|
||||
total_floor_area_m2=50.0, room_height_m=2.5,
|
||||
party_wall_length_m=0.0, heat_loss_perimeter_m=28.0, floor=0,
|
||||
),
|
||||
],
|
||||
),
|
||||
sap_alternative_wall_1=SapAlternativeWall(
|
||||
wall_area=12.5, wall_dry_lined="N", wall_construction=4,
|
||||
wall_insulation_type=4, wall_thickness_measured="Y",
|
||||
wall_insulation_thickness="NI", is_sheltered=True,
|
||||
),
|
||||
)
|
||||
epc = make_minimal_sap10_epc(
|
||||
total_floor_area_m2=50.0, country_code="ENG", sap_building_parts=[main],
|
||||
)
|
||||
|
||||
# Act
|
||||
no_corridor = heat_transmission_from_cert(epc, door_count=2, corridor_door_count=0)
|
||||
with_corridor = heat_transmission_from_cert(epc, door_count=2, corridor_door_count=1)
|
||||
|
||||
# Assert — no-corridor: both doors external at the age-default U.
|
||||
door_u = no_corridor.doors_w_per_k / (2 * 1.85)
|
||||
# with-corridor: 1 external @door_u + 1 corridor @1.4 (both 1.85 m²).
|
||||
assert abs(with_corridor.doors_w_per_k - (1.85 * door_u + 1.85 * 1.4)) <= 0.02
|
||||
# The corridor door (U=1.4) is cheaper than an external door (U≈3.0) and
|
||||
# its area moves off the main wall onto the sheltered alt wall, so the
|
||||
# net fabric heat loss drops.
|
||||
assert with_corridor.fabric_heat_loss_w_per_k < no_corridor.fabric_heat_loss_w_per_k
|
||||
|
||||
|
||||
def test_window_uses_effective_u_value_with_curtain_resistance_per_sap10_2_section_3_2() -> None:
|
||||
"""SAP10.2 §3.2: the window U-value used for heat-transmission is the
|
||||
effective form `U_eff = 1/(1/U_raw + 0.04)` — the 0.04 m²K/W is the
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue