- 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>
62 lines
1.9 KiB
Python
62 lines
1.9 KiB
Python
"""Unit tests for BESS capacity degradation model."""
|
|
|
|
import pytest
|
|
|
|
from remodel_engine.generation.bess_state import simulate_bess_capacity
|
|
from remodel_engine.schemas.generation import BessConfig
|
|
|
|
BASE_CFG = BessConfig(
|
|
capacity_mwh=500.0,
|
|
power_mw=125.0,
|
|
rte=0.85,
|
|
design_cycles=6000.0,
|
|
eol_soh=0.70,
|
|
)
|
|
|
|
|
|
def test_output_length() -> None:
|
|
cycles = [365.0 * (i + 1) for i in range(25)]
|
|
result = simulate_bess_capacity(BASE_CFG, cycles)
|
|
assert len(result) == 25
|
|
|
|
|
|
def test_index_is_1_to_25() -> None:
|
|
cycles = [365.0 * (i + 1) for i in range(25)]
|
|
result = simulate_bess_capacity(BASE_CFG, cycles)
|
|
assert list(result.index) == list(range(1, 26))
|
|
|
|
|
|
def test_capacity_decreases_with_cycles() -> None:
|
|
cycles = [365.0 * (i + 1) for i in range(25)]
|
|
result = simulate_bess_capacity(BASE_CFG, cycles)
|
|
assert all(result.iloc[i] >= result.iloc[i + 1] for i in range(24))
|
|
|
|
|
|
def test_zero_cycles_gives_full_capacity() -> None:
|
|
"""Zero cumulative cycles → SOH = 1.0 → usable = nameplate."""
|
|
result = simulate_bess_capacity(BASE_CFG, [0.0] * 25)
|
|
assert all(abs(v - BASE_CFG.capacity_mwh) < 1e-9 for v in result)
|
|
|
|
|
|
def test_soh_floor_at_eol() -> None:
|
|
"""Very high cycle count should floor at eol_soh * nameplate."""
|
|
extreme_cycles = [100_000.0] * 25
|
|
result = simulate_bess_capacity(BASE_CFG, extreme_cycles)
|
|
floor = BASE_CFG.capacity_mwh * BASE_CFG.eol_soh
|
|
assert all(abs(v - floor) < 1e-9 for v in result)
|
|
|
|
|
|
def test_augmentation_increases_capacity() -> None:
|
|
cfg = BessConfig(
|
|
capacity_mwh=500.0,
|
|
power_mw=125.0,
|
|
augmentation_schedule=[(10, 200.0)],
|
|
)
|
|
cycles = [0.0] * 25
|
|
result = simulate_bess_capacity(cfg, cycles)
|
|
assert result.iloc[9] > result.iloc[8] # year 10 > year 9
|
|
|
|
|
|
def test_wrong_cycles_length_raises() -> None:
|
|
with pytest.raises(ValueError, match="25 entries"):
|
|
simulate_bess_capacity(BASE_CFG, [365.0] * 10)
|