mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice S0380.127: resolve Elmhurst "No Access" cylinder via RdSAP 10 Table 28
Elmhurst Summary §15.1 sometimes lodges "Cylinder Size: No Access" (the
inaccessible-cylinder lodging form). Pre-slice the mapper strict-raised
`UnmappedElmhurstLabel` because `_ELMHURST_CYLINDER_SIZE_LABEL_TO_SAP10`
only carried the three lodged-size labels (Normal/Medium/Large).
Per RdSAP 10 Specification Table 28 page 55 ("Cylinder size"):
> "Inaccessible:
> - if off-peak electric dual immersion: 210 litres
> - if from solid fuel boiler: 160 litres
> - otherwise: 110 litres"
And per §10.5.1 page 53:
> "An electric immersion is assumed dual in the following cases:
> - cylinder is inaccessible and electricity tariff is dual"
So the 210-L "off-peak electric dual immersion" branch fires automatically
when both (a) cylinder is inaccessible AND (b) water heating is electric
AND (c) meter type is dual / off-peak (no separate dual-immersion lodging
required).
New helper `_resolve_elmhurst_inaccessible_cylinder_size` keys off
§15.0 "Water Heating Fuel Type" + §14.2 "Electricity meter type":
- solid fuel water heating fuel (Anthracite, House coal, Wood, etc.)
→ 160 L → SAP10 cylinder_size enum 3 (Medium)
- "Electricity" + dual/18-hour/24-hour/off-peak meter
→ 210 L → SAP10 cylinder_size enum 4 (Large)
- otherwise → 110 L → SAP10 cylinder_size enum 2 (Normal)
`_elmhurst_cylinder_size_code` extended with optional water_heating_fuel
+ meter_type kwargs; the single call site at line 4459 threads
`survey.water_heating.water_heating_fuel_type` and
`survey.meters.electricity_meter_type`.
Property 001431 (the heating-systems corpus dwelling) lodges `pcdb 1`
with §14.0 Potterton oil boiler (PCDF 716) + §15.0 "Water Heating Fuel
Type: Heating oil" + §14.2 "Electricity meter type: 18 Hour" — water
fuel is oil (not electric, not solid fuel) → "otherwise" branch → 110 L
→ enum 2 (Normal). `pcdb 1` now cascade-executes (corpus tally 34 → 35
OK / 41 populated).
Extended handover suite at HEAD post-slice: **831 pass, 0 fail**
(was 830 + 1 new AAA test).
Pyright net-zero on touched files (45 → 45 — pre-existing errors
unrelated).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
e25aa02109
commit
11ecac94dc
2 changed files with 118 additions and 2 deletions
|
|
@ -236,6 +236,39 @@ def test_summary_001479_mapper_extensions_count_matches_extension_bps() -> None:
|
|||
assert len(epc.sap_building_parts) == 3
|
||||
|
||||
|
||||
def test_summary_001431_pcdb_1_inaccessible_cylinder_resolves_to_normal_per_rdsap_10_table_28() -> None:
|
||||
# Arrange — Heating-systems corpus fixture 001431 / "pcdb 1" lodges
|
||||
# §15.1 "Cylinder Size: No Access" (the Elmhurst inaccessible-cylinder
|
||||
# lodging form). Per RdSAP 10 Specification Table 28 page 55:
|
||||
#
|
||||
# "Inaccessible:
|
||||
# - if off-peak electric dual immersion: 210 litres
|
||||
# - if from solid fuel boiler: 160 litres
|
||||
# - otherwise: 110 litres"
|
||||
#
|
||||
# pcdb 1 lodges §14.0 Main Heating as a Potterton oil boiler (PCDF
|
||||
# 716) + §15.0 Water Heating Fuel Type "Heating oil" → not an
|
||||
# electric dual immersion, not a solid fuel boiler → the spec's
|
||||
# "otherwise" branch → **110 litres** = SAP10 cylinder_size enum 2
|
||||
# (Normal per `_ELMHURST_CYLINDER_SIZE_LABEL_TO_SAP10`).
|
||||
#
|
||||
# Pre-slice the mapper strict-raised `UnmappedElmhurstLabel` on the
|
||||
# "No Access" string because `_elmhurst_cylinder_size_code` only
|
||||
# carried the three lodged-size dict entries (Normal/Medium/Large).
|
||||
summary_pdf = (
|
||||
Path(__file__).parents[3]
|
||||
/ "sap worksheets/heating systems examples/pcdb 1/Summary_001431.pdf"
|
||||
)
|
||||
pages = _summary_pdf_to_textract_style_pages(summary_pdf)
|
||||
site_notes = ElmhurstSiteNotesExtractor(pages).extract()
|
||||
|
||||
# Act
|
||||
epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes)
|
||||
|
||||
# Assert
|
||||
assert epc.sap_heating.cylinder_size == 2
|
||||
|
||||
|
||||
def test_summary_001431_electric_1_underfloor_heating_resolves_to_in_screed_per_rdsap_10_section_10_11() -> None:
|
||||
# Arrange — Heating-systems corpus fixture 001431 / "electric 1" lodges
|
||||
# §14.0 "Heat Emitter: Underfloor Heating" (bare form, no subtype
|
||||
|
|
|
|||
|
|
@ -4127,8 +4127,73 @@ _ELMHURST_CYLINDER_INSULATION_LABEL_TO_SAP10: Dict[str, int] = {
|
|||
}
|
||||
|
||||
|
||||
# Elmhurst §15.0 "Water Heating Fuel Type" labels that route to solid-
|
||||
# fuel Table 32 codes (Anthracite, House coal, Wood logs/pellets, etc.).
|
||||
# Used by `_resolve_elmhurst_inaccessible_cylinder_size` to detect the
|
||||
# "from solid fuel boiler" branch of RdSAP 10 Table 28 page 55.
|
||||
_ELMHURST_SOLID_FUEL_WATER_HEATING_LABELS: frozenset[str] = frozenset({
|
||||
"Anthracite",
|
||||
"House coal",
|
||||
"Manufactured smokeless fuel",
|
||||
"Wood logs",
|
||||
"Wood pellets",
|
||||
"Wood chips",
|
||||
"Dual fuel (mineral and wood)",
|
||||
"Coal",
|
||||
})
|
||||
|
||||
# Elmhurst §14.2 "Electricity meter type" labels that signify off-peak
|
||||
# / dual metering (where an inaccessible electric immersion is assumed
|
||||
# dual per RdSAP 10 §10.5.1 → Table 28 "off-peak electric dual
|
||||
# immersion" 210 L branch).
|
||||
_ELMHURST_DUAL_OFF_PEAK_METER_LABELS: frozenset[str] = frozenset({
|
||||
"Dual",
|
||||
"Dual (24 hour)",
|
||||
"18 Hour",
|
||||
"Off-peak 18 hour",
|
||||
"10 Hour",
|
||||
})
|
||||
|
||||
|
||||
def _resolve_elmhurst_inaccessible_cylinder_size(
|
||||
water_heating_fuel_label: str,
|
||||
meter_type_label: str,
|
||||
) -> int:
|
||||
"""RdSAP 10 Specification Table 28 page 55 — derive cylinder size
|
||||
when §15.1 lodges "No Access" / Inaccessible.
|
||||
|
||||
Spec rule verbatim:
|
||||
|
||||
"Inaccessible:
|
||||
- if off-peak electric dual immersion: 210 litres
|
||||
- if from solid fuel boiler: 160 litres
|
||||
- otherwise: 110 litres"
|
||||
|
||||
Returns SAP10 cylinder_size enum (per
|
||||
`_ELMHURST_CYLINDER_SIZE_LABEL_TO_SAP10`):
|
||||
2 (Normal) → 110 L — modal "otherwise" branch
|
||||
3 (Medium) → 160 L — solid fuel boiler
|
||||
4 (Large) → 210 L — off-peak electric dual immersion
|
||||
|
||||
Per RdSAP 10 §10.5.1: "An electric immersion is assumed dual in the
|
||||
following cases: cylinder is inaccessible and electricity tariff
|
||||
is dual" — so the 210-L branch fires automatically when both
|
||||
conditions hold (no separate "is_dual_immersion" lodging needed).
|
||||
"""
|
||||
if water_heating_fuel_label in _ELMHURST_SOLID_FUEL_WATER_HEATING_LABELS:
|
||||
return 3 # Medium / 160 L
|
||||
is_electric = water_heating_fuel_label.startswith("Electricity")
|
||||
is_off_peak = meter_type_label in _ELMHURST_DUAL_OFF_PEAK_METER_LABELS
|
||||
if is_electric and is_off_peak:
|
||||
return 4 # Large / 210 L
|
||||
return 2 # Normal / 110 L
|
||||
|
||||
|
||||
def _elmhurst_cylinder_size_code(
|
||||
cylinder_size_label: Optional[str], cylinder_present: bool,
|
||||
cylinder_size_label: Optional[str],
|
||||
cylinder_present: bool,
|
||||
water_heating_fuel_label: Optional[str] = None,
|
||||
meter_type_label: Optional[str] = None,
|
||||
) -> Optional[int]:
|
||||
"""Map an Elmhurst §15.1 "Cylinder Size" label to the SAP10
|
||||
cascade enum. Returns None when no cylinder is present or the
|
||||
|
|
@ -4136,9 +4201,25 @@ def _elmhurst_cylinder_size_code(
|
|||
`UnmappedElmhurstLabel` when the label IS lodged but isn't in
|
||||
`_ELMHURST_CYLINDER_SIZE_LABEL_TO_SAP10` — that's a mapper-coverage
|
||||
gap that should be made explicit so the next fixture forces a dict
|
||||
entry, not silently routed off the HW-with-cylinder cascade path."""
|
||||
entry, not silently routed off the HW-with-cylinder cascade path.
|
||||
|
||||
The bare lodging "No Access" (Inaccessible) routes through
|
||||
`_resolve_elmhurst_inaccessible_cylinder_size` per RdSAP 10
|
||||
Table 28 page 55."""
|
||||
if not cylinder_present or cylinder_size_label is None:
|
||||
return None
|
||||
if cylinder_size_label == "No Access":
|
||||
if water_heating_fuel_label is None or meter_type_label is None:
|
||||
raise UnmappedElmhurstLabel(
|
||||
"cylinder_size",
|
||||
(
|
||||
"lodged 'No Access' requires water_heating fuel + "
|
||||
"meter context to apply RdSAP 10 Table 28 (p.55)"
|
||||
),
|
||||
)
|
||||
return _resolve_elmhurst_inaccessible_cylinder_size(
|
||||
water_heating_fuel_label, meter_type_label,
|
||||
)
|
||||
code = _ELMHURST_CYLINDER_SIZE_LABEL_TO_SAP10.get(cylinder_size_label)
|
||||
if code is None:
|
||||
raise UnmappedElmhurstLabel("cylinder_size", cylinder_size_label)
|
||||
|
|
@ -4459,6 +4540,8 @@ def _map_elmhurst_sap_heating(survey: ElmhurstSiteNotes) -> SapHeating:
|
|||
cylinder_size=_elmhurst_cylinder_size_code(
|
||||
survey.water_heating.cylinder_size_label,
|
||||
survey.water_heating.hot_water_cylinder_present,
|
||||
water_heating_fuel_label=survey.water_heating.water_heating_fuel_type,
|
||||
meter_type_label=survey.meters.electricity_meter_type,
|
||||
),
|
||||
cylinder_insulation_type=_elmhurst_cylinder_insulation_code(
|
||||
survey.water_heating.cylinder_insulation_label,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue