mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-27 23:35:01 +00:00
Merge pull request #1662 from Hestia-Homes/fix/floor-to-unheated-space-heat-loss
Bill a flat's floor to an unheated space as semi-exposed
This commit is contained in:
commit
0e99ad711e
2 changed files with 113 additions and 2 deletions
|
|
@ -1082,9 +1082,39 @@ def heat_transmission_from_cert(
|
|||
is_above_partial = bool(
|
||||
ground_fd is not None and ground_fd.is_above_partially_heated_space
|
||||
)
|
||||
# RdSAP 10 §3.12 (PDF p.26) — the FLATS AND MAISONETTES floor rules:
|
||||
# "There is no heat loss through the floor if there is another flat
|
||||
# below. Otherwise the floor area of the flat, or the lower floor of
|
||||
# the maisonette, is: ... a semi-exposed floor if there are unheated
|
||||
# premises below it (e.g. an enclosed garage) ... Semi-exposed
|
||||
# (sheltered) floors are treated as if they were fully exposed".
|
||||
# §5.13 (PDF p.48) confirms the U-value identity: "no distinction is
|
||||
# made ... between an exposed floor ... and a semi-exposed floor ...
|
||||
# and the U-values in Table 20 are used".
|
||||
#
|
||||
# API floor_heat_loss=2 -> floor_type "To unheated space". It set NO
|
||||
# exposure signal, so on a mid-/top-floor flat (where
|
||||
# `_dwelling_exposure_from_type` defaults has_exposed_floor=False,
|
||||
# assuming a heated dwelling below) nothing overrode the suppression
|
||||
# and the entire floor was billed at 0 W/K.
|
||||
#
|
||||
# SCOPED TO THE FLAT CASE ON PURPOSE. §3.12 is the flats/maisonettes
|
||||
# section, and the accredited Elmhurst worksheet disagrees with Table 20
|
||||
# for a HOUSE: golden cert 7536-3827-0600-0600-0276 (detached) lodges
|
||||
# floor_heat_loss=2 on its Main part and the worksheet bills that floor
|
||||
# at the BS EN ISO 13370 ground-floor U 0.97, NOT Table 20's 1.20
|
||||
# (worksheet-validated by simulated case 15). A house's floor stays on
|
||||
# the ground-floor cascade; only a flat whose floor would otherwise be
|
||||
# suppressed to zero takes the §3.12 semi-exposed route.
|
||||
part_floor_is_unheated_space = (
|
||||
"to unheated space" in (part.floor_type or "").lower()
|
||||
)
|
||||
semi_exposed_flat_floor = (
|
||||
part_floor_is_unheated_space and not exposure.has_exposed_floor
|
||||
)
|
||||
if part.has_basement:
|
||||
uf = u_basement_floor(age_band)
|
||||
elif is_exposed_floor:
|
||||
elif is_exposed_floor or semi_exposed_flat_floor:
|
||||
uf = u_exposed_floor(
|
||||
age_band=age_band, insulation_thickness_mm=floor_ins_thickness
|
||||
)
|
||||
|
|
@ -1228,7 +1258,7 @@ def heat_transmission_from_cert(
|
|||
# the "another dwelling below" party signal overrides it downward.
|
||||
part_has_exposed_floor = (
|
||||
exposure.has_exposed_floor or is_exposed_floor or is_above_partial
|
||||
or part_floor_is_ground
|
||||
or part_floor_is_ground or semi_exposed_flat_floor
|
||||
) and not part_floor_is_party
|
||||
floor_area_total = _round_half_up(
|
||||
geom["ground_floor_area_m2"] if part_has_exposed_floor else 0.0,
|
||||
|
|
|
|||
|
|
@ -2713,3 +2713,84 @@ def test_room_in_roof_detailed_gable_wall_excluded_from_line_31_external_area()
|
|||
assert result.total_external_element_area_m2 == pytest.approx(
|
||||
expected_line_31, abs=0.001
|
||||
)
|
||||
|
||||
|
||||
def test_floor_to_unheated_space_on_flat_carries_semi_exposed_loss() -> None:
|
||||
# Arrange — a mid-/top-floor flat whose lowest floor is lodged "To unheated
|
||||
# space" (API floor_heat_loss=2, e.g. an enclosed garage or undercroft
|
||||
# below). RdSAP 10 §3.12 (PDF p.26) — the flats/maisonettes section — makes
|
||||
# this a SEMI-exposed floor: "There is no heat loss through the floor if
|
||||
# there is another flat below. Otherwise the floor area of the flat ... is
|
||||
# ... a semi-exposed floor if there are unheated premises below it (e.g. an
|
||||
# enclosed garage) ... Semi-exposed (sheltered) floors are treated as if
|
||||
# they were fully exposed". §5.13 (PDF p.48) confirms the U-value identity:
|
||||
# "no distinction is made ... between an exposed floor ... and a
|
||||
# semi-exposed floor ... and the U-values in Table 20 are used".
|
||||
#
|
||||
# Code 2 previously set NO exposure signal, so the dwelling-level flat
|
||||
# heuristic (has_exposed_floor=False, assuming a heated dwelling below) went
|
||||
# unopposed and the whole floor was billed at 0 W/K.
|
||||
main = make_building_part(
|
||||
construction_age_band="B",
|
||||
wall_construction=4, wall_insulation_type=4,
|
||||
party_wall_construction=1, roof_construction=4,
|
||||
floor_type="To unheated space",
|
||||
floor_dimensions=[
|
||||
make_floor_dimension(
|
||||
total_floor_area_m2=18.0, room_height_m=2.88,
|
||||
party_wall_length_m=0.0, heat_loss_perimeter_m=8.68, floor=0,
|
||||
),
|
||||
],
|
||||
)
|
||||
epc = make_minimal_sap10_epc(
|
||||
total_floor_area_m2=18.0, country_code="ENG", sap_building_parts=[main],
|
||||
)
|
||||
|
||||
# Act — dwelling-level exposure suppresses the floor (flat label).
|
||||
result = heat_transmission_from_cert(
|
||||
epc, exposure=DwellingExposure(has_exposed_floor=False, has_exposed_roof=True),
|
||||
)
|
||||
|
||||
# Assert — Table 20 semi-exposed loss (1.20 W/m²K × 18 m² = 21.6 W/K),
|
||||
# not the suppressed 0.0.
|
||||
assert result.floor_w_per_k == pytest.approx(21.6, abs=0.1)
|
||||
|
||||
|
||||
def test_floor_to_unheated_space_on_house_stays_on_the_ground_floor_cascade() -> None:
|
||||
# Arrange — the SAME lodgement on a dwelling whose floor is already counted
|
||||
# (a house: has_exposed_floor=True). §3.12's semi-exposed rule is scoped to
|
||||
# flats and maisonettes, and the accredited Elmhurst worksheet agrees for a
|
||||
# house: golden cert 7536-3827-0600-0600-0276 (detached) lodges
|
||||
# floor_heat_loss=2 on its Main part and the worksheet bills that floor on
|
||||
# the BS EN ISO 13370 ground-floor cascade at U 0.97 — NOT Table 20's 1.20
|
||||
# (worksheet-validated by simulated case 15).
|
||||
#
|
||||
# So a house lodging "To unheated space" must bill exactly as if it lodged a
|
||||
# ground floor. This pins the scope of the flat fix above.
|
||||
def _house(floor_type: str) -> float:
|
||||
part = make_building_part(
|
||||
construction_age_band="B",
|
||||
wall_construction=4, wall_insulation_type=4,
|
||||
party_wall_construction=1, roof_construction=4,
|
||||
floor_type=floor_type,
|
||||
floor_dimensions=[
|
||||
make_floor_dimension(
|
||||
total_floor_area_m2=18.0, room_height_m=2.88,
|
||||
party_wall_length_m=0.0, heat_loss_perimeter_m=8.68, floor=0,
|
||||
),
|
||||
],
|
||||
)
|
||||
epc = make_minimal_sap10_epc(
|
||||
total_floor_area_m2=18.0, country_code="ENG", sap_building_parts=[part],
|
||||
)
|
||||
return heat_transmission_from_cert(
|
||||
epc,
|
||||
exposure=DwellingExposure(has_exposed_floor=True, has_exposed_roof=True),
|
||||
).floor_w_per_k
|
||||
|
||||
# Act
|
||||
unheated_space = _house("To unheated space")
|
||||
|
||||
# Assert — identical to the ground-floor lodgement, i.e. untouched by the
|
||||
# §3.12 flat rule (and in particular NOT Table 20's 1.20 × 18 = 21.6).
|
||||
assert unheated_space == pytest.approx(_house("Ground floor"), abs=1e-9)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue