From fbedf4541b0423cc42241477a2b18ead0ba60e56 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 7 Jul 2026 22:10:37 +0000 Subject: [PATCH] =?UTF-8?q?A=20small=20roof=20derives=20Sub-Ladder=20rungs?= =?UTF-8?q?=20below=20Google's=20smallest=20config=20=F0=9F=9F=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit North-drop now runs once: the selection computes the usable (north-dropped) ladder, feasibility filters it, and Sub-Ladder derivation consumes it. Co-Authored-By: Claude Fable 5 --- .../generators/solar_recommendation.py | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/domain/modelling/generators/solar_recommendation.py b/domain/modelling/generators/solar_recommendation.py index 6b5b18f4a..110599a0b 100644 --- a/domain/modelling/generators/solar_recommendation.py +++ b/domain/modelling/generators/solar_recommendation.py @@ -228,25 +228,18 @@ def _resized_to( def _sub_ladder_configs( - potential: SolarPotential, panel_cap: float + usable: list[SolarPanelConfiguration], panel_cap: float ) -> tuple[SolarPanelConfiguration, ...]: """Sub-Ladder Configurations (ADR-0058): rungs derived BELOW Google's smallest offered config, for when nothing on the ladder fits under the caps — a small roof gets the array its caps allow instead of no PV at all. One rung per whole panel count from the install floor (2) up to - ``floor(panel_cap)``, derived from the smallest north-dropped config.""" + ``floor(panel_cap)``, derived from the smallest of the (already + north-dropped) ``usable`` configs.""" rung_max: int = math.floor(panel_cap) - if rung_max < _MIN_SUB_LADDER_PANELS: - return () - smallest: Optional[SolarPanelConfiguration] = None - for config in potential.configurations: - dropped = _drop_north_segments(config) - if not dropped.segments: - continue - if smallest is None or dropped.panels_count < smallest.panels_count: - smallest = dropped - if smallest is None: + if rung_max < _MIN_SUB_LADDER_PANELS or not usable: return () + smallest: SolarPanelConfiguration = min(usable, key=lambda c: c.panels_count) return tuple( _resized_to(smallest, panels) for panels in range(_MIN_SUB_LADDER_PANELS, rung_max + 1) @@ -274,16 +267,19 @@ def select_conservative_configs( ) if roof_cap is not None: panel_cap = min(panel_cap, roof_cap) - feasible: list[SolarPanelConfiguration] = [ + usable: list[SolarPanelConfiguration] = [ trimmed for config in potential.configurations for trimmed in (_drop_north_segments(config),) - if trimmed.segments and trimmed.panels_count <= panel_cap + if trimmed.segments + ] + feasible: list[SolarPanelConfiguration] = [ + config for config in usable if config.panels_count <= panel_cap ] if not feasible: # Nothing on Google's ladder fits under the caps — a small roof, not # an unusable one. Derive Sub-Ladder Configurations (ADR-0058). - return _sub_ladder_configs(potential, panel_cap) + return _sub_ladder_configs(usable, panel_cap) # Collapse rungs that trimmed to the same usable size (north-drop can make # distinct original rungs coincide), keeping the higher-generation layout — # the Optimiser's dial is panel count (≈ kWp ≈ cost), so duplicates of the