Remodel/packages/engine/tests/integration/test_parity.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

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%}"
)