mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
A small roof derives Sub-Ladder rungs below Google's smallest config 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
79d7743bb1
commit
1a34e788d1
1 changed files with 64 additions and 1 deletions
|
|
@ -14,6 +14,7 @@ selection, the overlay and `recommend_solar` land in later slices.
|
|||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from dataclasses import replace
|
||||
from typing import Optional
|
||||
|
||||
from datatypes.epc.domain.epc_property_data import (
|
||||
|
|
@ -186,6 +187,66 @@ def _drop_north_segments(config: SolarPanelConfiguration) -> SolarPanelConfigura
|
|||
)
|
||||
|
||||
|
||||
# ADR-0058 — the practical install floor for a Sub-Ladder Configuration; a
|
||||
# roof whose cap resolves below this is never offered PV.
|
||||
_MIN_SUB_LADDER_PANELS = 2
|
||||
|
||||
|
||||
def _resized_to(
|
||||
config: SolarPanelConfiguration, panels: int
|
||||
) -> SolarPanelConfiguration:
|
||||
"""A copy of ``config`` filled to ``panels`` panels, each kept segment's
|
||||
yearly energy scaled pro-rata (conservative: Google places panels
|
||||
best-first within a segment, so the kept panels' true yield is at least
|
||||
the pro-rata figure)."""
|
||||
kept: list[SolarRoofSegment] = []
|
||||
remaining: int = panels
|
||||
for segment in config.segments:
|
||||
if remaining <= 0:
|
||||
break
|
||||
take: int = min(segment.panels_count, remaining)
|
||||
factor: float = take / segment.panels_count
|
||||
kept.append(
|
||||
replace(
|
||||
segment,
|
||||
panels_count=take,
|
||||
yearly_energy_dc_kwh=segment.yearly_energy_dc_kwh * factor,
|
||||
)
|
||||
)
|
||||
remaining -= take
|
||||
return SolarPanelConfiguration(
|
||||
panels_count=sum(segment.panels_count for segment in kept),
|
||||
yearly_energy_dc_kwh=sum(segment.yearly_energy_dc_kwh for segment in kept),
|
||||
segments=tuple(kept),
|
||||
)
|
||||
|
||||
|
||||
def _sub_ladder_configs(
|
||||
potential: SolarPotential, 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."""
|
||||
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:
|
||||
return ()
|
||||
return tuple(
|
||||
_resized_to(smallest, panels)
|
||||
for panels in range(_MIN_SUB_LADDER_PANELS, rung_max + 1)
|
||||
)
|
||||
|
||||
|
||||
def select_conservative_configs(
|
||||
potential: SolarPotential,
|
||||
dwelling_roof_area_m2: Optional[float] = None,
|
||||
|
|
@ -214,7 +275,9 @@ def select_conservative_configs(
|
|||
if trimmed.segments and trimmed.panels_count <= panel_cap
|
||||
]
|
||||
if not feasible:
|
||||
return ()
|
||||
# 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)
|
||||
# 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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue