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 "