- 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>
57 lines
2.2 KiB
Python
57 lines
2.2 KiB
Python
"""S1-T10: Excel parity gate for the Nagasamudra reference scenario.
|
|
|
|
HOW TO UNLOCK THIS TEST
|
|
───────────────────────
|
|
1. Open tests/fixtures/nagasamudra_inputs.json.
|
|
2. Replace the solar/wind config values with the actual project inputs.
|
|
3. Fill in _expected.solar_y1_mwh and _expected.wind_y1_mwh from the Excel model.
|
|
4. Remove the `pytest.skip` call below.
|
|
|
|
The test will then verify that year-1 generation matches Excel within 0.1 %
|
|
(the sprint parity gate from PROJECT.md).
|
|
"""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
FIXTURE = Path(__file__).parent.parent / "fixtures" / "nagasamudra_inputs.json"
|
|
|
|
|
|
@pytest.mark.skip(reason="Parity gate: awaiting real Nagasamudra data from user (S1-T10)")
|
|
def test_nagasamudra_solar_y1_parity() -> None:
|
|
raw = json.loads(FIXTURE.read_text())
|
|
expected_mwh = raw["_expected"]["solar_y1_mwh"]
|
|
assert expected_mwh is not None, "Fill _expected.solar_y1_mwh in fixture"
|
|
|
|
from remodel_engine.generation.solar import simulate_solar
|
|
from remodel_engine.schemas.generation import SolarConfig
|
|
|
|
cfg = SolarConfig(**raw["solar"])
|
|
df = simulate_solar(cfg)
|
|
actual_mwh = float(df[df["year"] == 1]["ac_power_mw"].sum())
|
|
rel_error = abs(actual_mwh - expected_mwh) / expected_mwh
|
|
assert rel_error <= 0.001, (
|
|
f"Solar Y1 parity FAIL: engine={actual_mwh:.1f} MWh, "
|
|
f"Excel={expected_mwh:.1f} MWh, error={rel_error:.4%}"
|
|
)
|
|
|
|
|
|
@pytest.mark.skip(reason="Parity gate: awaiting real Nagasamudra data from user (S1-T10)")
|
|
def test_nagasamudra_wind_y1_parity() -> None:
|
|
raw = json.loads(FIXTURE.read_text())
|
|
expected_mwh = raw["_expected"]["wind_y1_mwh"]
|
|
assert expected_mwh is not None, "Fill _expected.wind_y1_mwh in fixture"
|
|
|
|
from remodel_engine.generation.wind import simulate_wind
|
|
from remodel_engine.schemas.generation import WindConfig
|
|
|
|
cfg = WindConfig(**raw["wind"])
|
|
df = simulate_wind(cfg)
|
|
actual_mwh = float(df[df["year"] == 1]["ac_power_mw"].sum())
|
|
rel_error = abs(actual_mwh - expected_mwh) / expected_mwh
|
|
assert rel_error <= 0.001, (
|
|
f"Wind Y1 parity FAIL: engine={actual_mwh:.1f} MWh, "
|
|
f"Excel={expected_mwh:.1f} MWh, error={rel_error:.4%}"
|
|
)
|