solar-intelligence / tests /test_advanced_simulation.py
ghostieee11's picture
Upload folder using huggingface_hub
15b595e verified
Raw
History Blame Contribute Delete
9.1 kB
"""Tests for Phase 3: Advanced simulation features.
Covers:
- Single-axis and dual-axis tracking simulation
- Horizon shading model
- Inter-row shading for solar farms
- Bifacial panel gain estimation
- Rooftop suitability scoring (RooftopScorer)
"""
from __future__ import annotations
import numpy as np
import pytest
from solar_intelligence.data_loader import generate_synthetic_solar_data
from solar_intelligence.orientation_simulator import OrientationSimulator, RooftopScorer
@pytest.fixture
def dataset():
return generate_synthetic_solar_data(lat=28.6, lon=77.2, start_year=2023, end_year=2023)
@pytest.fixture
def ghi(dataset):
return dataset["ALLSKY_SFC_SW_DWN"].values
@pytest.fixture
def sim():
return OrientationSimulator(
latitude=28.6, longitude=77.2,
tilt_angles=[0, 30],
azimuths={"North": 0, "South": 180, "East": 90, "West": 270},
)
# ---------------------------------------------------------------------------
# Tracking Simulation Tests
# ---------------------------------------------------------------------------
class TestTrackingSimulation:
def test_single_axis_returns_dict(self, sim, ghi):
result = sim.simulate_tracking(ghi, mode="single_axis")
assert isinstance(result, dict)
assert "tracking_mode" in result
assert result["tracking_mode"] == "single_axis"
def test_single_axis_has_energy(self, sim, ghi):
result = sim.simulate_tracking(ghi, mode="single_axis")
assert result["annual_energy_kwh"] > 0
def test_single_axis_beats_fixed(self, sim, ghi):
result = sim.simulate_tracking(ghi, mode="single_axis")
assert result["gain_vs_fixed_pct"] > 0
def test_dual_axis_returns_dict(self, sim, ghi):
result = sim.simulate_tracking(ghi, mode="dual_axis")
assert result["tracking_mode"] == "dual_axis"
assert result["annual_energy_kwh"] > 0
def test_dual_axis_beats_single(self, sim, ghi):
single = sim.simulate_tracking(ghi, mode="single_axis")
dual = sim.simulate_tracking(ghi, mode="dual_axis")
# Dual axis should capture more or equal energy
assert dual["annual_energy_kwh"] >= single["annual_energy_kwh"] * 0.9
# ---------------------------------------------------------------------------
# Horizon Shading Tests
# ---------------------------------------------------------------------------
class TestHorizonShading:
def test_shading_returns_dict(self, sim, ghi):
result = sim.horizon_shading(ghi)
assert isinstance(result, dict)
assert "shading_loss_pct" in result
def test_shading_loss_positive(self, sim, ghi):
# Default profile has obstructions
result = sim.horizon_shading(ghi)
assert result["shading_loss_pct"] >= 0
def test_no_shading_with_flat_horizon(self, sim, ghi):
# Flat horizon = no obstructions
flat = {az: 0 for az in range(0, 360, 45)}
result = sim.horizon_shading(ghi, horizon_profile=flat)
assert result["shading_loss_pct"] == 0
def test_heavy_shading_high_loss(self, sim, ghi):
# High obstructions all around
heavy = {az: 40 for az in range(0, 360, 45)}
result = sim.horizon_shading(ghi, horizon_profile=heavy)
assert result["shading_loss_pct"] > 5
def test_shaded_energy_less_than_unshaded(self, sim, ghi):
result = sim.horizon_shading(ghi)
assert result["shaded_energy_kwh"] <= result["unshaded_energy_kwh"]
def test_daylight_hours_positive(self, sim, ghi):
result = sim.horizon_shading(ghi)
assert result["total_daylight_hours"] > 2000
# ---------------------------------------------------------------------------
# Inter-Row Shading Tests
# ---------------------------------------------------------------------------
class TestInterRowShading:
def test_inter_row_returns_dict(self, sim):
result = sim.inter_row_shading(tilt=30, row_spacing_ratio=2.0)
assert isinstance(result, dict)
def test_adequate_spacing_no_loss(self, sim):
result = sim.inter_row_shading(tilt=30, row_spacing_ratio=10.0)
assert result["shading_loss_pct"] == 0
assert result["current_spacing_adequate"] is True
def test_tight_spacing_has_loss(self, sim):
result = sim.inter_row_shading(tilt=30, row_spacing_ratio=0.5)
assert result["shading_loss_pct"] > 0
assert result["current_spacing_adequate"] is False
def test_min_spacing_positive(self, sim):
result = sim.inter_row_shading(tilt=30)
assert result["min_spacing_ratio"] > 0
def test_steeper_tilt_longer_shadow(self, sim):
shallow = sim.inter_row_shading(tilt=15)
steep = sim.inter_row_shading(tilt=60)
assert steep["shadow_length_ratio"] > shallow["shadow_length_ratio"]
# ---------------------------------------------------------------------------
# Bifacial Gain Tests
# ---------------------------------------------------------------------------
class TestBifacialGain:
def test_bifacial_returns_dict(self, sim, ghi):
result = sim.bifacial_gain(ghi, tilt=30)
assert isinstance(result, dict)
assert "bifacial_gain_pct" in result
def test_bifacial_gain_positive(self, sim, ghi):
result = sim.bifacial_gain(ghi, tilt=30)
assert result["bifacial_gain_pct"] > 0
def test_total_exceeds_front(self, sim, ghi):
result = sim.bifacial_gain(ghi, tilt=30)
assert result["total_energy_kwh"] > result["front_energy_kwh"]
def test_higher_tilt_more_rear(self, sim, ghi):
low_tilt = sim.bifacial_gain(ghi, tilt=10)
high_tilt = sim.bifacial_gain(ghi, tilt=45)
assert high_tilt["rear_irradiance_pct"] > low_tilt["rear_irradiance_pct"]
def test_higher_bifaciality_more_gain(self, sim, ghi):
low_bf = sim.bifacial_gain(ghi, tilt=30, bifaciality=0.50)
high_bf = sim.bifacial_gain(ghi, tilt=30, bifaciality=0.85)
assert high_bf["bifacial_gain_pct"] > low_bf["bifacial_gain_pct"]
def test_horizontal_zero_gain(self, sim, ghi):
result = sim.bifacial_gain(ghi, tilt=0)
# At 0 tilt, view factor is 0 -> no rear irradiance
assert result["bifacial_gain_pct"] == 0
# ---------------------------------------------------------------------------
# Rooftop Suitability Scorer Tests
# ---------------------------------------------------------------------------
class TestRooftopScorer:
def test_scorer_returns_dict(self):
scorer = RooftopScorer()
result = scorer.score(avg_daily_ghi=5.5, optimal_tilt=28, roof_tilt=30)
assert isinstance(result, dict)
assert "total_score" in result
assert "rating" in result
def test_score_range(self):
scorer = RooftopScorer()
result = scorer.score(avg_daily_ghi=5.5, optimal_tilt=28, roof_tilt=30)
assert 0 <= result["total_score"] <= 100
def test_excellent_location(self):
scorer = RooftopScorer()
result = scorer.score(
avg_daily_ghi=7.0, optimal_tilt=25, roof_tilt=25,
variability_index=0.08, avg_temperature=22,
)
assert result["rating"] == "Excellent"
assert result["total_score"] >= 80
def test_poor_location(self):
scorer = RooftopScorer()
result = scorer.score(
avg_daily_ghi=1.5, optimal_tilt=55, roof_tilt=10,
variability_index=0.35, avg_temperature=-5,
)
assert result["total_score"] < 40
def test_components_present(self):
scorer = RooftopScorer()
result = scorer.score(avg_daily_ghi=5.0, optimal_tilt=30, roof_tilt=30)
assert "solar_resource" in result["components"]
assert "tilt_match" in result["components"]
assert "climate_stability" in result["components"]
assert "temperature" in result["components"]
def test_weights_sum_to_one(self):
scorer = RooftopScorer()
result = scorer.score(avg_daily_ghi=5.0, optimal_tilt=30, roof_tilt=30)
weights = result["weights"]
total_weight = sum(weights.values())
assert abs(total_weight - 1.0) < 0.01
def test_tilt_mismatch_lowers_score(self):
scorer = RooftopScorer()
matched = scorer.score(avg_daily_ghi=5.5, optimal_tilt=30, roof_tilt=30)
mismatched = scorer.score(avg_daily_ghi=5.5, optimal_tilt=30, roof_tilt=80)
assert matched["total_score"] > mismatched["total_score"]
def test_recommendations_for_poor(self):
scorer = RooftopScorer()
result = scorer.score(
avg_daily_ghi=1.0, optimal_tilt=30, roof_tilt=80,
variability_index=0.45, avg_temperature=45,
)
assert len(result["recommendations"]) > 0
def test_no_recommendations_for_excellent(self):
scorer = RooftopScorer()
result = scorer.score(
avg_daily_ghi=7.0, optimal_tilt=25, roof_tilt=25,
variability_index=0.08, avg_temperature=22,
)
assert len(result["recommendations"]) == 0