diff --git a/domain/modelling/solar_potential.py b/domain/modelling/solar_potential.py new file mode 100644 index 00000000..1d90e14f --- /dev/null +++ b/domain/modelling/solar_potential.py @@ -0,0 +1,124 @@ +"""Solar Potential — the installable PV potential of a dwelling, projected +from a Google Solar ``buildingInsights`` response (ADR-0026). + +The production source of PV array configuration is the Google Solar API: the +raw ``buildingInsights`` JSON is fetched once by Ingestion and persisted as +JSONB (`SolarRepository`), never re-fetched. This module is the strictly-typed +projection Modelling reads over that JSON — the panel-count ladder +(``solarPanelConfigs``), each rung broken into the roof segments the SAP +calculator scores, with Google's continuous azimuth/tilt mapped to the SAP +octant / RdSAP pitch enums. + +`SolarPotential` is *not* the dwelling's existing PV (that lives on the EPC's +``photovoltaic_arrays`` and is empty for a non-PV dwelling); it is the +*potential* the solar Recommendation Generator installs. The Google JSON → +`SolarPotential` mapping is its own validated boundary (CONTEXT: Solar +Potential). +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Mapping + +# Google's `azimuthDegrees` is a compass bearing: 0°=N, 90°=E, 180°=S, 270°=W, +# increasing clockwise. The SAP octant codes (ORIENTATION_BY_SAP10_CODE in the +# calculator) are 1=N, 2=NE, 3=E, 4=SE, 5=S, 6=SW, 7=W, 8=NW — exactly the +# eight 45° compass points in code order, so snapping to the nearest octant and +# adding one yields the SAP code. +_OCTANT_COUNT = 8 +_DEGREES_PER_OCTANT = 45.0 + +# RdSAP 10 §11.1 fixes PV tilt to one of five values; the calculator's +# `_PV_PITCH_DEG_BY_CODE` is the inverse of this. Google reports a continuous +# `pitchDegrees`, so we snap to the nearest fixed tilt and return its code. +_PITCH_CODE_BY_DEGREES: dict[float, int] = {0.0: 1, 30.0: 2, 45.0: 3, 60.0: 4, 90.0: 5} + + +def azimuth_to_sap_octant(azimuth_degrees: float) -> int: + """Bucket a Google compass azimuth (0°=N, clockwise) to the SAP octant code + {1=N, 2=NE, 3=E, 4=SE, 5=S, 6=SW, 7=W, 8=NW}.""" + index: int = round(azimuth_degrees / _DEGREES_PER_OCTANT) % _OCTANT_COUNT + return index + 1 + + +def pitch_to_sap_code(pitch_degrees: float) -> int: + """Snap a Google continuous tilt to the nearest RdSAP 10 §11.1 fixed tilt + and return its code {0°→1, 30°→2, 45°→3, 60°→4, 90°→5}.""" + nearest: float = min( + _PITCH_CODE_BY_DEGREES, key=lambda deg: abs(deg - pitch_degrees) + ) + return _PITCH_CODE_BY_DEGREES[nearest] + + +@dataclass(frozen=True) +class SolarRoofSegment: + """One roof plane within a panel configuration — the panels Google places + on it and the orientation, tilt and expected DC generation that drive the + SAP Appendix M output.""" + + segment_index: int + panels_count: int + azimuth_degrees: float + pitch_degrees: float + yearly_energy_dc_kwh: float + + @property + def sap_orientation(self) -> int: + """The SAP octant code for this plane's azimuth.""" + return azimuth_to_sap_octant(self.azimuth_degrees) + + @property + def sap_pitch_code(self) -> int: + """The RdSAP §11.1 pitch code for this plane's tilt.""" + return pitch_to_sap_code(self.pitch_degrees) + + +@dataclass(frozen=True) +class SolarPanelConfiguration: + """One rung of Google's ``solarPanelConfigs`` ladder: a whole-array layout + of ``panels_count`` panels spread across the roof segments, with the + array's total expected yearly DC generation.""" + + panels_count: int + yearly_energy_dc_kwh: float + segments: tuple[SolarRoofSegment, ...] + + +@dataclass(frozen=True) +class SolarPotential: + """Strictly-typed projection of a Google Solar ``buildingInsights`` + response — the panel ladder and the per-segment geometry Modelling needs to + size, score and cost a PV array (ADR-0026).""" + + panel_capacity_watts: float + max_array_panels_count: int + configurations: tuple[SolarPanelConfiguration, ...] + + @classmethod + def from_building_insights(cls, insights: Mapping[str, Any]) -> "SolarPotential": + """Project a raw Google ``buildingInsights`` response (as persisted by + `SolarRepository`) into a `SolarPotential`.""" + solar_potential: Mapping[str, Any] = insights["solarPotential"] + 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"]), + ) + for summary in config.get("roofSegmentSummaries", []) + ), + ) + for config in solar_potential.get("solarPanelConfigs", []) + ) + return cls( + panel_capacity_watts=float(solar_potential["panelCapacityWatts"]), + max_array_panels_count=int(solar_potential["maxArrayPanelsCount"]), + configurations=configurations, + ) diff --git a/tests/domain/modelling/fixtures/google_building_insights_001431.json b/tests/domain/modelling/fixtures/google_building_insights_001431.json new file mode 100644 index 00000000..76893f69 --- /dev/null +++ b/tests/domain/modelling/fixtures/google_building_insights_001431.json @@ -0,0 +1,2064 @@ +{ + "name": "buildings/ChIJu6gRkhsadkgR3ZyfYd7sHug", + "center": { + "latitude": 51.5930771, + "longitude": -0.17263489999999998 + }, + "regionCode": "GB", + "boundingBox": { + "ne": { + "latitude": 51.5931594, + "longitude": -0.17251819999999998 + }, + "sw": { + "latitude": 51.5930075, + "longitude": -0.172758 + } + }, + "imageryDate": { + "day": 12, + "year": 2022, + "month": 5 + }, + "imageryQuality": "HIGH", + "solarPotential": { + "solarPanels": [ + { + "center": { + "latitude": 51.5930397, + "longitude": -0.1726569 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 1, + "yearlyEnergyDcKwh": 406.4439 + }, + { + "center": { + "latitude": 51.59305080000001, + "longitude": -0.1726366 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 1, + "yearlyEnergyDcKwh": 402.52658 + }, + { + "center": { + "latitude": 51.593044899999995, + "longitude": -0.17262819999999998 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 1, + "yearlyEnergyDcKwh": 404.62482 + }, + { + "center": { + "latitude": 51.5930561, + "longitude": -0.17260789999999998 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 1, + "yearlyEnergyDcKwh": 403.4238 + }, + { + "center": { + "latitude": 51.593061999999996, + "longitude": -0.1726163 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 1, + "yearlyEnergyDcKwh": 402.6263 + }, + { + "center": { + "latitude": 51.5930732, + "longitude": -0.172596 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 1, + "yearlyEnergyDcKwh": 401.73557 + }, + { + "center": { + "latitude": 51.593067299999994, + "longitude": -0.17258750000000003 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 1, + "yearlyEnergyDcKwh": 400.45892 + }, + { + "center": { + "latitude": 51.593038899999996, + "longitude": -0.1726197 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 1, + "yearlyEnergyDcKwh": 397.46198 + }, + { + "center": { + "latitude": 51.593020599999996, + "longitude": -0.1726711 + }, + "orientation": "PORTRAIT", + "segmentIndex": 3, + "yearlyEnergyDcKwh": 397.16306 + }, + { + "center": { + "latitude": 51.5930501, + "longitude": -0.1725994 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 1, + "yearlyEnergyDcKwh": 396.7709 + }, + { + "center": { + "latitude": 51.5930275, + "longitude": -0.17268129999999998 + }, + "orientation": "PORTRAIT", + "segmentIndex": 3, + "yearlyEnergyDcKwh": 394.17886 + }, + { + "center": { + "latitude": 51.5930344, + "longitude": -0.1726915 + }, + "orientation": "PORTRAIT", + "segmentIndex": 3, + "yearlyEnergyDcKwh": 394.34616 + }, + { + "center": { + "latitude": 51.59307510000001, + "longitude": -0.1725536 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 2, + "yearlyEnergyDcKwh": 329.602 + }, + { + "center": { + "latitude": 51.593051599999995, + "longitude": -0.17268 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 0, + "yearlyEnergyDcKwh": 306.43567 + }, + { + "center": { + "latitude": 51.593080199999996, + "longitude": -0.1725441 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 2, + "yearlyEnergyDcKwh": 304.82623 + }, + { + "center": { + "latitude": 51.593092999999996, + "longitude": -0.17256169999999998 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 2, + "yearlyEnergyDcKwh": 307.24442 + }, + { + "center": { + "latitude": 51.5930628, + "longitude": -0.1726598 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 0, + "yearlyEnergyDcKwh": 301.32895 + }, + { + "center": { + "latitude": 51.593068599999995, + "longitude": -0.1726681 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 0, + "yearlyEnergyDcKwh": 305.9167 + }, + { + "center": { + "latitude": 51.5930799, + "longitude": -0.1726479 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 0, + "yearlyEnergyDcKwh": 304.88898 + }, + { + "center": { + "latitude": 51.593057, + "longitude": -0.17265139999999998 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 0, + "yearlyEnergyDcKwh": 304.3315 + }, + { + "center": { + "latitude": 51.5930682, + "longitude": -0.17263119999999998 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 0, + "yearlyEnergyDcKwh": 305.38583 + }, + { + "center": { + "latitude": 51.593091099999995, + "longitude": -0.1726277 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 0, + "yearlyEnergyDcKwh": 303.91528 + }, + { + "center": { + "latitude": 51.5931024, + "longitude": -0.1726074 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 0, + "yearlyEnergyDcKwh": 303.26538 + }, + { + "center": { + "latitude": 51.593108199999996, + "longitude": -0.17261579999999999 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 0, + "yearlyEnergyDcKwh": 304.74707 + }, + { + "center": { + "latitude": 51.593114, + "longitude": -0.1726242 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 0, + "yearlyEnergyDcKwh": 302.9333 + }, + { + "center": { + "latitude": 51.59307450000001, + "longitude": -0.17267649999999998 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 0, + "yearlyEnergyDcKwh": 302.04916 + }, + { + "center": { + "latitude": 51.593074099999995, + "longitude": -0.1726395 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 0, + "yearlyEnergyDcKwh": 301.7327 + }, + { + "center": { + "latitude": 51.5930853, + "longitude": -0.1726193 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 0, + "yearlyEnergyDcKwh": 301.3263 + }, + { + "center": { + "latitude": 51.5930574, + "longitude": -0.1726883 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 0, + "yearlyEnergyDcKwh": 301.02057 + }, + { + "center": { + "latitude": 51.59309700000001, + "longitude": -0.17263599999999998 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 0, + "yearlyEnergyDcKwh": 300.82922 + }, + { + "center": { + "latitude": 51.593085699999996, + "longitude": -0.17265629999999998 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 0, + "yearlyEnergyDcKwh": 300.44504 + }, + { + "center": { + "latitude": 51.593096599999996, + "longitude": -0.1725991 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 0, + "yearlyEnergyDcKwh": 299.17642 + }, + { + "center": { + "latitude": 51.5931028, + "longitude": -0.1726444 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 0, + "yearlyEnergyDcKwh": 296.2391 + }, + { + "center": { + "latitude": 51.5930916, + "longitude": -0.1726646 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 0, + "yearlyEnergyDcKwh": 296.18088 + }, + { + "center": { + "latitude": 51.593063199999996, + "longitude": -0.17269669999999998 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 0, + "yearlyEnergyDcKwh": 293.59402 + }, + { + "center": { + "latitude": 51.59305200000001, + "longitude": -0.17271689999999998 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 0, + "yearlyEnergyDcKwh": 305.23026 + }, + { + "center": { + "latitude": 51.5930578, + "longitude": -0.1727253 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 0, + "yearlyEnergyDcKwh": 301.22372 + }, + { + "center": { + "latitude": 51.5930691, + "longitude": -0.1727051 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 0, + "yearlyEnergyDcKwh": 292.2862 + }, + { + "center": { + "latitude": 51.5930803, + "longitude": -0.1726848 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 0, + "yearlyEnergyDcKwh": 290.64398 + }, + { + "center": { + "latitude": 51.593119900000005, + "longitude": -0.1726325 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 0, + "yearlyEnergyDcKwh": 289.80667 + }, + { + "center": { + "latitude": 51.5931086, + "longitude": -0.1726527 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 0, + "yearlyEnergyDcKwh": 288.4848 + }, + { + "center": { + "latitude": 51.5931253, + "longitude": -0.1726039 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 0, + "yearlyEnergyDcKwh": 282.09225 + }, + { + "center": { + "latitude": 51.593063699999995, + "longitude": -0.1727336 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 0, + "yearlyEnergyDcKwh": 279.7994 + }, + { + "center": { + "latitude": 51.593074900000005, + "longitude": -0.1727134 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 0, + "yearlyEnergyDcKwh": 275.29916 + }, + { + "center": { + "latitude": 51.5930981, + "longitude": -0.1725522 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 2, + "yearlyEnergyDcKwh": 275.05432 + }, + { + "center": { + "latitude": 51.593097400000005, + "longitude": -0.172673 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 0, + "yearlyEnergyDcKwh": 268.4053 + }, + { + "center": { + "latitude": 51.5930861, + "longitude": -0.1726932 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 0, + "yearlyEnergyDcKwh": 268.11542 + }, + { + "center": { + "latitude": 51.5931109, + "longitude": -0.1725698 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 2, + "yearlyEnergyDcKwh": 263.44928 + }, + { + "center": { + "latitude": 51.5931311, + "longitude": -0.1726123 + }, + "orientation": "LANDSCAPE", + "segmentIndex": 0, + "yearlyEnergyDcKwh": 257.0994 + } + ], + "buildingStats": { + "areaMeters2": 184.19221, + "groundAreaMeters2": 142.64, + "sunshineQuantiles": [ + 313.60382, + 508.0561, + 668.3278, + 728.89105, + 742.7094, + 752.1113, + 761.09265, + 780.59656, + 976.15955, + 1005.2664, + 1051.9331 + ] + }, + "wholeRoofStats": { + "areaMeters2": 141.65375, + "groundAreaMeters2": 118.49, + "sunshineQuantiles": [ + 331.97247, + 666.4743, + 728.30896, + 740.458, + 749.04724, + 756.74945, + 764.77496, + 821.7157, + 995.1525, + 1007.2804, + 1051.9331 + ] + }, + "panelWidthMeters": 1.045, + "roofSegmentStats": [ + { + "stats": { + "areaMeters2": 81.86243, + "groundAreaMeters2": 68.14, + "sunshineQuantiles": [ + 331.97247, + 647.7773, + 723.54095, + 731.9598, + 739.6582, + 745.87164, + 750.24554, + 755.70526, + 761.72235, + 773.11017, + 1019.3838 + ] + }, + "center": { + "latitude": 51.5930848, + "longitude": -0.1726575 + }, + "boundingBox": { + "ne": { + "latitude": 51.593143299999994, + "longitude": -0.1725812 + }, + "sw": { + "latitude": 51.5930404, + "longitude": -0.1727561 + } + }, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "planeHeightAtCenterMeters": 95.861626 + }, + { + "stats": { + "areaMeters2": 28.93977, + "groundAreaMeters2": 24.57, + "sunshineQuantiles": [ + 515.37933, + 862.21326, + 989.1015, + 999.8666, + 1002.4724, + 1004.9659, + 1007.32605, + 1009.34906, + 1011.6587, + 1014.892, + 1051.9331 + ] + }, + "center": { + "latitude": 51.59305320000001, + "longitude": -0.17261469999999998 + }, + "boundingBox": { + "ne": { + "latitude": 51.59309, + "longitude": -0.17257 + }, + "sw": { + "latitude": 51.593024799999995, + "longitude": -0.1726705 + } + }, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "planeHeightAtCenterMeters": 96.91601 + }, + { + "stats": { + "areaMeters2": 18.239426, + "groundAreaMeters2": 15.14, + "sunshineQuantiles": [ + 353.97595, + 418.49237, + 657.0197, + 750.6194, + 756.22955, + 760.17053, + 764.10645, + 768.5595, + 775.32855, + 804.11694, + 1034.555 + ] + }, + "center": { + "latitude": 51.5930914, + "longitude": -0.1725575 + }, + "boundingBox": { + "ne": { + "latitude": 51.593123899999995, + "longitude": -0.17252019999999998 + }, + "sw": { + "latitude": 51.5930659, + "longitude": -0.17258569999999998 + } + }, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "planeHeightAtCenterMeters": 96.632774 + }, + { + "stats": { + "areaMeters2": 12.612122, + "groundAreaMeters2": 10.64, + "sunshineQuantiles": [ + 491.0724, + 674.4169, + 747.51666, + 955.5824, + 982.1831, + 991.83075, + 995.9849, + 997.93945, + 1001.9863, + 1016.61017, + 1039.1875 + ] + }, + "center": { + "latitude": 51.593027799999994, + "longitude": -0.17268129999999998 + }, + "boundingBox": { + "ne": { + "latitude": 51.593043599999994, + "longitude": -0.1726451 + }, + "sw": { + "latitude": 51.593010400000004, + "longitude": -0.172722 + } + }, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "planeHeightAtCenterMeters": 96.54124 + } + ], + "panelHeightMeters": 1.879, + "solarPanelConfigs": [ + { + "panelsCount": 4, + "yearlyEnergyDcKwh": 1617.0192, + "roofSegmentSummaries": [ + { + "panelsCount": 4, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 1617.0192 + } + ] + }, + { + "panelsCount": 5, + "yearlyEnergyDcKwh": 2019.6454, + "roofSegmentSummaries": [ + { + "panelsCount": 5, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 2019.6455 + } + ] + }, + { + "panelsCount": 6, + "yearlyEnergyDcKwh": 2421.3809, + "roofSegmentSummaries": [ + { + "panelsCount": 6, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 2421.381 + } + ] + }, + { + "panelsCount": 7, + "yearlyEnergyDcKwh": 2821.8398, + "roofSegmentSummaries": [ + { + "panelsCount": 7, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 2821.84 + } + ] + }, + { + "panelsCount": 8, + "yearlyEnergyDcKwh": 3219.3018, + "roofSegmentSummaries": [ + { + "panelsCount": 8, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3219.302 + } + ] + }, + { + "panelsCount": 9, + "yearlyEnergyDcKwh": 3616.4648, + "roofSegmentSummaries": [ + { + "panelsCount": 8, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3219.302 + }, + { + "panelsCount": 1, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 397.16306 + } + ] + }, + { + "panelsCount": 10, + "yearlyEnergyDcKwh": 4013.2358, + "roofSegmentSummaries": [ + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 1, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 397.16306 + } + ] + }, + { + "panelsCount": 11, + "yearlyEnergyDcKwh": 4407.4146, + "roofSegmentSummaries": [ + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 2, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 791.3419 + } + ] + }, + { + "panelsCount": 12, + "yearlyEnergyDcKwh": 4801.7607, + "roofSegmentSummaries": [ + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 13, + "yearlyEnergyDcKwh": 5131.363, + "roofSegmentSummaries": [ + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 1, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 329.602 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 14, + "yearlyEnergyDcKwh": 5437.7983, + "roofSegmentSummaries": [ + { + "panelsCount": 1, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 306.43567 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 1, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 329.602 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 15, + "yearlyEnergyDcKwh": 5742.6245, + "roofSegmentSummaries": [ + { + "panelsCount": 1, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 306.43567 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 2, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 634.4282 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 16, + "yearlyEnergyDcKwh": 6049.869, + "roofSegmentSummaries": [ + { + "panelsCount": 1, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 306.43567 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 3, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 941.67267 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 17, + "yearlyEnergyDcKwh": 6351.198, + "roofSegmentSummaries": [ + { + "panelsCount": 2, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 607.7646 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 3, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 941.67267 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 18, + "yearlyEnergyDcKwh": 6657.1147, + "roofSegmentSummaries": [ + { + "panelsCount": 3, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 913.6813 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 3, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 941.67267 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 19, + "yearlyEnergyDcKwh": 6962.004, + "roofSegmentSummaries": [ + { + "panelsCount": 4, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 1218.5702 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 3, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 941.67267 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 20, + "yearlyEnergyDcKwh": 7266.3354, + "roofSegmentSummaries": [ + { + "panelsCount": 5, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 1522.9017 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 3, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 941.67267 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 21, + "yearlyEnergyDcKwh": 7571.721, + "roofSegmentSummaries": [ + { + "panelsCount": 6, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 1828.2876 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 3, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 941.67267 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 22, + "yearlyEnergyDcKwh": 7875.636, + "roofSegmentSummaries": [ + { + "panelsCount": 7, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 2132.203 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 3, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 941.67267 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 23, + "yearlyEnergyDcKwh": 8178.902, + "roofSegmentSummaries": [ + { + "panelsCount": 8, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 2435.4683 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 3, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 941.67267 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 24, + "yearlyEnergyDcKwh": 8483.648, + "roofSegmentSummaries": [ + { + "panelsCount": 9, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 2740.2153 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 3, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 941.67267 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 25, + "yearlyEnergyDcKwh": 8786.582, + "roofSegmentSummaries": [ + { + "panelsCount": 10, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 3043.1487 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 3, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 941.67267 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 26, + "yearlyEnergyDcKwh": 9088.631, + "roofSegmentSummaries": [ + { + "panelsCount": 11, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 3345.1978 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 3, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 941.67267 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 27, + "yearlyEnergyDcKwh": 9390.364, + "roofSegmentSummaries": [ + { + "panelsCount": 12, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 3646.9304 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 3, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 941.67267 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 28, + "yearlyEnergyDcKwh": 9691.69, + "roofSegmentSummaries": [ + { + "panelsCount": 13, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 3948.2566 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 3, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 941.67267 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 29, + "yearlyEnergyDcKwh": 9992.711, + "roofSegmentSummaries": [ + { + "panelsCount": 14, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 4249.2773 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 3, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 941.67267 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 30, + "yearlyEnergyDcKwh": 10293.54, + "roofSegmentSummaries": [ + { + "panelsCount": 15, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 4550.1064 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 3, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 941.67267 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 31, + "yearlyEnergyDcKwh": 10593.985, + "roofSegmentSummaries": [ + { + "panelsCount": 16, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 4850.5513 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 3, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 941.67267 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 32, + "yearlyEnergyDcKwh": 10893.161, + "roofSegmentSummaries": [ + { + "panelsCount": 17, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 5149.7275 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 3, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 941.67267 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 33, + "yearlyEnergyDcKwh": 11189.4, + "roofSegmentSummaries": [ + { + "panelsCount": 18, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 5445.967 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 3, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 941.67267 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 34, + "yearlyEnergyDcKwh": 11485.581, + "roofSegmentSummaries": [ + { + "panelsCount": 19, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 5742.1475 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 3, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 941.67267 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 35, + "yearlyEnergyDcKwh": 11779.176, + "roofSegmentSummaries": [ + { + "panelsCount": 20, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 6035.7417 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 3, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 941.67267 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 36, + "yearlyEnergyDcKwh": 12084.406, + "roofSegmentSummaries": [ + { + "panelsCount": 21, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 6340.972 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 3, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 941.67267 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 37, + "yearlyEnergyDcKwh": 12385.63, + "roofSegmentSummaries": [ + { + "panelsCount": 22, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 6642.196 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 3, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 941.67267 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 38, + "yearlyEnergyDcKwh": 12677.916, + "roofSegmentSummaries": [ + { + "panelsCount": 23, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 6934.482 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 3, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 941.67267 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 39, + "yearlyEnergyDcKwh": 12968.56, + "roofSegmentSummaries": [ + { + "panelsCount": 24, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 7225.126 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 3, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 941.67267 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 40, + "yearlyEnergyDcKwh": 13258.366, + "roofSegmentSummaries": [ + { + "panelsCount": 25, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 7514.9326 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 3, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 941.67267 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 41, + "yearlyEnergyDcKwh": 13546.852, + "roofSegmentSummaries": [ + { + "panelsCount": 26, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 7803.4175 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 3, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 941.67267 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 42, + "yearlyEnergyDcKwh": 13828.943, + "roofSegmentSummaries": [ + { + "panelsCount": 27, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 8085.51 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 3, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 941.67267 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 43, + "yearlyEnergyDcKwh": 14108.743, + "roofSegmentSummaries": [ + { + "panelsCount": 28, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 8365.31 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 3, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 941.67267 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 44, + "yearlyEnergyDcKwh": 14384.042, + "roofSegmentSummaries": [ + { + "panelsCount": 29, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 8640.608 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 3, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 941.67267 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 45, + "yearlyEnergyDcKwh": 14659.097, + "roofSegmentSummaries": [ + { + "panelsCount": 29, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 8640.608 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 4, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 1216.727 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 46, + "yearlyEnergyDcKwh": 14927.502, + "roofSegmentSummaries": [ + { + "panelsCount": 30, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 8909.014 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 4, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 1216.727 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 47, + "yearlyEnergyDcKwh": 15195.617, + "roofSegmentSummaries": [ + { + "panelsCount": 31, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 9177.129 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 4, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 1216.727 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 48, + "yearlyEnergyDcKwh": 15459.066, + "roofSegmentSummaries": [ + { + "panelsCount": 31, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 9177.129 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 5, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 1480.1764 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + }, + { + "panelsCount": 49, + "yearlyEnergyDcKwh": 15716.166, + "roofSegmentSummaries": [ + { + "panelsCount": 32, + "pitchDegrees": 33.65681, + "segmentIndex": 0, + "azimuthDegrees": 316.0337, + "yearlyEnergyDcKwh": 9434.229 + }, + { + "panelsCount": 9, + "pitchDegrees": 31.896425, + "segmentIndex": 1, + "azimuthDegrees": 136.27895, + "yearlyEnergyDcKwh": 3616.073 + }, + { + "panelsCount": 5, + "pitchDegrees": 33.894073, + "segmentIndex": 2, + "azimuthDegrees": 47.24721, + "yearlyEnergyDcKwh": 1480.1764 + }, + { + "panelsCount": 3, + "pitchDegrees": 32.474247, + "segmentIndex": 3, + "azimuthDegrees": 225.10983, + "yearlyEnergyDcKwh": 1185.6881 + } + ] + } + ], + "panelCapacityWatts": 400, + "panelLifetimeYears": 20, + "maxArrayAreaMeters2": 96.21419, + "maxArrayPanelsCount": 49, + "maxSunshineHoursPerYear": 1015.6337, + "carbonOffsetFactorKgPerMwh": 478.99942 + }, + "imageryProcessedDate": { + "day": 18, + "year": 2024, + "month": 4 + } +} \ No newline at end of file diff --git a/tests/domain/modelling/test_solar_potential.py b/tests/domain/modelling/test_solar_potential.py new file mode 100644 index 00000000..1561242f --- /dev/null +++ b/tests/domain/modelling/test_solar_potential.py @@ -0,0 +1,102 @@ +"""Slice 2 — the typed `SolarPotential` projection over a Google Solar +`buildingInsights` response (ADR-0026). + +Pins the orientation/pitch mappings and the projection against the real +London `buildingInsights` example (mirrored into fixtures from the +user-provided RTF). +""" + +import json +from pathlib import Path +from typing import Any + +from domain.modelling.solar_potential import ( + SolarPotential, + azimuth_to_sap_octant, + pitch_to_sap_code, +) + +_FIXTURE: Path = ( + Path(__file__).resolve().parent + / "fixtures" + / "google_building_insights_001431.json" +) + + +def _insights() -> dict[str, Any]: + with _FIXTURE.open(encoding="utf-8") as handle: + data: dict[str, Any] = json.load(handle) + return data + + +def test_azimuth_to_sap_octant_cardinals_and_diagonals() -> None: + # Arrange / Act / Assert — Google azimuth 0=N clockwise → SAP octant code + assert azimuth_to_sap_octant(0.0) == 1 # N + assert azimuth_to_sap_octant(45.0) == 2 # NE + assert azimuth_to_sap_octant(90.0) == 3 # E + assert azimuth_to_sap_octant(135.0) == 4 # SE + assert azimuth_to_sap_octant(180.0) == 5 # S + assert azimuth_to_sap_octant(225.0) == 6 # SW + assert azimuth_to_sap_octant(270.0) == 7 # W + assert azimuth_to_sap_octant(315.0) == 8 # NW + assert azimuth_to_sap_octant(360.0) == 1 # wraps to N + + +def test_pitch_to_sap_code_snaps_to_rdsap_enum() -> None: + # Arrange / Act / Assert — RdSAP 10 §11.1 fixed tilts + assert pitch_to_sap_code(0.0) == 1 + assert pitch_to_sap_code(30.0) == 2 + assert pitch_to_sap_code(45.0) == 3 + assert pitch_to_sap_code(60.0) == 4 + assert pitch_to_sap_code(90.0) == 5 + # Real Google pitches (~32-34°) snap to the 30° code + assert pitch_to_sap_code(33.65681) == 2 + assert pitch_to_sap_code(31.896425) == 2 + + +def test_projection_reads_potential_level_fields() -> None: + # Arrange + insights = _insights() + + # Act + potential = SolarPotential.from_building_insights(insights) + + # Assert + assert abs(potential.panel_capacity_watts - 400.0) <= 1e-4 + assert potential.max_array_panels_count == 49 + assert len(potential.configurations) == 46 + + +def test_projection_first_config_single_segment() -> None: + # Arrange + insights = _insights() + + # Act + potential = SolarPotential.from_building_insights(insights) + first = potential.configurations[0] + + # Assert — the smallest rung: 4 panels on one SE roof plane + assert first.panels_count == 4 + assert len(first.segments) == 1 + segment = first.segments[0] + assert segment.segment_index == 1 + assert segment.panels_count == 4 + assert abs(segment.azimuth_degrees - 136.27895) <= 1e-4 + assert abs(segment.yearly_energy_dc_kwh - 1617.0192) <= 1e-4 + assert segment.sap_orientation == 4 # SE + assert segment.sap_pitch_code == 2 # ~32° → 30° + + +def test_projection_largest_config_spans_all_segments() -> None: + # Arrange + insights = _insights() + + # Act + potential = SolarPotential.from_building_insights(insights) + largest = potential.configurations[-1] + + # Assert — the 49-panel rung spans all four roof planes + assert largest.panels_count == 49 + assert sum(s.panels_count for s in largest.segments) == 49 + octants = {s.sap_orientation for s in largest.segments} + assert octants == {8, 4, 2, 6} # NW, SE, NE, SW