mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice S0380.91: party-wall Cavity-masonry-filled U=0.2 (RdSAP 10 Table 15 row 3)
RdSAP 10 §5.10 Table 15 (PDF p.42) "U-values of party walls":
Party wall type U
--------------------------------------------- ----
Solid masonry / timber frame / system built 0.0
Cavity masonry unfilled 0.5
Cavity masonry filled 0.2
Unable to determine, house or bungalow 0.25
Unable to determine, flat or maisonette* 0.0
Pre-slice the cascade collapsed CF (Cavity masonry filled) into the same
SAP10 wall_construction code 4 as CU (Cavity masonry unfilled), so the
filled-cavity row's spec U=0.2 was silently rounded up to the unfilled
U=0.5. The mapper at `_ELMHURST_PARTY_WALL_CODE_TO_SAP10["CF"]: 4` and
`_API_PARTY_WALL_CONSTRUCTION_TO_SAP10[3]: 4` both flagged this as a
known approximation since S0380.64; today's slice closes it.
Introduces a party-wall-only synthetic SAP10 code
`WALL_CAVITY_FILLED_PARTY = 11` (distinct from the main wall_construction
codes 1-10 since Table 15 treats filled vs unfilled cavity as separate
party-wall types). `u_wall` doesn't consume code 11 so main-wall U-value
cascades are unaffected. Cohort + golden audit: only cert 000565 Ext1
lodges CF on the Elmhurst side; zero golden certs lodge API code 3, so
flipping the dispatch is scoped to one BP.
Cert 000565 movement (HEAD edb1e6b8 → this slice):
- cascade party_walls_w_per_k: 93.255 → 65.13 ✓ EXACT vs worksheet 65.13
- sap_score (integer): 27 → 28 (Δ−2 → Δ−1)
- sap_score_continuous: 27.3534 → 27.9893 (Δ−1.16 → Δ−0.52)
- space_heating_kwh: 60468.18 → 59639.74 (Δ+1460 → Δ+631; 57% closed)
- main_heating_fuel_kwh: 35569.52 → 35082.20 (Δ+859 → Δ+371; 57% closed)
- co2_kg_per_yr: 6581.12 → 6506.48 (Δ+133 → Δ+59)
- total_fuel_cost_gbp: 4784.29 → 4726.75 (Δ+104 → Δ+46)
- hot_water_kwh: 3755.03 ✓ EXACT unchanged
- lighting / pumps_fans: sub-spec residuals unchanged
Continuous SAP at 27.9893 is 0.51 below the 28.5 rounding-up threshold;
the remaining +631 SH residual (ventilation +27 W/K + doors missing +21
W/K + downstream) pushes integer score to 29 once those land.
Cohort + 9 golden API + 38 cohort-2 API + 6 U985 Elmhurst certs all
unaffected (no CF lodgements; party_wall_construction=4 still routes to
0.5 for CU). Existing `test_u_party_wall_unfilled_cavity_returns_table15
_value` regression-guards code 4 stays at U=0.5.
Test baseline: 575 pass + 9 expected `000565` fails (was 574 + 9, +1 net
new cascade pin test). 105/105 pass in `test_rdsap_uvalues.py` including
new CF unit test. Pyright net-zero per touched file (baseline 1/65/32/13
preserved). 3 pre-existing failures in adjacent test files (test_heat_
transmission roof + basement, test_from_rdsap_schema floor_area) unchanged.
Per [[project-sap10_ml-deprecation]] the synthetic code constant lives
alongside its consumer `u_party_wall` in `domain/sap10_ml/rdsap_uvalues.py`
(editing the existing file). When the deprecation migration moves
`rdsap_uvalues.py` to `domain/sap10_calculator/`, `WALL_CAVITY_FILLED_
PARTY` moves with it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
7a45737865
commit
b6ebcad54d
4 changed files with 78 additions and 32 deletions
|
|
@ -45,6 +45,7 @@ from datatypes.epc.domain.mapper import (
|
|||
)
|
||||
from domain.sap10_calculator.calculator import calculate_sap_from_inputs
|
||||
from domain.sap10_calculator.rdsap.cert_to_inputs import SAP_10_2_SPEC_PRICES, cert_to_inputs
|
||||
from domain.sap10_ml.rdsap_uvalues import u_party_wall
|
||||
from domain.sap10_calculator.worksheet.tests import (
|
||||
_elmhurst_worksheet_000474 as _w000474,
|
||||
_elmhurst_worksheet_000477 as _w000477,
|
||||
|
|
@ -1543,14 +1544,14 @@ def test_summary_000565_bp0_alt1_stone_granite_thin_wall_routes_to_u_value_2p34_
|
|||
)
|
||||
|
||||
|
||||
def test_summary_000565_ext1_party_wall_routes_to_cavity_filled_code_4() -> None:
|
||||
# Arrange — RdSAP 10 Table 15 row 3 "Cavity masonry filled":
|
||||
# cert 000565 Ext1 lodges "CF Cavity masonry filled". Routes
|
||||
# to SAP10 code 4 (Cavity). TODO(S0380.64+1): Table 15 row 3
|
||||
# spec U=0.20; today's `u_party_wall` only returns 0.0 / 0.5 /
|
||||
# 0.25 for code 4 so the cascade conservatively rounds up to
|
||||
# the cavity-unfilled U=0.5 — matches the pre-existing
|
||||
# `_API_PARTY_WALL_CONSTRUCTION_TO_SAP10[3]` approximation.
|
||||
def test_summary_000565_ext1_party_wall_routes_to_cavity_filled_code_11() -> None:
|
||||
# Arrange — RdSAP 10 §5.10 Table 15 row 3 (PDF p.42) "Cavity masonry
|
||||
# filled -> U=0.2 W/m²K". Cert 000565 Ext1 lodges "CF Cavity masonry
|
||||
# filled". The synthetic SAP10 code `WALL_CAVITY_FILLED_PARTY=11`
|
||||
# (introduced S0380.91) distinguishes filled-cavity party walls from
|
||||
# the construction-class-shared code 4 (which `u_party_wall` resolves
|
||||
# to 0.5 per Table 15 row 2). Code 11 is party-wall-only; it never
|
||||
# appears as a main `wall_construction` so `u_wall` is unaffected.
|
||||
pages = _summary_pdf_to_textract_style_pages(_SUMMARY_000565_PDF)
|
||||
site_notes = ElmhurstSiteNotesExtractor(pages).extract()
|
||||
|
||||
|
|
@ -1558,7 +1559,29 @@ def test_summary_000565_ext1_party_wall_routes_to_cavity_filled_code_4() -> None
|
|||
epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes)
|
||||
|
||||
# Assert
|
||||
assert epc.sap_building_parts[1].party_wall_construction == 4
|
||||
assert epc.sap_building_parts[1].party_wall_construction == 11
|
||||
|
||||
|
||||
def test_summary_000565_ext1_party_wall_cf_routes_to_u_value_0p2() -> None:
|
||||
# Arrange — cascade integration check for slice S0380.91: route
|
||||
# cert 000565's Summary §8.1 "CF Cavity masonry filled" lodgement
|
||||
# through extractor + mapper + heat_transmission and verify Ext1's
|
||||
# party-wall U-value is 0.2 (Table 15 row 3) rather than the prior
|
||||
# 0.5 (cavity-unfilled approximation). Localises the slice to one
|
||||
# surface area × U product so the cascade aggregate movement (-28
|
||||
# W/K on party_walls, ~-1000 kWh of cert 000565's +1460 SH residual)
|
||||
# is traceable to one BP.
|
||||
pages = _summary_pdf_to_textract_style_pages(_SUMMARY_000565_PDF)
|
||||
site_notes = ElmhurstSiteNotesExtractor(pages).extract()
|
||||
epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes)
|
||||
construction = epc.sap_building_parts[1].party_wall_construction
|
||||
assert isinstance(construction, int)
|
||||
|
||||
# Act
|
||||
u = u_party_wall(party_wall_construction=construction)
|
||||
|
||||
# Assert
|
||||
assert abs(u - 0.2) <= 1e-4
|
||||
|
||||
|
||||
def test_summary_mapper_raises_on_unmapped_wall_type_code() -> None:
|
||||
|
|
|
|||
|
|
@ -2193,13 +2193,12 @@ _ELMHURST_PARTY_WALL_CODE_TO_SAP10: Dict[str, int] = {
|
|||
"CU": 4, # Cavity masonry unfilled — same U=0.5 cascade; Elmhurst
|
||||
# encodes party-wall cavity type with the masonry sub-code
|
||||
# (CU vs CF filled) — observed first on cert 001479 Main.
|
||||
"CF": 4, # Cavity masonry filled (cert 000565 Ext1) — RdSAP 10
|
||||
# Table 15 row 3 spec U=0.20. The cascade's `u_party_wall`
|
||||
# only returns 0.0 / 0.5 / 0.25 for code 4 today, so CF
|
||||
# rounds up to the conservative cavity-unfilled U=0.5 —
|
||||
# matches the existing `_API_PARTY_WALL_CONSTRUCTION_TO
|
||||
# _SAP10[3]` approximation until u_party_wall gains the
|
||||
# filled-cavity branch (TODO).
|
||||
"CF": 11, # Cavity masonry filled (cert 000565 Ext1) — RdSAP 10
|
||||
# §5.10 Table 15 row 3 (PDF p.42) spec U=0.20. Routes via
|
||||
# the synthetic `WALL_CAVITY_FILLED_PARTY=11` code that
|
||||
# `u_party_wall` resolves to 0.2 (slice S0380.91). Code 11
|
||||
# is party-wall-only and never appears as a main wall
|
||||
# `wall_construction` so `u_wall` is unaffected.
|
||||
# "U Unable to determine" — the cohort's modal lodgement. The cohort
|
||||
# hand-built convention uses 0 as the explicit "unknown" sentinel
|
||||
# (rather than None) so cross-mapper field parity is preserved; the
|
||||
|
|
@ -2252,10 +2251,10 @@ class UnmappedApiCode(ValueError):
|
|||
|
||||
# GOV.UK API party_wall_construction enum → SAP10 wall_construction
|
||||
# integer (the domain `u_party_wall` consumes). The API uses a different
|
||||
# enum from the regular wall_construction field — RdSAP 10 Table 15
|
||||
# (p.31 "U-values of party walls") defines 5 categories, mapped to the
|
||||
# nearest SAP10 wall_construction code that `u_party_wall` resolves to
|
||||
# the spec U-value:
|
||||
# enum from the regular wall_construction field — RdSAP 10 §5.10 Table 15
|
||||
# (PDF p.42 "U-values of party walls") defines 5 categories, mapped to
|
||||
# the SAP10 wall_construction code that `u_party_wall` resolves to the
|
||||
# spec U-value:
|
||||
# 0 = "Not applicable" / no party wall (detached etc.) → cascade
|
||||
# returns 0.25 by default but party_wall_length is 0 so the
|
||||
# contribution is 0 regardless.
|
||||
|
|
@ -2263,10 +2262,10 @@ class UnmappedApiCode(ValueError):
|
|||
# (WALL_SOLID_BRICK) → u_party_wall = 0.0 (Table 15 row 1).
|
||||
# 2 = "Cavity masonry unfilled" → SAP10 code 4 (WALL_CAVITY) →
|
||||
# u_party_wall = 0.5 (Table 15 row 2).
|
||||
# 3 = "Cavity masonry filled" → spec U=0.2 (Table 15 row 3) — not
|
||||
# yet representable; the cascade only emits 0.0 / 0.5 / 0.25 from
|
||||
# the current u_party_wall, so this code rounds up to the
|
||||
# conservative 0.5 (matches the cavity-unfilled W/K).
|
||||
# 3 = "Cavity masonry filled" → synthetic SAP10 code 11
|
||||
# (WALL_CAVITY_FILLED_PARTY) → u_party_wall = 0.2 (Table 15 row 3,
|
||||
# slice S0380.91). Distinct from code 4 because Table 15 lists
|
||||
# the filled / unfilled cavity rows as separate party-wall types.
|
||||
# 4 = "Unable to determine, house or bungalow" → None (cascade
|
||||
# default 0.25).
|
||||
# 5 = "Unable to determine, flat or maisonette" → cascade should
|
||||
|
|
@ -2278,7 +2277,7 @@ _API_PARTY_WALL_CONSTRUCTION_TO_SAP10: Dict[int, Optional[int]] = {
|
|||
0: None,
|
||||
1: 3, # Solid masonry / timber / system → U=0.0
|
||||
2: 4, # Cavity masonry unfilled → U=0.5
|
||||
3: 4, # Cavity masonry filled (cascade falls through to 0.5 — TODO)
|
||||
3: 11, # Cavity masonry filled → U=0.2 (WALL_CAVITY_FILLED_PARTY)
|
||||
4: None, # Unable to determine, house — cascade default 0.25
|
||||
5: None, # Unable to determine, flat — TODO: u_party_wall=0.0 path
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,6 +115,12 @@ WALL_COB: Final[int] = 7
|
|||
WALL_PARK_HOME: Final[int] = 8
|
||||
WALL_CURTAIN: Final[int] = 9
|
||||
WALL_UNKNOWN: Final[int] = 10
|
||||
# Party-wall only: distinguishes "Cavity masonry filled" from "Cavity masonry
|
||||
# unfilled" per RdSAP 10 §5.10 Table 15 row 3 (PDF p.42) — the spec lists
|
||||
# them as separate party-wall types with U=0.2 vs U=0.5. Main wall U-value
|
||||
# cascade (`u_wall`) does not consume this code; cavity-wall insulation
|
||||
# state on a main wall flows through `wall_insulation_type` + Table 6.
|
||||
WALL_CAVITY_FILLED_PARTY: Final[int] = 11
|
||||
|
||||
|
||||
# RdSAP schema `wall_insulation_type` codes (empirically confirmed across
|
||||
|
|
@ -1094,13 +1100,12 @@ def u_party_wall(
|
|||
) -> float:
|
||||
"""RdSAP10 party-wall U-value in W/m^2K, never null.
|
||||
|
||||
Mapping: solid masonry / timber / system built -> 0.0; cavity unfilled
|
||||
-> 0.5; cavity filled -> 0.2; unknown -> 0.25 (house default) or 0.0
|
||||
when `is_flat=True` per RdSAP 10 Table 15 footnote *: "for flats and
|
||||
maisonettes with unknown party-wall construction, U=0.0" (each side
|
||||
of the party wall is a heated dwelling, so no heat loss is assumed
|
||||
by default; this matches the worksheet Elmhurst produces for flat
|
||||
fixtures such as cert 0036-6325-1100-0063-1226).
|
||||
Mapping per RdSAP 10 §5.10 Table 15 (PDF p.42):
|
||||
- Solid masonry / timber / system built -> 0.0 (row 1)
|
||||
- Cavity masonry unfilled -> 0.5 (row 2)
|
||||
- Cavity masonry filled -> 0.2 (row 3)
|
||||
- Unable to determine, house -> 0.25 (row 4)
|
||||
- Unable to determine, flat / maisonette -> 0.0 (row 5, footnote *)
|
||||
|
||||
`None` and `0` are both treated as the unknown sentinel — the
|
||||
Elmhurst mapper lodges `0` for the "U Unable to determine" code per
|
||||
|
|
@ -1114,6 +1119,8 @@ def u_party_wall(
|
|||
return 0.0
|
||||
if party_wall_construction == WALL_CAVITY:
|
||||
return 0.5
|
||||
if party_wall_construction == WALL_CAVITY_FILLED_PARTY:
|
||||
return 0.2
|
||||
return 0.25
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import pytest
|
|||
from domain.sap10_ml.rdsap_uvalues import (
|
||||
Country,
|
||||
WALL_CAVITY,
|
||||
WALL_CAVITY_FILLED_PARTY,
|
||||
WALL_CURTAIN,
|
||||
WALL_INSULATION_FILLED_CAVITY,
|
||||
WALL_SOLID_BRICK,
|
||||
|
|
@ -1233,6 +1234,22 @@ def test_u_party_wall_unfilled_cavity_returns_table15_value() -> None:
|
|||
assert result == pytest.approx(0.5, abs=0.001)
|
||||
|
||||
|
||||
def test_u_party_wall_cavity_masonry_filled_returns_0p2_per_rdsap_10_table_15_row_3() -> None:
|
||||
# Arrange — RdSAP 10 §5.10 Table 15 row 3 (PDF p.42) "Cavity masonry
|
||||
# filled -> 0.2 W/m²K". Before slice S0380.91 the `u_party_wall`
|
||||
# cascade only resolved 0.0 / 0.5 / 0.25 for code 4 so Elmhurst
|
||||
# "CF" lodgements rounded up to the conservative cavity-unfilled
|
||||
# U=0.5 — over-counting party-wall heat loss by (0.5 - 0.2) × area.
|
||||
# New synthetic code `WALL_CAVITY_FILLED_PARTY = 11` distinguishes
|
||||
# filled cavity from the construction-class-shared code 4.
|
||||
|
||||
# Act
|
||||
result = u_party_wall(party_wall_construction=WALL_CAVITY_FILLED_PARTY)
|
||||
|
||||
# Assert
|
||||
assert abs(result - 0.2) <= 1e-4
|
||||
|
||||
|
||||
def test_u_party_wall_unknown_returns_table15_house_default() -> None:
|
||||
# Arrange — Table 15: unable to determine, house -> 0.25 W/m^2K.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue