"""Slice 4 — conservative PV config selection (ADR-0026). From Google's full `solarPanelConfigs` ladder, drop north-facing segments (within 30° of due north), cap usable panels at ~70% of maxArrayPanelsCount (imagery misses obstructions; MCS wants an edge setback), then sample up to five configs spanning min→max by energy so the Optimiser gets a genuine size/cost choice. """ import json from pathlib import Path from typing import Any from domain.modelling.generators.solar_recommendation import ( select_conservative_configs, ) from domain.modelling.solar_potential import ( SolarPanelConfiguration, SolarPotential, SolarRoofSegment, ) _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 _segment(panels: int, azimuth: float, energy: float) -> SolarRoofSegment: return SolarRoofSegment( segment_index=0, panels_count=panels, azimuth_degrees=azimuth, pitch_degrees=30.0, yearly_energy_dc_kwh=energy, ) def test_real_example_samples_five_spanning_configs() -> None: # Arrange potential = SolarPotential.from_building_insights(_insights()) # Act configs = select_conservative_configs(potential) # Assert — five rungs spanning the conservative range, ascending by size, # all ≤ 70% of maxArrayPanelsCount (49 → 34.3) assert [c.panels_count for c in configs] == [4, 12, 19, 26, 34] assert all(c.panels_count <= 0.70 * potential.max_array_panels_count for c in configs) def test_north_facing_segments_are_dropped() -> None: # Arrange — a single config with a due-north plane and a south plane south = _segment(panels=6, azimuth=180.0, energy=2000.0) north = _segment(panels=4, azimuth=5.0, energy=900.0) near_north = _segment(panels=2, azimuth=345.0, energy=400.0) # within 30° of N potential = SolarPotential( panel_capacity_watts=400.0, max_array_panels_count=20, configurations=( SolarPanelConfiguration( panels_count=12, yearly_energy_dc_kwh=3300.0, segments=(south, north, near_north), ), ), ) # Act configs = select_conservative_configs(potential) # Assert — only the south plane survives; counts/energy recomputed to it assert len(configs) == 1 only = configs[0] assert only.panels_count == 6 assert abs(only.yearly_energy_dc_kwh - 2000.0) <= 1e-4 assert [s.azimuth_degrees for s in only.segments] == [180.0] def test_cap_excludes_configs_above_seventy_percent() -> None: # Arrange — max 10 panels → cap 7; a 6-panel and an 8-panel rung potential = SolarPotential( panel_capacity_watts=400.0, max_array_panels_count=10, configurations=( SolarPanelConfiguration( panels_count=6, yearly_energy_dc_kwh=2000.0, segments=(_segment(6, 180.0, 2000.0),), ), SolarPanelConfiguration( panels_count=8, yearly_energy_dc_kwh=2600.0, segments=(_segment(8, 180.0, 2600.0),), ), ), ) # Act configs = select_conservative_configs(potential) # Assert — only the 6-panel rung (≤7) survives assert [c.panels_count for c in configs] == [6] def test_all_north_or_empty_yields_no_configs() -> None: # Arrange — every plane faces north potential = SolarPotential( panel_capacity_watts=400.0, max_array_panels_count=20, configurations=( SolarPanelConfiguration( panels_count=4, yearly_energy_dc_kwh=800.0, segments=(_segment(4, 10.0, 800.0),), ), ), ) # Act configs = select_conservative_configs(potential) # Assert assert configs == ()