Remodel/packages/engine/tests/unit/test_wind.py
Mannu 314127effc [S1-T01 through T11] Solar, Wind, BESS generation simulation + CLI
- Pydantic schemas: SolarConfig, WindConfig, BessConfig, GenerationResult
- Catalog: synthetic 8760h profiles for RJ/KA/GJ solar and wind; LRU-cached loader
- generation/solar.py: 25yr hourly simulation (DC losses, inverter, clipping, soiling, degradation)
- generation/wind.py: power-law shear, piecewise power curve, wake/electrical losses, degradation
- generation/bess_state.py: SOH-based capacity degradation with augmentation schedule
- CLI: remodel --input scenario.json --output gen.parquet (Typer upgraded to 0.25.1 for Click 8.3 compat)
- 43 unit tests, 97.4% coverage; mypy strict + ruff clean
- S1-T10 parity gate: placeholder fixture + skipped integration tests (awaiting Nagasamudra data)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 10:04:21 +05:30

96 lines
3 KiB
Python

"""S1-T07: Wind simulation unit tests."""
import numpy as np
from remodel_engine.generation.wind import annual_plf, simulate_wind
from remodel_engine.schemas.generation import WindConfig
SMALL_WIND = WindConfig(
location_id="RJ",
capacity_mw=100.0,
)
def test_output_shape() -> None:
df = simulate_wind(SMALL_WIND)
assert df.shape == (25 * 8760, 6)
assert set(df.columns) == {
"year", "hour", "wind_speed_hub", "power_fraction", "ac_power_mw", "degradation_factor"
}
def test_plf_in_reasonable_range() -> None:
"""Rajasthan wind PLF should be 25 %-45 %."""
df = simulate_wind(SMALL_WIND)
plf = annual_plf(df, SMALL_WIND.capacity_mw)
assert all(0.25 <= v <= 0.45 for v in plf), f"PLF out of range: {plf.values}"
def test_no_negative_power() -> None:
df = simulate_wind(SMALL_WIND)
assert (df["ac_power_mw"] >= 0).all()
def test_power_never_exceeds_capacity() -> None:
df = simulate_wind(SMALL_WIND)
assert (df["ac_power_mw"] <= SMALL_WIND.capacity_mw + 1e-9).all()
def test_power_fraction_in_01() -> None:
df = simulate_wind(SMALL_WIND)
assert (df["power_fraction"] >= 0).all()
assert (df["power_fraction"] <= 1.0 + 1e-9).all()
def test_degradation_monotonically_decreasing() -> None:
df = simulate_wind(SMALL_WIND)
yearly_deg = df.groupby("year")["degradation_factor"].first().values
diffs = np.diff(yearly_deg)
assert (diffs < 0).all(), "Wind degradation factors must decrease each year"
def test_year1_degradation_is_one() -> None:
"""Wind degradation starts at 1.0 in year 1 (no LID, unlike solar)."""
df = simulate_wind(SMALL_WIND)
y1_deg = df[df["year"] == 1]["degradation_factor"].iloc[0]
assert abs(y1_deg - 1.0) < 1e-9
def test_zero_wind_speed_gives_zero_power() -> None:
"""Hours with zero wind (below cut-in) must produce zero power."""
df = simulate_wind(SMALL_WIND)
zero_wind = df[df["wind_speed_hub"] < 3.0]
if len(zero_wind) > 0:
assert (zero_wind["power_fraction"] < 1e-9).all()
def test_gale_wind_cutout() -> None:
"""Wind speeds above cut-out (25 m/s) should produce zero power."""
df = simulate_wind(SMALL_WIND)
gale = df[df["wind_speed_hub"] > 25.0]
if len(gale) > 0:
assert (gale["ac_power_mw"] < 1e-9).all()
def test_shear_increases_wind_speed() -> None:
"""Hub height > ref height → hub wind speed > ref wind speed."""
cfg = WindConfig(
location_id="RJ",
capacity_mw=100.0,
hub_height_m=140.0,
ref_height_m=100.0,
wind_shear_exponent=0.14,
)
df = simulate_wind(cfg)
# hub speeds should be larger than reference speeds on average
from remodel_engine.catalog.loader import load_wind_profile
ref_speeds = load_wind_profile("RJ")
hub_speeds = df[df["year"] == 1]["wind_speed_hub"].values
assert hub_speeds.mean() > ref_speeds.mean()
def test_ka_plf_reasonable() -> None:
cfg = WindConfig(location_id="KA", capacity_mw=100.0)
df = simulate_wind(cfg)
plf = annual_plf(df, cfg.capacity_mw)
assert all(0.30 <= v <= 0.55 for v in plf)