- 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>
118 lines
4.3 KiB
Markdown
118 lines
4.3 KiB
Markdown
# 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 | 1–25 |
|
||
| `hour` | int16 | 0–8759 (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 = 1−deg_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)^(n−1)`
|
||
|
||
---
|
||
|
||
### `simulate_wind(config: WindConfig) → pd.DataFrame`
|
||
|
||
**Input**: `WindConfig`
|
||
|
||
**Output**: DataFrame with 219 000 rows, columns:
|
||
|
||
| Column | Type | Description |
|
||
|--------|------|-------------|
|
||
| `year` | int16 | 1–25 |
|
||
| `hour` | int16 | 0–8759 |
|
||
| `wind_speed_hub` | float64 | Hub-height wind speed (m/s) |
|
||
| `power_fraction` | float64 | Power curve output (0–1) |
|
||
| `ac_power_mw` | float64 | Final AC output |
|
||
| `degradation_factor` | float64 | 1.0 in Y1, `(1−deg_annual)^(n−1)` 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 × (1−wake) × (1−electrical) × 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 1–25, 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` (0–1).
|
||
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`.
|