mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
S0380.192: drop placeholder roof surfaces from Simplified room-in-roof (Elmhurst)
A Simplified room-in-roof (RdSAP 10 §3.9.1, PDF p.21) does NOT measure
its slope / flat-ceiling / stud-wall surfaces — the Elmhurst Summary
lodges placeholder Length/Height cells (a 40 m flat-ceiling height, a
32 m slope on a 4.65 m-wide gable). The spec instead derives one
timber-framed "remaining area" from the floor area:
A_RR = 12.5 × √(A_RR_floor / 1.5) §3.9.1(d)
A_RR_final = A_RR − ΣA_RR_gable/other §3.9.1(e)
The cascade already computes A_RR_final itself (heat_transmission.py:
`12.5 × √(A_RR_floor/1.5) − rr_walls_in_a_rr_area` residual), but only
when `detailed_surfaces` carries no roof-going kind (`has_roof_lodgement`
gate). `_map_elmhurst_rir_surface` emitted the placeholder slope/ceiling
rows as raw L×H for every assessment type, flipping that gate and billing
1024 m² + 160 m² of explicit roof area — a 7.5× fabric-heat-loss
explosion (cert 001431 sim case 2: SAP −14.6 vs worksheet 69, space
heating 114 378 vs ~15 000 kWh).
Fix: for a Simplified assessment, drop the roof-going surfaces in the
mapper so the cascade's residual formula fires. This matches how the API
path already (correctly) handles the same Simplified RR — scalar gable
fields, no roof-going detailed_surfaces (golden cert 6035) — and the
gables-only cert 000565. Detailed (§3.10) assessments still measure these
surfaces and keep them.
With the fix, sim case 2 total external area = 232.94 (worksheet exact),
roof 78.33 (was 2725.89), SAP 69.29 → worksheet integer 69. A small
residual (~450 kWh main fuel) remains — a separate fabric gap to walk
next. 2308 passed (+2), 0 failed; pyright net-zero.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ec9ef0e8bb
commit
9cb98344fa
2 changed files with 92 additions and 1 deletions
|
|
@ -3416,6 +3416,25 @@ def _map_elmhurst_rir_surface(
|
|||
if prefix is None:
|
||||
return None
|
||||
kind = _RIR_KIND_FROM_NAME_PREFIX[prefix]
|
||||
# RdSAP 10 §3.9.1 (PDF p.21) Simplified assessment: the roof-going
|
||||
# surfaces (slope / flat ceiling / stud wall) are NOT measured — the
|
||||
# Summary lodges placeholder Length/Height cells (e.g. a 40 m ceiling
|
||||
# height, a 32 m slope on a 4.65 m-wide gable). The spec instead
|
||||
# derives one timber-framed "remaining area" from the floor area:
|
||||
# A_RR = 12.5 × √(A_RR_floor / 1.5) §3.9.1(d)
|
||||
# A_RR_final = A_RR − ΣA_RR_gable/other §3.9.1(e)
|
||||
# The cascade computes A_RR_final itself (heat_transmission.py — the
|
||||
# `12.5 × √(A_RR_floor / 1.5) − rr_walls_in_a_rr_area` residual),
|
||||
# but ONLY when `detailed_surfaces` carries no roof-going kind
|
||||
# (`has_roof_lodgement` gate). Emitting these placeholder rows flips
|
||||
# that gate and bills their raw L×H as explicit roof area (a 7.5×
|
||||
# heat-loss explosion). Drop them for Simplified so the cascade's
|
||||
# residual formula fires — matching how the API path already handles
|
||||
# the same Simplified RR (scalar gable fields, no roof-going
|
||||
# detailed_surfaces; cert 6035) and the gables-only cert 000565.
|
||||
# Detailed (§3.10) assessments DO measure these surfaces — keep them.
|
||||
if is_simplified and kind in ("slope", "flat_ceiling", "stud_wall"):
|
||||
return None
|
||||
u_value_override: Optional[float] = None
|
||||
if kind == "gable_wall" and surface.gable_type == "Sheltered":
|
||||
kind = "gable_wall_external"
|
||||
|
|
|
|||
|
|
@ -14,10 +14,18 @@ area fraction); SAP 10.3 specification (13-01-2026) Tables 4a/4e/12.
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Final
|
||||
from typing import Final, Optional
|
||||
|
||||
import pytest
|
||||
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_map_elmhurst_room_in_roof, # pyright: ignore[reportPrivateUsage]
|
||||
)
|
||||
from datatypes.epc.surveys.elmhurst_site_notes import (
|
||||
RoomInRoof as ElmhurstRoomInRoof,
|
||||
RoomInRoofSurface as ElmhurstRoomInRoofSurface,
|
||||
)
|
||||
|
||||
from datatypes.epc.domain.epc_property_data import (
|
||||
EpcPropertyData,
|
||||
MainHeatingDetail,
|
||||
|
|
@ -2120,6 +2128,70 @@ def test_elmhurst_main_fuel_to_sap10_maps_bio_liquid_water_heating_labels() -> N
|
|||
assert _ELMHURST_MAIN_FUEL_TO_SAP10["Bio-liquid HVO from used cooking oil"] == 71
|
||||
|
||||
|
||||
def _placeholder_rir_surfaces() -> "list[ElmhurstRoomInRoofSurface]":
|
||||
# A §3.9.1 Simplified Room-in-Roof lodges the roof-going Length/Height
|
||||
# cells as placeholders (a 40 m flat-ceiling height, a 32 m slope on a
|
||||
# 4.65 m gable) — Elmhurst ignores them and derives the area from the
|
||||
# floor area. Gables ARE measured (4.65 × 2.45 = 11.39).
|
||||
def surf(
|
||||
name: str, length: float, height: float,
|
||||
gable_type: Optional[str] = None, default_u: Optional[float] = None,
|
||||
) -> "ElmhurstRoomInRoofSurface":
|
||||
return ElmhurstRoomInRoofSurface(
|
||||
name=name, length_m=length, height_m=height, insulation="",
|
||||
insulation_type=None, gable_type=gable_type,
|
||||
default_u_value=default_u, u_value_known=False, u_value=0.0,
|
||||
)
|
||||
|
||||
return [
|
||||
surf("Flat Ceiling 1", 4.00, 40.00), # placeholder
|
||||
surf("Slope 1", 32.00, 32.00), # placeholder
|
||||
surf("Gable Wall 1", 4.65, 2.45, gable_type="Exposed", default_u=0.29),
|
||||
surf("Gable Wall 2", 4.65, 2.45, gable_type="Party", default_u=0.25),
|
||||
]
|
||||
|
||||
|
||||
def test_elmhurst_simplified_rir_drops_placeholder_roof_surfaces() -> None:
|
||||
# Arrange — RdSAP 10 §3.9.1 (PDF p.21): a Simplified RR's slope /
|
||||
# flat ceiling / stud wall are not measured; emitting their
|
||||
# placeholder L×H as `detailed_surfaces` makes the cascade bill them
|
||||
# as explicit roof area (7.5× heat-loss explosion) instead of firing
|
||||
# the spec's `A_RR = 12.5√(A_floor/1.5) − Σwalls` residual formula.
|
||||
rir = ElmhurstRoomInRoof(
|
||||
floor_area_m2=29.75, construction_age_band="A",
|
||||
assessment="Simplified Type 1", surfaces=_placeholder_rir_surfaces(),
|
||||
)
|
||||
|
||||
# Act
|
||||
mapped = _map_elmhurst_room_in_roof(rir)
|
||||
|
||||
# Assert — roof-going surfaces dropped, both gables retained.
|
||||
assert mapped is not None
|
||||
kinds = sorted(s.kind for s in (mapped.detailed_surfaces or []))
|
||||
assert "slope" not in kinds
|
||||
assert "flat_ceiling" not in kinds
|
||||
assert kinds == ["gable_wall", "gable_wall_external"]
|
||||
|
||||
|
||||
def test_elmhurst_detailed_rir_keeps_roof_surfaces() -> None:
|
||||
# Arrange — a Detailed (§3.10) assessment DOES measure slope / flat
|
||||
# ceiling, so they must be retained (regression guard so the
|
||||
# Simplified drop doesn't bleed into Detailed lodgements).
|
||||
rir = ElmhurstRoomInRoof(
|
||||
floor_area_m2=29.75, construction_age_band="A",
|
||||
assessment="Detailed", surfaces=_placeholder_rir_surfaces(),
|
||||
)
|
||||
|
||||
# Act
|
||||
mapped = _map_elmhurst_room_in_roof(rir)
|
||||
|
||||
# Assert — slope + flat ceiling retained under the Detailed path.
|
||||
assert mapped is not None
|
||||
kinds = sorted(s.kind for s in (mapped.detailed_surfaces or []))
|
||||
assert "slope" in kinds
|
||||
assert "flat_ceiling" in kinds
|
||||
|
||||
|
||||
def test_elmhurst_gas_boiler_main_fuel_derives_carrier_from_water_heating() -> None:
|
||||
# Arrange — SAP 10.2 Table 4b (PDF p.168) rows 101-119 are "Gas
|
||||
# boilers (including mains gas, LPG and biogas)". The code identifies
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue