mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice S0380.93: floor above partially-heated space U=0.7 (RdSAP 10 §5.14)
RdSAP 10 §5.14 (PDF p.47) "U-value of floor above a partially heated space": > "The U-value of a floor above partially heated premises is taken as > 0.7 W/m²K. This applies typically for a flat above non-domestic > premises that are not heated to the same extent or duration as the > flat." Cert 000565 Ext1 lodges Summary §9 "Location: P Above partially heated space" + "Default U-value: 0.70". Worksheet line (28b) confirms "Exposed floor Ext1 ... 34.0000 0.7000 23.8000". Pre-slice the cascade routed BP[1] floor through the BS EN ISO 13370 ground-floor formula (the "else" branch of the floor U-value dispatch in `heat_transmission.py`) — producing cascade U=0.76 vs spec 0.70. Over-counted floor heat loss by (0.76 − 0.70) × 34 m² = +2.04 W/K on the part subtotal and on the total HTC. Slice span (4 layers): 1. **Helper** — `u_floor_above_partially_heated_space()` in `domain/sap10_ml/rdsap_uvalues.py`, verbatim spec constant 0.7 (no age-band / insulation-thickness inputs). Lives in `sap10_ml` per [[project-sap10_ml-deprecation]] (edit existing file fine). 2. **Schema** — `SapFloorDimension.is_above_partially_heated_space: bool = False` (parallel to existing `is_exposed_floor`). Mutually exclusive with the exposed-floor / basement-floor branches. 3. **Mapper** — new `_is_floor_above_partially_heated_space(location)` helper detecting "above partially heated" in the Elmhurst §9 floor location string. Plumbed into `_map_elmhurst_building_part` floor- dim construction; only applies to the ground floor (i==0). 4. **Cascade** — `heat_transmission.py` adds a new branch between the exposed-floor and ground-floor branches: `is_above_partial → u_floor_above_partially_heated_space()`. Cert 000565 movement (HEAD `a7894b11` → this slice): - cascade floor_w_per_k: 72.41 → 70.37 (Δ +10.74 → Δ +8.70) - cascade BP[1] floor U: 0.76 → 0.70 (✓ EXACT vs ws 0.70) - sap_score (integer): 29 ✓ EXACT (unchanged — at goal) - sap_score_continuous: 28.7663 → 28.8131 (+0.0468 drift) - space_heating_kwh: −367 → −427 (small drift further under) - main_heating_fuel: −216 → −251 (downstream of SH) - co2_kg_per_yr: −32 → −37 - total_fuel_cost_gbp: −23 → −27 - hot_water_kwh: ✓ 0 EXACT unchanged The small continuous-SAP drift is the expected arithmetic of closing a single component when adjacent components remain unclosed (floor +10.74 was cancelling thermal_bridging −11.76 + roof −7.94 at the net-HTC level). Per [[feedback-zero-error-strict]] + [[feedback- spec-citation-in-commits]] the spec-correct slice ships regardless of transient continuous-SAP drift; remaining residual components (floor +8.70 from BP[2] Ext2 lodged 200 mm insulation thickness; roof −7.94; thermal_bridging −11.76; walls −1.67) each get their own spec-cited slice. Cohort safety: only cert 000565 Ext1 in the cohort lodges "Above partially heated space". All other Elmhurst cohort fixtures + 9 golden + 38 cohort-2 API certs default to `is_above_partially_ heated_space=False` so cascade behaviour is unchanged. Test baseline: 583 pass + 8 expected `000565` fails (was 582 + 8; +1 new mapper-chain test). Pyright net-zero per touched file (1/65/1/32/13/13 preserved). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
a7894b1185
commit
23aaa4fa66
6 changed files with 101 additions and 3 deletions
|
|
@ -1650,6 +1650,32 @@ def test_summary_000565_section_12_1_extracts_mechanical_extract_decentralised_m
|
|||
)
|
||||
|
||||
|
||||
def test_summary_000565_ext1_floor_above_partially_heated_routes_to_u_value_0p7_per_rdsap_10_section_5_14() -> None:
|
||||
# Arrange — RdSAP 10 §5.14 (PDF p.47) "U-value of floor above a
|
||||
# partially heated space":
|
||||
# "The U-value of a floor above partially heated premises is taken
|
||||
# as 0.7 W/m²K. This applies typically for a flat above non-
|
||||
# domestic premises that are not heated to the same extent or
|
||||
# duration as the flat."
|
||||
# Cert 000565 Summary §9 1st Extension lodges "Location: P Above
|
||||
# partially heated space" + "Default U-value: 0.70". Pre-slice the
|
||||
# cascade routed BP[1] floor through the BS EN ISO 13370 ground-
|
||||
# floor formula → cascade U=0.76 (vs spec 0.70, over by +2.04 W/K
|
||||
# × 34 m²). The mapper now flags `is_above_partially_heated_space=
|
||||
# True` on the ground SapFloorDimension so `heat_transmission`
|
||||
# dispatches to the §5.14 constant.
|
||||
pages = _summary_pdf_to_textract_style_pages(_SUMMARY_000565_PDF)
|
||||
site_notes = ElmhurstSiteNotesExtractor(pages).extract()
|
||||
|
||||
# Act
|
||||
epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes)
|
||||
|
||||
# Assert
|
||||
ext1_ground = epc.sap_building_parts[1].sap_floor_dimensions[0]
|
||||
assert ext1_ground.floor == 0
|
||||
assert ext1_ground.is_above_partially_heated_space is True
|
||||
|
||||
|
||||
def test_summary_000565_mev_decentralised_routes_to_extract_or_piv_outside_mv_kind() -> None:
|
||||
# Arrange — mapper plumbing for SAP 10.2 §2 (23a)/(24c) MEV: the
|
||||
# Elmhurst "Mechanical extract, decentralised (MEV dc)" string maps
|
||||
|
|
|
|||
|
|
@ -306,6 +306,12 @@ class SapFloorDimension:
|
|||
# first storey upward. False means a ground floor (on soil), the
|
||||
# default path through the BS EN ISO 13370 / Table 19 cascade.
|
||||
is_exposed_floor: bool = False
|
||||
# RdSAP 10 §5.14 (PDF p.47): True when this floor sits above non-
|
||||
# domestic premises heated to a lesser extent / duration. Routes to
|
||||
# the constant U=0.7 W/m²K instead of Table 19/20 or §5.13. First
|
||||
# surfaced on cert 000565 Ext1 (Summary §9 "P Above partially
|
||||
# heated space" + Default U-value 0.70).
|
||||
is_above_partially_heated_space: bool = False
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
|
|||
|
|
@ -2864,6 +2864,19 @@ def _is_floor_exposed_to_unheated_space(location: Optional[str]) -> bool:
|
|||
return "above unheated" in lower or "external air" in lower
|
||||
|
||||
|
||||
def _is_floor_above_partially_heated_space(location: Optional[str]) -> bool:
|
||||
"""True when the lodged Elmhurst §9 floor location is "P Above
|
||||
partially heated space". Routes the cascade to the RdSAP 10 §5.14
|
||||
(PDF p.47) constant U=0.7 W/m²K — distinct from `_is_floor_
|
||||
exposed_to_unheated_space` (Table 20 fully-unheated below) and from
|
||||
the ground-floor default (BS EN ISO 13370). First surfaced on cert
|
||||
000565 Ext1 (Summary §9 "P Above partially heated space"; worksheet
|
||||
line (28b) "Exposed floor Ext1 ... 0.7000")."""
|
||||
if location is None:
|
||||
return False
|
||||
return "above partially heated" in location.lower()
|
||||
|
||||
|
||||
def _extract_age_band(age_range: str) -> str:
|
||||
"""Return the letter code from a site-notes age range, e.g. 'I: 1996 - 2002' → 'I'."""
|
||||
return age_range.split(":")[0].strip()
|
||||
|
|
@ -3065,15 +3078,20 @@ def _map_elmhurst_building_part(
|
|||
key=lambda f: (0 if _is_lowest(f.name) else 1, f.name),
|
||||
)
|
||||
floor_is_exposed = _is_floor_exposed_to_unheated_space(floor.location)
|
||||
floor_is_above_partial = _is_floor_above_partially_heated_space(floor.location)
|
||||
floor_dims: List[SapFloorDimension] = []
|
||||
for i, f in enumerate(ordered):
|
||||
# SAP convention adds 0.25 m to non-ground room heights for the
|
||||
# joist/floor-void contribution; the ground floor uses the
|
||||
# lodged value directly.
|
||||
height = f.room_height_m if i == 0 else f.room_height_m + _UPPER_FLOOR_HEIGHT_ADD_M
|
||||
# `is_exposed_floor` only applies to the ground floor of a bp
|
||||
# sitting above unheated space (e.g. an extension over a porch).
|
||||
# `is_exposed_floor` / `is_above_partially_heated_space` only
|
||||
# apply to the ground floor of a bp sitting above unheated /
|
||||
# partially-heated space (e.g. an extension over a non-domestic
|
||||
# premises). Mutually exclusive with each other and with the
|
||||
# ground-floor BS EN ISO 13370 cascade.
|
||||
is_exposed = floor_is_exposed and i == 0
|
||||
is_above_partial = floor_is_above_partial and i == 0
|
||||
floor_dims.append(
|
||||
SapFloorDimension(
|
||||
room_height_m=height,
|
||||
|
|
@ -3082,6 +3100,7 @@ def _map_elmhurst_building_part(
|
|||
heat_loss_perimeter_m=f.heat_loss_perimeter_m,
|
||||
floor=i,
|
||||
is_exposed_floor=is_exposed,
|
||||
is_above_partially_heated_space=is_above_partial,
|
||||
)
|
||||
)
|
||||
alt_walls: List[Optional[SapAlternativeWall]] = [
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ from domain.sap10_ml.rdsap_uvalues import (
|
|||
u_door,
|
||||
u_exposed_floor,
|
||||
u_floor,
|
||||
u_floor_above_partially_heated_space,
|
||||
u_party_wall,
|
||||
u_roof,
|
||||
u_rr_default_all_elements,
|
||||
|
|
@ -680,14 +681,22 @@ def heat_transmission_from_cert(
|
|||
# geometry input. Set on the ground SapFloorDimension when
|
||||
# the part hangs off the main from the first storey upward
|
||||
# (e.g. 000490 Extension 1).
|
||||
# 3. Ground floor — BS EN ISO 13370 / Table 19 cascade.
|
||||
# 3. Above partially heated space — RdSAP 10 §5.14 constant
|
||||
# U=0.7 W/m²K (e.g. cert 000565 Ext1 above non-domestic
|
||||
# premises).
|
||||
# 4. Ground floor — BS EN ISO 13370 / Table 19 cascade.
|
||||
is_exposed_floor = bool(ground_fd is not None and ground_fd.is_exposed_floor)
|
||||
is_above_partial = bool(
|
||||
ground_fd is not None and ground_fd.is_above_partially_heated_space
|
||||
)
|
||||
if part.has_basement:
|
||||
uf = u_basement_floor(age_band)
|
||||
elif is_exposed_floor:
|
||||
uf = u_exposed_floor(
|
||||
age_band=age_band, insulation_thickness_mm=floor_ins_thickness
|
||||
)
|
||||
elif is_above_partial:
|
||||
uf = u_floor_above_partially_heated_space()
|
||||
else:
|
||||
# The per-bp `floor_construction_type` lodgement ("Suspended
|
||||
# timber" / "Solid") takes precedence over the global
|
||||
|
|
|
|||
|
|
@ -1074,6 +1074,25 @@ _BASEMENT_FLOOR_BY_BAND: Final[dict[str, float]] = {
|
|||
}
|
||||
|
||||
|
||||
# RdSAP 10 §5.14 (PDF p.47) — "U-value of floor above a partially
|
||||
# heated space":
|
||||
# "The U-value of a floor above partially heated premises is taken
|
||||
# as 0.7 W/m²K. This applies typically for a flat above non-
|
||||
# domestic premises that are not heated to the same extent or
|
||||
# duration as the flat."
|
||||
# Verbatim constant — no age band / insulation thickness inputs.
|
||||
# Distinct from `u_exposed_floor` (Table 20 for unheated below) and
|
||||
# from `u_floor` (BS EN ISO 13370 ground-floor formula).
|
||||
_PARTIALLY_HEATED_FLOOR_U_W_PER_M2K: Final[float] = 0.7
|
||||
|
||||
|
||||
def u_floor_above_partially_heated_space() -> float:
|
||||
"""RdSAP 10 §5.14 (PDF p.47) — U-value (W/m²K) of a floor above a
|
||||
partially heated premises. Verbatim 0.7 W/m²K from the spec; no
|
||||
geometry / age / insulation inputs."""
|
||||
return _PARTIALLY_HEATED_FLOOR_U_W_PER_M2K
|
||||
|
||||
|
||||
def u_basement_wall(age_band: Optional[str]) -> float:
|
||||
"""Basement-wall U-value (W/m²K), RdSAP10 Table 23. Defaults to the
|
||||
A-E value (0.7) when age band is missing — matches the worst-case
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ from domain.sap10_ml.rdsap_uvalues import (
|
|||
u_door,
|
||||
u_exposed_floor,
|
||||
u_floor,
|
||||
u_floor_above_partially_heated_space,
|
||||
u_party_wall,
|
||||
u_roof,
|
||||
u_rr_default_all_elements,
|
||||
|
|
@ -1127,6 +1128,24 @@ def test_u_exposed_floor_age_b_unknown_insulation_uses_table_20_row_a_to_g() ->
|
|||
assert result == pytest.approx(1.20, abs=0.001)
|
||||
|
||||
|
||||
def test_u_floor_above_partially_heated_space_returns_0p7_per_rdsap_10_section_5_14() -> None:
|
||||
# Arrange — RdSAP 10 §5.14 (PDF p.47) "U-value of floor above a
|
||||
# partially heated space":
|
||||
# "The U-value of a floor above partially heated premises is
|
||||
# taken as 0.7 W/m²K. This applies typically for a flat above
|
||||
# non-domestic premises that are not heated to the same extent
|
||||
# or duration as the flat."
|
||||
# Verbatim constant — no age-band or insulation-thickness inputs.
|
||||
# Cert 000565 Ext1 (Summary §9: "P Above partially heated space",
|
||||
# Default U-value 0.70) exercises this branch.
|
||||
|
||||
# Act
|
||||
result = u_floor_above_partially_heated_space()
|
||||
|
||||
# Assert
|
||||
assert abs(result - 0.7) <= 1e-4
|
||||
|
||||
|
||||
def test_u_floor_falls_back_to_mid_range_when_geometry_unknown() -> None:
|
||||
# Arrange — geometry missing.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue