Remodel/packages/engine/docs/generation.md
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

4.3 KiB
Raw Permalink Blame History

Generation Module

remodel_engine.generation — 25-year hourly simulation for solar, wind, and BESS capacity.

Functions

simulate_solar(config: SolarConfig) → pd.DataFrame

Runs the full solar model chain for 25 years at hourly resolution.

Input: SolarConfig (see schemas/generation.py)

Output: DataFrame with 219 000 rows (25 × 8 760), columns:

Column Type Description
year int16 125
hour int16 08759 (within each year)
dc_power_mw float64 DC output after wiring + mismatch losses
ac_power_mw_pre_clip float64 After inverter + AC losses, before clipping
ac_power_mw float64 Final AC output (clipped, availability, soiling, degradation)
degradation_factor float64 Year-level multiplier (Y1 = 1deg_y1, monotonically ↓)

Model chain:

irradiance_norm × capacity_dc_mwp × (1  dc_loss)          → dc_power_mw
dc_power_mw × inverter_η × (1  ac_loss)                   → ac_power_mw_pre_clip
min(ac_power_mw_pre_clip, capacity_ac_mw)                   → clipped
clipped × availability × (1  soiling_monthly)              → ac_soiled_base
ac_soiled_base × degradation_factor                         → ac_power_mw

Degradation:

  • Year 1: 1 degradation_y1 (LID + initial)
  • Year n: (1 degradation_y1) × (1 degradation_annual)^(n1)

simulate_wind(config: WindConfig) → pd.DataFrame

Input: WindConfig

Output: DataFrame with 219 000 rows, columns:

Column Type Description
year int16 125
hour int16 08759
wind_speed_hub float64 Hub-height wind speed (m/s)
power_fraction float64 Power curve output (01)
ac_power_mw float64 Final AC output
degradation_factor float64 1.0 in Y1, (1deg_annual)^(n1) for Y_n

Model chain:

wind_speed_ref × (hub_h / ref_h)^shear_α               → wind_speed_hub
power_curve(wind_speed_hub)                             → power_fraction
power_fraction × capacity_mw                            → gross_mw
gross_mw × (1wake) × (1electrical) × availability    → net_base
net_base × degradation_factor                           → ac_power_mw

Power curve: piecewise-linear, cut-in 3 m/s, rated at 12.5 m/s, cut-out 25 m/s. Defined in catalog/defaults.py::WIND_POWER_CURVE_POINTS.


simulate_bess_capacity(config: BessConfig, cum_cycles_per_year: list[float]) → pd.Series

Models physical battery capacity over 25 years. Does not simulate dispatch.

Input:

  • config: BessConfig with nameplate MWh, design_cycles, eol_soh, augmentation_schedule
  • cum_cycles_per_year: cumulative full-equivalent cycles at end of each year (from dispatch module, or a conservative default)

Output: pd.Series indexed 125, values = usable MWh

Degradation:

SOH = max(eol_soh,  1  cum_cycles/design_cycles × (1  eol_soh))
usable_mwh = (nameplate + augmentation_so_far) × SOH

Catalog profiles

Bundled 8760-hour CSV files in catalog/profiles/:

Location Solar CSV Wind CSV Notes
RJ solar/RJ_8760.csv wind/RJ_8760.csv Rajasthan ~27°N, desert
KA solar/KA_8760.csv wind/KA_8760.csv Karnataka ~13°N, coastal
GJ solar/GJ_8760.csv wind/GJ_8760.csv Gujarat ~23°N, Kutch

Solar CSVs: columns hour, irradiance_norm (01). Wind CSVs: columns hour, wind_speed_ms (m/s at 100 m reference height).

Load via catalog.loader.load_solar_profile(location_id) / load_wind_profile(location_id). Results are LRU-cached per process.

Adding a new location profile

  1. Create catalog/profiles/solar/<ID>_8760.csv and catalog/profiles/wind/<ID>_8760.csv.
  2. Add the ID to catalog/loader._VALID_LOCATIONS.
  3. Add a test in tests/unit/test_catalog.py.

CLI

remodel simulate-gen --input scenario.json --output gen.parquet

scenario.json must have a "solar" key (SolarConfig fields) and/or a "wind" key (WindConfig fields).

Parity gate (S1-T10)

See tests/integration/test_parity.py. Currently skipped pending user-supplied fixture values. To unlock: fill tests/fixtures/nagasamudra_inputs.json._expected and remove the pytest.skip.