mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice 57: Pre-1950 Elmhurst sloping-ceiling roofs map to thickness=0
Cert 001479 Ext2 §8 lodges:
Type: PS Pitched, sloping ceiling
Insulation: S Sloping ceiling insulation
Insulation Thickness: As Built
age C (1930-49)
The Summary's "As Built" thickness encodes "the dwelling as originally
constructed" — for pre-1950 sloping-ceiling roofs that's uninsulated
(no roof insulation in original 1930s construction). The worksheet's
§3 row pins U=2.30 (Table 16 row 0, uninsulated).
Pre-slice the mapper passed thickness=None through, routing to
`u_roof`'s Table 18 col 1 default (0.40 W/m²K for age C). That table
assumes joist insulation accessible from the loft — wrong geometry for
PS (Pitched, sloping ceiling) which has no loft access for retrofit.
Add `_resolve_sloping_ceiling_thickness`: when roof_type starts with
"PS" + lodged thickness is None + age ∈ {A,B,C,D} → thickness=0.
Other ages leave None (cascade default), matching Ext1's worksheet
U=0.15 at age M.
Cascade SAP 61.93 → 61.39 (−0.54, expected — uninsulated roof adds
heat loss); cohort 6 certs all green at 1e-4 (none have PS+age≤D);
pyright net-zero baseline preserved.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
07ed871f7b
commit
7a9a8b7ebe
2 changed files with 53 additions and 2 deletions
|
|
@ -306,3 +306,24 @@ def test_summary_001479_ext2_floor_is_exposed_to_external_air() -> None:
|
|||
ext2 = epc.sap_building_parts[2]
|
||||
assert ext2.floor_type == "To external air"
|
||||
assert ext2.sap_floor_dimensions[0].is_exposed_floor is True
|
||||
|
||||
|
||||
def test_summary_001479_ext2_sloping_ceiling_roof_uninsulated_for_pre_1950() -> None:
|
||||
# Arrange — cert 001479 Ext2 §8 lodges "Type: PS Pitched, sloping
|
||||
# ceiling" + "Insulation Thickness: As Built" + age band C (1930-49).
|
||||
# Original 1930s construction had no sloping-ceiling insulation;
|
||||
# worksheet §3 `External roof Ext2 … 2.30` pins U=2.30 (uninsulated
|
||||
# Table 16 row 0). Pre-slice the mapper passed thickness=None through,
|
||||
# routing to `u_roof`'s pitched-roof Table 18 col 1 default (0.40 for
|
||||
# age C, assumes loft-joist retrofit) — wrong geometry for PS.
|
||||
# Ext1's PS roof at age M leaves thickness=None (modern build,
|
||||
# cascade default U=0.15 matches worksheet).
|
||||
pages = _summary_pdf_to_textract_style_pages(_SUMMARY_001479_PDF)
|
||||
site_notes = ElmhurstSiteNotesExtractor(pages).extract()
|
||||
|
||||
# Act
|
||||
epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes)
|
||||
|
||||
# Assert
|
||||
assert epc.sap_building_parts[2].roof_insulation_thickness == 0
|
||||
assert epc.sap_building_parts[1].roof_insulation_thickness is None
|
||||
|
|
|
|||
|
|
@ -1879,6 +1879,33 @@ def _elmhurst_wall_insulation_int(coded: str) -> Optional[int]:
|
|||
_UPPER_FLOOR_HEIGHT_ADD_M: float = 0.25
|
||||
|
||||
|
||||
# Pre-1950 age bands lodged with an "As Built" sloping-ceiling roof get
|
||||
# their original-construction U-value (uninsulated) — original 1930-49
|
||||
# construction had no sloping-ceiling insulation by default. RdSAP10
|
||||
# Table 18 col 1 (the pitched-roof default behind `u_roof`) assumes
|
||||
# joist insulation accessible from the loft, which doesn't apply to PS
|
||||
# sloping-ceiling roofs; without an explicit thickness override the
|
||||
# cascade would understate U for this geometry. Cert 001479 Ext2's
|
||||
# worksheet row `External roof Ext2 … 2.22, 2.30` pins U=2.30 at this
|
||||
# age + geometry combination.
|
||||
_PRE_1950_AGE_CODES: Final[frozenset[str]] = frozenset({"A", "B", "C", "D"})
|
||||
|
||||
|
||||
def _resolve_sloping_ceiling_thickness(
|
||||
roof: ElmhurstRoofDetails, age_code: str,
|
||||
) -> Optional[int]:
|
||||
"""Map an Elmhurst sloping-ceiling roof (`PS Pitched, sloping ceiling`)
|
||||
with no lodged thickness ("As Built") to an explicit 0 mm for pre-
|
||||
1950 age bands. Other lodgements pass through unchanged."""
|
||||
if roof.insulation_thickness_mm is not None:
|
||||
return roof.insulation_thickness_mm
|
||||
if not roof.roof_type.startswith("PS"):
|
||||
return None
|
||||
if age_code in _PRE_1950_AGE_CODES:
|
||||
return 0
|
||||
return None
|
||||
|
||||
|
||||
def _is_floor_exposed_to_unheated_space(location: Optional[str]) -> bool:
|
||||
"""True when the floor sits above an unheated space OR is exposed
|
||||
directly to external air. Both lodgements route through
|
||||
|
|
@ -2118,9 +2145,10 @@ def _map_elmhurst_building_part(
|
|||
]
|
||||
while len(alt_walls) < 2:
|
||||
alt_walls.append(None)
|
||||
age_code = _leading_code(age_band)
|
||||
return SapBuildingPart(
|
||||
identifier=identifier,
|
||||
construction_age_band=_leading_code(age_band),
|
||||
construction_age_band=age_code,
|
||||
wall_construction=_elmhurst_wall_construction_int(walls.wall_type),
|
||||
wall_insulation_type=_elmhurst_wall_insulation_int(walls.insulation),
|
||||
wall_thickness_measured=not walls.thickness_unknown,
|
||||
|
|
@ -2128,7 +2156,9 @@ def _map_elmhurst_building_part(
|
|||
sap_floor_dimensions=floor_dims,
|
||||
wall_thickness_mm=walls.thickness_mm,
|
||||
roof_insulation_location=_strip_code(roof.insulation),
|
||||
roof_insulation_thickness=roof.insulation_thickness_mm,
|
||||
roof_insulation_thickness=_resolve_sloping_ceiling_thickness(
|
||||
roof, age_code,
|
||||
),
|
||||
floor_type=_strip_code(floor.location),
|
||||
floor_construction_type=_strip_code(floor.floor_type),
|
||||
floor_insulation_type_str=_strip_code(floor.insulation),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue