mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Slice 3 of the Solar PV Recommendation Generator (ADR-0026). Per roof segment,
back-solve the effective overshading factor ZPV from Google's expected
generation against SAP's own unshaded annual output:
ZPV = (yearlyEnergyDcKwh × 0.955) / (0.8 × kWp × S)
reusing the calculator's Appendix U3.3 annual solar radiation S via a new
public seam `pv_annual_solar_radiation_kwh_per_m2`. Dividing Google's
generation by SAP's S cancels orientation/tilt and isolates shading; the
result snaps to the RdSAP bucket {1:1.0, 2:0.8, 3:0.5, 4:0.35} via the
ADR-0026 midpoint cutpoints (≥0.90→1, 0.65–0.90→2, 0.425–0.65→3, <0.425→4;
ZPV>1→1). The real London example's planes all back-solve to ZPV>1 → code 1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
89 lines
3.2 KiB
Python
89 lines
3.2 KiB
Python
"""Slice 3 — generation-calibrated PV overshading (ADR-0026).
|
||
|
||
Google's `yearlyEnergyDcKwh` per segment already encodes real orientation,
|
||
tilt and shading from imagery. Dividing its AC equivalent by SAP's own
|
||
unshaded annual output (0.8 × kWp × S) cancels orientation/tilt and isolates
|
||
the effective overshading factor ZPV, which snaps to the RdSAP bucket
|
||
{1:1.0, 2:0.8, 3:0.5, 4:0.35}.
|
||
"""
|
||
|
||
import json
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
from domain.modelling.generators.solar_recommendation import (
|
||
overshading_code_from_zpv,
|
||
segment_overshading_code,
|
||
)
|
||
from domain.modelling.solar_potential import SolarPotential, SolarRoofSegment
|
||
from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
||
pv_annual_solar_radiation_kwh_per_m2,
|
||
)
|
||
|
||
_FIXTURE: Path = (
|
||
Path(__file__).resolve().parent
|
||
/ "fixtures"
|
||
/ "google_building_insights_001431.json"
|
||
)
|
||
_DC_TO_AC_RATE = 0.955
|
||
_SAP_PV_PERFORMANCE_FACTOR = 0.8
|
||
|
||
|
||
def _insights() -> dict[str, Any]:
|
||
with _FIXTURE.open(encoding="utf-8") as handle:
|
||
data: dict[str, Any] = json.load(handle)
|
||
return data
|
||
|
||
|
||
def test_overshading_cutpoints_snap_to_rdsap_buckets() -> None:
|
||
# Arrange / Act / Assert — ADR-0026 midpoints: ≥0.90→1, 0.65–0.90→2,
|
||
# 0.425–0.65→3, <0.425→4, and ZPV>1 clamps to 1.
|
||
assert overshading_code_from_zpv(1.20) == 1
|
||
assert overshading_code_from_zpv(0.90) == 1
|
||
assert overshading_code_from_zpv(0.89) == 2
|
||
assert overshading_code_from_zpv(0.65) == 2
|
||
assert overshading_code_from_zpv(0.64) == 3
|
||
assert overshading_code_from_zpv(0.425) == 3
|
||
assert overshading_code_from_zpv(0.42) == 4
|
||
assert overshading_code_from_zpv(0.10) == 4
|
||
|
||
|
||
def _segment_with_zpv(target_zpv: float) -> SolarRoofSegment:
|
||
"""A south-facing 30°-tilt 2 kWp segment whose Google generation is set so
|
||
its back-solved overshading factor is ``target_zpv``."""
|
||
orientation, pitch_code, panels, capacity = 5, 2, 5, 400.0 # 5 × 400 W = 2 kWp
|
||
kwp = panels * capacity / 1000
|
||
s = pv_annual_solar_radiation_kwh_per_m2(orientation, pitch_code)
|
||
g_ac = _SAP_PV_PERFORMANCE_FACTOR * kwp * s * target_zpv
|
||
yearly_dc = g_ac / _DC_TO_AC_RATE
|
||
return SolarRoofSegment(
|
||
segment_index=0,
|
||
panels_count=panels,
|
||
azimuth_degrees=180.0, # S → octant 5
|
||
pitch_degrees=30.0, # → code 2
|
||
yearly_energy_dc_kwh=yearly_dc,
|
||
)
|
||
|
||
|
||
def test_segment_overshading_recovers_each_bucket() -> None:
|
||
# Arrange / Act / Assert — a segment dialled to each bucket midpoint
|
||
capacity = 400.0
|
||
assert segment_overshading_code(_segment_with_zpv(1.0), capacity) == 1
|
||
assert segment_overshading_code(_segment_with_zpv(0.8), capacity) == 2
|
||
assert segment_overshading_code(_segment_with_zpv(0.5), capacity) == 3
|
||
assert segment_overshading_code(_segment_with_zpv(0.35), capacity) == 4
|
||
|
||
|
||
def test_real_example_segments_are_unshaded() -> None:
|
||
# Arrange
|
||
potential = SolarPotential.from_building_insights(_insights())
|
||
largest = potential.configurations[-1]
|
||
|
||
# Act
|
||
codes = {
|
||
segment_overshading_code(seg, potential.panel_capacity_watts)
|
||
for seg in largest.segments
|
||
}
|
||
|
||
# Assert — a clear London roof: every plane back-solves to ZPV > 1 → code 1
|
||
assert codes == {1}
|