Dwelling-Roof Cap bounds the PV array to the dwelling's own roof 🟥

select_conservative_configs must accept the dwelling's roof area and cap panels
to its usable roof (ADR-0038) — bounding a 55m² dwelling to ~16 panels under
Google footprint conflation, while staying a no-op on correctly-matched homes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-26 12:15:28 +00:00
parent d6a59be950
commit 904c205e3a

View file

@ -111,6 +111,57 @@ def test_cap_excludes_configs_above_seventy_percent() -> None:
assert [c.panels_count for c in configs] == [6]
def _south(panels: int) -> SolarRoofSegment:
return _segment(panels=panels, azimuth=180.0, energy=panels * 100.0)
def _potential_with_panel_dims(
max_panels: int, panel_counts: tuple[int, ...]
) -> SolarPotential:
# Google panel footprint 1.879 × 1.045 ≈ 1.964 m².
return SolarPotential(
panel_capacity_watts=400.0,
max_array_panels_count=max_panels,
configurations=tuple(
SolarPanelConfiguration(
panels_count=n,
yearly_energy_dc_kwh=n * 100.0,
segments=(_south(n),),
)
for n in panel_counts
),
panel_height_m=1.879,
panel_width_m=1.045,
)
def test_dwelling_roof_cap_bounds_a_small_dwelling() -> None:
# ADR-0038: Google's maxArrayPanelsCount (58) reflects a conflated whole-
# building roof; a 55 m² dwelling's own usable roof (≈ 55/cos30° × 0.5 ≈
# 32 m² ≈ 16 panels) must bound the array, well below the 0.70×58 ≈ 40 cap.
potential = _potential_with_panel_dims(58, (4, 12, 20, 30, 41, 58))
# Google cap alone allows up to the 30-panel rung (41/58 exceed 0.70×58).
assert max(c.panels_count for c in select_conservative_configs(potential)) == 30
capped = select_conservative_configs(potential, dwelling_roof_area_m2=55.0)
assert capped # still offers the small rungs
assert all(c.panels_count <= 16 for c in capped)
assert max(c.panels_count for c in capped) == 12
def test_dwelling_roof_cap_is_a_no_op_on_a_matched_home() -> None:
# ADR-0038: on a correctly-matched home Google's roof ≈ the dwelling's, so
# the area budget is ≳ what Google offers and the cap does NOT bite.
potential = _potential_with_panel_dims(58, (4, 12, 20, 30, 41, 58))
baseline = [c.panels_count for c in select_conservative_configs(potential)]
matched = select_conservative_configs(potential, dwelling_roof_area_m2=300.0)
assert [c.panels_count for c in matched] == baseline
def test_all_north_or_empty_yields_no_configs() -> None:
# Arrange — every plane faces north
potential = SolarPotential(