From d6a59be950ccaefb6a1cbc45f53374afd05cb22b Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 26 Jun 2026 12:14:00 +0000 Subject: [PATCH] =?UTF-8?q?SolarPotential=20carries=20panel=20dims=20+=20p?= =?UTF-8?q?er-segment=20centre/area=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enrich each SolarRoofSegment from roofSegmentStats (centre + areaMeters2, keyed by segmentIndex) and read panelHeightMeters/panelWidthMeters onto SolarPotential — the geometry the Dwelling-Roof Cap (ADR-0038) needs. All Optional; existing projection + config-selection tests stay green. Co-Authored-By: Claude Opus 4.8 (1M context) --- domain/modelling/solar_potential.py | 65 +++++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 7 deletions(-) diff --git a/domain/modelling/solar_potential.py b/domain/modelling/solar_potential.py index d805e131..cf9c13ad 100644 --- a/domain/modelling/solar_potential.py +++ b/domain/modelling/solar_potential.py @@ -62,6 +62,14 @@ class SolarRoofSegment: azimuth_degrees: float pitch_degrees: float yearly_energy_dc_kwh: float + # Per-segment centre + roof-plane area, enriched from the top-level + # `roofSegmentStats` (keyed by `segmentIndex`) — the per-config + # `roofSegmentSummaries` omit them. Used by the Dwelling-Roof Cap + # (ADR-0038) to rank segments by distance from the dwelling and bound the + # array by usable roof area. None when the stats block lacks the segment. + center_latitude: Optional[float] = None + center_longitude: Optional[float] = None + area_m2: Optional[float] = None @property def sap_orientation(self) -> int: @@ -94,6 +102,11 @@ class SolarPotential: panel_capacity_watts: float max_array_panels_count: int configurations: tuple[SolarPanelConfiguration, ...] + # Physical panel footprint (Google `panelHeightMeters` / `panelWidthMeters`) + # — the Dwelling-Roof Cap (ADR-0038) converts a usable roof-area budget into + # a panel count via this. None for partial blocks lacking the fields. + panel_height_m: Optional[float] = None + panel_width_m: Optional[float] = None @classmethod def from_building_insights( @@ -111,18 +124,46 @@ class SolarPotential: or "panelCapacityWatts" not in solar_potential ): return None + # Per-segment centre + area live on the top-level `roofSegmentStats`, + # keyed by `segmentIndex`; the per-config `roofSegmentSummaries` carry + # only the panel/orientation fields. Build the lookup once. + stats_by_index: dict[int, Mapping[str, Any]] = { + int(stats["segmentIndex"]): stats + for stats in solar_potential.get("roofSegmentStats", []) + } + + def _segment(summary: Mapping[str, Any]) -> SolarRoofSegment: + index: int = int(summary["segmentIndex"]) + stats: Optional[Mapping[str, Any]] = stats_by_index.get(index) + center: Mapping[str, Any] = ( + stats.get("center", {}) if stats is not None else {} + ) + area: Optional[float] = ( + float(stats["stats"]["areaMeters2"]) + if stats is not None and "stats" in stats + else None + ) + return SolarRoofSegment( + segment_index=index, + panels_count=int(summary["panelsCount"]), + azimuth_degrees=float(summary["azimuthDegrees"]), + pitch_degrees=float(summary["pitchDegrees"]), + yearly_energy_dc_kwh=float(summary["yearlyEnergyDcKwh"]), + center_latitude=( + float(center["latitude"]) if "latitude" in center else None + ), + center_longitude=( + float(center["longitude"]) if "longitude" in center else None + ), + area_m2=area, + ) + configurations: tuple[SolarPanelConfiguration, ...] = tuple( SolarPanelConfiguration( panels_count=int(config["panelsCount"]), yearly_energy_dc_kwh=float(config["yearlyEnergyDcKwh"]), segments=tuple( - SolarRoofSegment( - segment_index=int(summary["segmentIndex"]), - panels_count=int(summary["panelsCount"]), - azimuth_degrees=float(summary["azimuthDegrees"]), - pitch_degrees=float(summary["pitchDegrees"]), - yearly_energy_dc_kwh=float(summary["yearlyEnergyDcKwh"]), - ) + _segment(summary) for summary in config.get("roofSegmentSummaries", []) ), ) @@ -132,4 +173,14 @@ class SolarPotential: panel_capacity_watts=float(solar_potential["panelCapacityWatts"]), max_array_panels_count=int(solar_potential["maxArrayPanelsCount"]), configurations=configurations, + panel_height_m=( + float(solar_potential["panelHeightMeters"]) + if "panelHeightMeters" in solar_potential + else None + ), + panel_width_m=( + float(solar_potential["panelWidthMeters"]) + if "panelWidthMeters" in solar_potential + else None + ), )