mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Slice S0380.5: surface insulated_door_u_value from Summary §10 'Average U-value'
Closes the three-layer gap that left the Summary mapper producing
`insulated_door_u_value=None` even though Summary §10 lodges
"Average U-value" / "1.20" explicitly on cert 0380:
1. `datatypes/epc/surveys/elmhurst_site_notes.py` — add
`ElmhurstSiteNotes.insulated_door_u_value: Optional[float] = None`,
placed in the defaulted-field block so existing fixtures that
omit the field still construct without changes.
2. `backend/documents_parser/elmhurst_extractor.py` — add
`_extract_door_u_value` that section-scopes the lookup to
`_section_lines("10.0 Doors:", "11.0 Windows:")` so the bare
"Average U-value" label cannot be shadowed by global U-value
lookups in §7 Walls / §8 Roofs / §9 Floors.
3. `datatypes/epc/domain/mapper.py` — surface
`insulated_door_u_value=survey.insulated_door_u_value` on the
`from_elmhurst_site_notes` path. The comment in
`epc_property_data.py:585` ("Not available in site notes") is now
outdated for Elmhurst Summary PDFs that lodge the explicit value.
Worksheet anchor (dr87-0001-000899.pdf line ref (26)):
Doors insulated 1 NetArea 3.7000 U-value 1.2000 A×U 4.4400 W/K
Forcing function (Slice S0380.1): cert 0380 Summary cascade
`doors_w_per_k` moves from 5.1800 to **4.4400 W/K — exact match
against worksheet line ref (26)**. The +0.74 W/K mis-attribution
was the default door-U fall-through that the lodged 1.20 value
silences. SAP moves 88.1981 (Δ -0.3123) → 88.2746 (Δ -0.2358).
Added focused unit test
`test_summary_0380_surfaces_insulated_door_u_value_1_2` that pins
the mapper boundary directly to the worksheet's lodged U-value 1.2,
so future debuggers can localise regressions in the new extractor /
field / mapper path before walking the full chain.
Pyright net-zero across all four edited files:
datatypes/epc/domain/mapper.py: 32 (baseline)
datatypes/epc/surveys/elmhurst_site_notes.py: 0
backend/documents_parser/elmhurst_extractor.py: 0
backend/documents_parser/tests/test_summary_pdf_mapper_chain.py: 0
Regression suite: 673 pass + 11 fail (vs handover baseline 669 + 10
— net +4 pass for the four GREEN unit tests across Slices S0380.2-5;
the +1 fail vs baseline is the S0380.1 chain test which this slice
moves to Δ -0.2358 but does not yet fully close).
Spec refs:
- SAP 10.2 Table 14 (door U-values: composite-construction default
cascade is silenced when the assessor lodges an explicit measured
U on the cert; routed via `insulated_door_u_value`).
- Cert 0380 worksheet dr87-0001-000899.pdf line ref (26) — the
A×U=4.4400 W/K spec value that this slice closes the Summary
cascade to exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
5fcb594f0a
commit
9faff3e122
4 changed files with 46 additions and 0 deletions
|
|
@ -348,6 +348,20 @@ class ElmhurstSiteNotesExtractor:
|
||||||
lines = [l.strip() for l in main_body.splitlines() if l.strip()]
|
lines = [l.strip() for l in main_body.splitlines() if l.strip()]
|
||||||
return self._floor_details_from_lines(lines)
|
return self._floor_details_from_lines(lines)
|
||||||
|
|
||||||
|
def _extract_door_u_value(self) -> Optional[float]:
|
||||||
|
"""Read the §10 Doors block's "Average U-value" lodging.
|
||||||
|
Scoped to the §10..§11 slice so the global "U-value" labels in
|
||||||
|
Walls/Roofs/Floors can't shadow the door reading. None when the
|
||||||
|
PDF omits the line (e.g. all doors recorded as uninsulated)."""
|
||||||
|
lines = self._section_lines("10.0 Doors:", "11.0 Windows:")
|
||||||
|
raw = self._local_val(lines, "Average U-value")
|
||||||
|
if not raw:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return float(raw.split()[0])
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
return None
|
||||||
|
|
||||||
# RIR surface row: `<name> <length> <height> [<insulation> [<ins_type>]
|
# RIR surface row: `<name> <length> <height> [<insulation> [<ins_type>]
|
||||||
# [<gable_type>] <default_u> <known> <u>]`. The middle slot
|
# [<gable_type>] <default_u> <known> <u>]`. The middle slot
|
||||||
# widths vary by surface kind; we match the four leading numerics
|
# widths vary by surface kind; we match the four leading numerics
|
||||||
|
|
@ -1203,6 +1217,7 @@ class ElmhurstSiteNotesExtractor:
|
||||||
floor=self._extract_floor(),
|
floor=self._extract_floor(),
|
||||||
door_count=self._int_val("Total Number of Doors"),
|
door_count=self._int_val("Total Number of Doors"),
|
||||||
insulated_door_count=self._int_val("Number of Insulated Doors"),
|
insulated_door_count=self._int_val("Number of Insulated Doors"),
|
||||||
|
insulated_door_u_value=self._extract_door_u_value(),
|
||||||
windows=self._extract_windows(),
|
windows=self._extract_windows(),
|
||||||
draught_proofing_percent=self._int_val("Draught Proofing"),
|
draught_proofing_percent=self._int_val("Draught Proofing"),
|
||||||
ventilation=self._extract_ventilation(),
|
ventilation=self._extract_ventilation(),
|
||||||
|
|
|
||||||
|
|
@ -564,6 +564,29 @@ def test_summary_0380_surfaces_wall_insulation_thickness_100mm() -> None:
|
||||||
assert main.wall_insulation_thickness == "100mm"
|
assert main.wall_insulation_thickness == "100mm"
|
||||||
|
|
||||||
|
|
||||||
|
def test_summary_0380_surfaces_insulated_door_u_value_1_2() -> None:
|
||||||
|
# Arrange — cert 0380's Summary §10 Doors block lodges the door
|
||||||
|
# U-value on the "Average U-value" / "1.20" line pair. The dr87
|
||||||
|
# worksheet line ref (26) confirms the spec value: "Doors
|
||||||
|
# insulated 1, NetArea 3.7000 m², U-value 1.2000, A×U 4.4400 W/K".
|
||||||
|
# Without surfacing the lodged U-value the cascade defaults the
|
||||||
|
# door U and overstates `doors_w_per_k` to 5.18 vs worksheet
|
||||||
|
# 4.44 W/K. The comment at
|
||||||
|
# `datatypes/epc/domain/epc_property_data.py:585` claimed the
|
||||||
|
# value was "not available in site notes" — that assertion is
|
||||||
|
# outdated for Elmhurst Summary PDFs which lodge it explicitly.
|
||||||
|
pages = _summary_pdf_to_textract_style_pages(_SUMMARY_000899_PDF)
|
||||||
|
site_notes = ElmhurstSiteNotesExtractor(pages).extract()
|
||||||
|
|
||||||
|
# Act
|
||||||
|
epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes)
|
||||||
|
|
||||||
|
# Assert — float compare with small tolerance (Summary lodges
|
||||||
|
# "1.20" which parses cleanly to 1.2; API lodges 1.2 directly).
|
||||||
|
assert epc.insulated_door_u_value is not None
|
||||||
|
assert abs(epc.insulated_door_u_value - 1.2) < 1e-6
|
||||||
|
|
||||||
|
|
||||||
def test_summary_0380_full_chain_sap_matches_worksheet_pdf_exactly() -> None:
|
def test_summary_0380_full_chain_sap_matches_worksheet_pdf_exactly() -> None:
|
||||||
# Arrange — cert 0380-2471-3250-2596-8761 (Summary_000899.pdf /
|
# Arrange — cert 0380-2471-3250-2596-8761 (Summary_000899.pdf /
|
||||||
# dr87-0001-000899.pdf) is the first heat-pump cert under per-cert
|
# dr87-0001-000899.pdf) is the first heat-pump cert under per-cert
|
||||||
|
|
|
||||||
|
|
@ -352,6 +352,7 @@ class EpcPropertyDataMapper:
|
||||||
open_chimneys_count=survey.ventilation.open_chimneys_count,
|
open_chimneys_count=survey.ventilation.open_chimneys_count,
|
||||||
habitable_rooms_count=survey.habitable_rooms,
|
habitable_rooms_count=survey.habitable_rooms,
|
||||||
insulated_door_count=survey.insulated_door_count,
|
insulated_door_count=survey.insulated_door_count,
|
||||||
|
insulated_door_u_value=survey.insulated_door_u_value,
|
||||||
cfl_fixed_lighting_bulbs_count=survey.lighting.cfl_count,
|
cfl_fixed_lighting_bulbs_count=survey.lighting.cfl_count,
|
||||||
led_fixed_lighting_bulbs_count=survey.lighting.led_count,
|
led_fixed_lighting_bulbs_count=survey.lighting.led_count,
|
||||||
incandescent_fixed_lighting_bulbs_count=survey.lighting.incandescent_count,
|
incandescent_fixed_lighting_bulbs_count=survey.lighting.incandescent_count,
|
||||||
|
|
|
||||||
|
|
@ -340,6 +340,13 @@ class ElmhurstSiteNotes:
|
||||||
# (preserves backward compatibility with the existing fixture).
|
# (preserves backward compatibility with the existing fixture).
|
||||||
extensions: List[ExtensionPart] = field(default_factory=lambda: []) # type: ignore[reportUnknownLambdaType]
|
extensions: List[ExtensionPart] = field(default_factory=lambda: []) # type: ignore[reportUnknownLambdaType]
|
||||||
|
|
||||||
|
# §10 "Average U-value" — lodged when at least one door is
|
||||||
|
# insulated. None when the line is absent from the PDF. Defaulted
|
||||||
|
# so existing fixtures that omit it continue to construct without
|
||||||
|
# changes; the API mapper surfaces this same field directly from
|
||||||
|
# the EPC schema.
|
||||||
|
insulated_door_u_value: Optional[float] = None
|
||||||
|
|
||||||
# §8.1 Rooms in Roof — Main property only in the observed corpus.
|
# §8.1 Rooms in Roof — Main property only in the observed corpus.
|
||||||
# When None the dwelling has no RR storey (a 2-storey house with a
|
# When None the dwelling has no RR storey (a 2-storey house with a
|
||||||
# cold loft instead of a room-in-roof). The mapper translates the
|
# cold loft instead of a room-in-roof). The mapper translates the
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue