mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
fix(ventilation): corridor flat assumes a draught lobby, zeroing §2 (13)
A flat accessed via an unheated corridor/stairwell assumes a draught lobby is present, so SAP 10.2 §2 line (13) = 0.0 rather than the 0.05 no-lobby infiltration penalty. Per RdSAP 10 Specification (10-06-2025, p.30, "Draught lobby"): "add infiltration 0.05 if draught lobby is not present, or use 0.0 if present. ... Flat or maisonette: Assume draught lobby if entrance door is facing corridor (heated or unheated) or stairwell." Signal: a SHELTERED alternative wall (the RdSAP §5.9 wall-to-unheated-corridor surface) is the evidence that the flat's entrance faces a corridor — the same evidence the corridor door (Table 26 U=1.4) rides on. New helper `_has_sheltered_corridor_wall` factors that check out of `_corridor_door_count` and gates `_has_draught_lobby`. Houses and exposed-gable flats (no sheltered alt wall) keep the lodged value / "assume no lobby if cannot be determined" default, so the §2 cascade is unchanged for every non-corridor dwelling. The cascade previously added the 0.05 penalty unconditionally, over-counting (16)/(18)/(21) by 0.05 ACH. On simulated case 34 (cert 001431 storage flat) this lifted effective air change (25)m from the worksheet's monthly 0.572-0.638 to 0.574-0.668, over-counting space-heating demand (98) by +46.3 kWh/yr (+0.41%) -> SAP -0.18. Closing it lands (25)m exactly on the worksheet (avg 0.6024) and (98) at 11356.3 vs ws 11357.2: case 34 SAP 35.1325 -> 35.3130 vs ws 35.3094 (Δ -0.1769 -> +0.0036) Guard-rails held (both improved): worksheet harness 47/47, 0 divergers (the other corridor flat, cert 2474, -0.32 -> -0.02); API gauge 60.0% -> 60.1% within 0.5, mean|err| 1.167 -> 1.163. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c10881ae7a
commit
450e33e15d
2 changed files with 90 additions and 14 deletions
|
|
@ -4015,25 +4015,54 @@ def heat_transmission_section_from_cert(epc: EpcPropertyData) -> HeatTransmissio
|
|||
)
|
||||
|
||||
|
||||
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).
|
||||
def _has_sheltered_corridor_wall(epc: EpcPropertyData) -> bool:
|
||||
"""Whether the dwelling is accessed via an unheated corridor/stairwell.
|
||||
|
||||
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.
|
||||
A SHELTERED alternative wall (`is_sheltered`, the RdSAP §5.9
|
||||
wall-to-unheated-corridor surface) is the evidence that the dwelling's
|
||||
entrance faces an unheated corridor or stairwell. False for houses and
|
||||
exposed-gable flats (no sheltered alt wall lodged).
|
||||
"""
|
||||
has_sheltered_alt = any(
|
||||
return 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 _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).
|
||||
|
||||
A sheltered alternative wall (`_has_sheltered_corridor_wall`) 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.
|
||||
"""
|
||||
return 1 if _has_sheltered_corridor_wall(epc) and epc.door_count > 0 else 0
|
||||
|
||||
|
||||
def _has_draught_lobby(epc: EpcPropertyData, sv: Optional[SapVentilation]) -> bool:
|
||||
"""RdSAP 10 §2 (13) — presence of a draught lobby.
|
||||
|
||||
Spec (RdSAP 10 Specification 10-06-2025, p.30, "Draught lobby"):
|
||||
"add infiltration 0.05 if draught lobby is not present, or use 0.0 if
|
||||
present. ... Flat or maisonette: Assume draught lobby if entrance door
|
||||
is facing corridor (heated or unheated) or stairwell."
|
||||
|
||||
A sheltered corridor wall (`_has_sheltered_corridor_wall`) is exactly
|
||||
that evidence: the flat's entrance faces an unheated corridor/stairwell,
|
||||
so a draught lobby is assumed present regardless of the lodged value.
|
||||
Otherwise fall back to the lodged value — which, when undetermined, is
|
||||
the RdSAP "assume no draught lobby if cannot be determined" default for
|
||||
houses.
|
||||
"""
|
||||
if _has_sheltered_corridor_wall(epc):
|
||||
return True
|
||||
return bool(sv.has_draught_lobby) if sv is not None and sv.has_draught_lobby is not None else False
|
||||
|
||||
|
||||
def _rooflight_total_area_m2_from_cert(epc: EpcPropertyData) -> float:
|
||||
|
|
@ -4829,7 +4858,7 @@ def ventilation_from_cert(
|
|||
flueless_gas_fires=vc.flueless_gas_fires,
|
||||
has_suspended_timber_floor=eff_has_susp,
|
||||
suspended_timber_floor_sealed=eff_sealed,
|
||||
has_draught_lobby=bool(sv.has_draught_lobby) if sv is not None and sv.has_draught_lobby is not None else False,
|
||||
has_draught_lobby=_has_draught_lobby(epc, sv),
|
||||
window_pct_draught_proofed=float(epc.percent_draughtproofed or 0),
|
||||
sheltered_sides=int(sv.sheltered_sides) if sv is not None and sv.sheltered_sides is not None else 2,
|
||||
air_permeability_ap4=ap4,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ from datatypes.epc.domain.epc_property_data import (
|
|||
EpcPropertyData,
|
||||
MainHeatingDetail,
|
||||
PhotovoltaicArray,
|
||||
SapAlternativeWall,
|
||||
SapFloorDimension,
|
||||
SapVentilation,
|
||||
)
|
||||
|
|
@ -1253,6 +1254,52 @@ def test_ventilation_from_cert_routes_mev_decentralised_to_extract_or_piv_outsid
|
|||
assert abs(w25 - expected) <= 1e-4
|
||||
|
||||
|
||||
def test_corridor_flat_assumes_draught_lobby_present_zeroing_line_13() -> None:
|
||||
# Arrange — RdSAP 10 Specification (10-06-2025) p.30, "Draught lobby":
|
||||
# "add infiltration 0.05 if draught lobby is not present, or use 0.0 if
|
||||
# present. ... Flat or maisonette: Assume draught lobby if entrance door
|
||||
# is facing corridor (heated or unheated) or stairwell." A SHELTERED
|
||||
# alternative wall is the RdSAP §5.9 wall-to-unheated-corridor surface —
|
||||
# the same evidence the corridor door rides on — so the flat's entrance
|
||||
# faces a corridor and a draught lobby is assumed present, zeroing line
|
||||
# (13). Simulated case 34 (cert 001431 storage flat): the cascade
|
||||
# previously added the 0.05 no-lobby penalty, over-counting (16)/(18) by
|
||||
# 0.05 ACH → +46 kWh/yr space demand → SAP −0.18.
|
||||
from dataclasses import replace
|
||||
|
||||
corridor_part = replace(
|
||||
make_building_part(construction_age_band="G"),
|
||||
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,
|
||||
),
|
||||
)
|
||||
exposed_part = make_building_part(construction_age_band="G")
|
||||
corridor_flat = make_minimal_sap10_epc(
|
||||
total_floor_area_m2=_TYPICAL_TFA_M2, country_code="ENG",
|
||||
sap_building_parts=[corridor_part],
|
||||
)
|
||||
exposed_flat = make_minimal_sap10_epc(
|
||||
total_floor_area_m2=_TYPICAL_TFA_M2, country_code="ENG",
|
||||
sap_building_parts=[exposed_part],
|
||||
)
|
||||
|
||||
# Act
|
||||
v_corridor = ventilation_from_cert(corridor_flat)
|
||||
v_exposed = ventilation_from_cert(exposed_flat)
|
||||
|
||||
# Assert — the corridor flat zeroes (13); a flat with no sheltered
|
||||
# corridor wall keeps the 0.05 no-lobby penalty (cannot be determined).
|
||||
assert abs(v_corridor.draught_lobby_ach - 0.0) <= 1e-9
|
||||
assert abs(v_exposed.draught_lobby_ach - 0.05) <= 1e-9
|
||||
# The lobby removes 0.05 ACH from (16); shelter (21) drops proportionally.
|
||||
assert v_corridor.infiltration_rate_ach < v_exposed.infiltration_rate_ach
|
||||
assert abs(
|
||||
(v_exposed.infiltration_rate_ach - v_corridor.infiltration_rate_ach) - 0.05
|
||||
) <= 1e-9
|
||||
|
||||
|
||||
def test_index_less_mev_uses_table_4g_default_sfp_for_fan_electricity() -> None:
|
||||
# Arrange — an MEV system with NO PCDB record (index absent / not in
|
||||
# Table 322). SAP 10.2 §2.6.3 / Table 4g note 1 prescribes a default
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue