From f92d4f26b8694eb85cc11d0aacd4683c4ebbef3e Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 14 Jul 2026 21:38:07 +0000 Subject: [PATCH] =?UTF-8?q?Extract=20the=20PasHub=20per-system=20PV=20bloc?= =?UTF-8?q?k=20and=20diverter=20flag=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "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) --- backend/documents_parser/extractor.py | 34 +++++++++++++++++++ .../tests/test_pashub_sap_accuracy.py | 5 ++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/backend/documents_parser/extractor.py b/backend/documents_parser/extractor.py index fe85c8f59..42ad98a56 100644 --- a/backend/documents_parser/extractor.py +++ b/backend/documents_parser/extractor.py @@ -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?") diff --git a/backend/documents_parser/tests/test_pashub_sap_accuracy.py b/backend/documents_parser/tests/test_pashub_sap_accuracy.py index 34141c47b..b22c44b31 100644 --- a/backend/documents_parser/tests/test_pashub_sap_accuracy.py +++ b/backend/documents_parser/tests/test_pashub_sap_accuracy.py @@ -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 "