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

118 lines
4.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
```bash
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`.