Extract the PasHub per-system PV block and diverter flag 🟩

"Photovoltaic array kWp Known? Yes" lodges a per-system block (kWp, pitch,
orientation, overshading) instead of the percent-roof estimate; the extractor
only read the estimate, so measured PV never reached the mapper and the
Appendix M credit was silently zero (issue #1590 bug 1). Harness MAE
2.632 -> 2.571 (fixture 507639151843's 13.5-SAP under-rate closes to +0.5);
ceiling ratcheted to 2.58.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-07-14 21:38:07 +00:00
parent 80de9b0c04
commit f92d4f26b8
2 changed files with 38 additions and 1 deletions

View file

@ -18,6 +18,7 @@ from datatypes.epc.surveys.pashub_rdsap_site_notes import (
MainBuildingMeasurements,
MainHeating,
PasHubRdSapSiteNotes,
PvArrayDetail,
Renewables,
RoomCountElements,
RoofSpace,
@ -435,8 +436,41 @@ class PasHubRdSapSiteNotesExtractor:
hydro=self._bool_in(r_section, "Is the dwelling connected to Hydro?"),
pv_connection=pv_connection,
percent_roof_covered_pv=percent_roof,
pv_diverter_present=self._optional_bool_in(
r_section, "Is there a PV diverter?"
),
pv_arrays=self._pv_arrays_in(r_section),
)
def _pv_arrays_in(self, r_section: List[str]) -> Optional[List[PvArrayDetail]]:
"""The "Photovoltaic array kWp Known? Yes" form lodges a per-system
block instead of the percent-roof estimate: "Number of PV systems?"
then "PV {n}: kWp value / Photovoltaic Pitch / Orientation /
Overshading" per system. Dropped detail silently zeroes the Appendix M
PV credit downstream (issue #1590 bug 1)."""
systems_raw = self._get_in(r_section, "Number of PV systems?")
if systems_raw is None:
return None
arrays: List[PvArrayDetail] = []
for n in range(1, int(systems_raw) + 1):
kwp_raw = self._get_in(r_section, f"PV {n}: kWp value:")
pitch_raw = self._get_in(r_section, f"PV {n}: Photovoltaic Pitch?")
arrays.append(
PvArrayDetail(
kwp=float(kwp_raw.split()[0]) if kwp_raw else None,
pitch_degrees=(
int(pitch_raw.split()[0]) if pitch_raw else None
),
orientation=self._get_in(
r_section, f"PV {n}: Photovoltaic Orientation?"
),
overshading=self._get_in(
r_section, f"PV {n}: Photovoltaic Overshading:"
),
)
)
return arrays or None
def extract_room_count_elements(self) -> RoomCountElements:
rce_section = self._section("Room Count Elements", "Customer Response")
heated_rooms_raw = self._get_in(rce_section, "Number of heated rooms?")

View file

@ -84,7 +84,10 @@ _MIN_WITHIN_HALF: float = 0.12
# Ratcheted 2.71 -> 2.64 by #1590 bug 2 (roof "Insulation At: None" -> explicit
# zero thickness): observed MAE 2.632 — the two ~7-SAP roof over-raters
# (507665533138 / 507670893756) correcting is exactly 14/205 ≈ 0.07 of MAE.
_MAX_SAP_MAE: float = 2.64
# Ratcheted 2.64 -> 2.58 by #1590 bug 1 (surveyed PV arrays now reach the
# Appendix M input): observed MAE 2.571 — 507639151843's 13.5-SAP zero-PV-credit
# under-rate closing.
_MAX_SAP_MAE: float = 2.58
_KNOWN_GAP_REASON = (
"pashub `from_site_notes` mapper does not int-code a main-heating "