mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
fix(floor): exposed floor on a flat carries heat loss (RdSAP §3.12)
A mid-/top-floor flat whose lowest floor is lodged as an exposed floor
(API floor_heat_loss=1) had its floor area zeroed by the dwelling-level
exposure heuristic, which keys only on the flat label and defaults
has_exposed_floor=False (assuming the floor sits over another *heated*
dwelling). RdSAP 10 §3.12 (PDF p.25) is explicit:
"Otherwise the floor area of the flat ... is:
- an exposed floor if there is an open space below"
i.e. a flat cantilevered over a passageway IS a heat-loss floor on
Table 20. The per-BP `is_exposed_floor` lodgement is authoritative and
now overrides the dwelling-level suppression upward, mirroring the
existing "another dwelling below" party override (which suppresses
downward). The code-1↔"E To external air" enum is confirmed by the
paired API+Summary worksheet certs (0350, 3800).
Eval: 45.1% → 45.3% within 0.5 (909 computed); cert 3836 +6.79 → +0.77,
5717 +1.31 → -0.07 and 0997 +0.76 → +0.05 cross into <0.5. Two
already-failing under-rated certs (7636, 2241) shift further — both are
dominated by independent cost-side over-counts the exposed floor merely
unmasks (7636 walls = 8.98 W/K for 33.87 m² is the real defect).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ae34ca4d74
commit
b40e0f67b8
2 changed files with 48 additions and 1 deletions
|
|
@ -972,7 +972,17 @@ def heat_transmission_from_cert(
|
|||
# lodgement is authoritative. Mirrors the roof's "another dwelling
|
||||
# above" override above. Cert 2115-4121-4711-9361-3686.
|
||||
part_floor_is_party = "another dwelling below" in (part.floor_type or "").lower()
|
||||
part_has_exposed_floor = exposure.has_exposed_floor and not part_floor_is_party
|
||||
# A floor lodged as an *exposed* floor (API floor_heat_loss=1 →
|
||||
# `is_exposed_floor`, "an exposed floor if there is an open space
|
||||
# below" per RdSAP 10 §3.12, PDF p.25) carries heat loss even when
|
||||
# the dwelling-level flat heuristic (`_dwelling_exposure`) defaults
|
||||
# a mid-/top-floor flat to has_exposed_floor=False on the assumption
|
||||
# its floor sits over another *heated* dwelling. The per-BP lodgement
|
||||
# is authoritative: it overrides the suppression upward, mirroring
|
||||
# how the "another dwelling below" party signal overrides it down.
|
||||
part_has_exposed_floor = (
|
||||
exposure.has_exposed_floor or is_exposed_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,
|
||||
_AREA_ROUND_DP,
|
||||
|
|
|
|||
|
|
@ -996,6 +996,43 @@ def test_floor_over_another_dwelling_below_zeroes_floor_despite_exposed_flag() -
|
|||
assert result.walls_w_per_k > 0
|
||||
|
||||
|
||||
def test_exposed_floor_on_flat_carries_heat_loss_despite_unexposed_flag() -> None:
|
||||
# Arrange — a top-/mid-floor flat whose lowest floor is lodged as an
|
||||
# exposed floor (API floor_heat_loss=1, "an exposed floor if there is
|
||||
# an open space below" per RdSAP 10 §3.12, PDF p.25 — e.g. a flat
|
||||
# cantilevered over a passageway) IS a heat-loss floor on Table 20.
|
||||
# The dwelling-level exposure heuristic, keyed only on the flat label,
|
||||
# defaults has_exposed_floor=False on the assumption the floor sits over
|
||||
# another heated dwelling; the per-BP `is_exposed_floor` lodgement is
|
||||
# authoritative and must override that suppression upward, mirroring the
|
||||
# "another dwelling below" party override (which suppresses downward).
|
||||
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 external air",
|
||||
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,
|
||||
),
|
||||
],
|
||||
)
|
||||
main.sap_floor_dimensions[0].is_exposed_floor = True
|
||||
epc = make_minimal_sap10_epc(
|
||||
total_floor_area_m2=18.0, country_code="ENG", sap_building_parts=[main],
|
||||
)
|
||||
|
||||
# Act — dwelling-level exposure flags the floor as NOT exposed (flat).
|
||||
result = heat_transmission_from_cert(
|
||||
epc, exposure=DwellingExposure(has_exposed_floor=False, has_exposed_roof=True),
|
||||
)
|
||||
|
||||
# Assert — the per-BP exposed-floor lodgement wins → Table 20 floor 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_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-
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue