Storage Dynamics¶
Charge Balance¶
The stored energy evolves over time according to charging, discharging, and self-discharge losses:
where:
- \(E_{s,t}\) — stored energy at the start of timestep \(t\)
- \(P^{\text{c}}_{s,t}\) — charging flow rate (energy entering the storage)
- \(P^{\text{d}}_{s,t}\) — discharging flow rate (energy leaving the storage)
- \(\eta^{\text{c}}_s\) — charging efficiency (losses during charging)
- \(\eta^{\text{d}}_s\) — discharging efficiency (losses during discharging)
- \(\delta_s \in [0, 1]\) — self-discharge rate per hour
- \(\Delta t_t\) — timestep duration in hours
The charge state has \(|\mathcal{T}| + 1\) values (one before each timestep plus one after the last timestep).
Charge State Bounds¶
The charge state is bounded by relative SOC limits scaled by the storage capacity:
Initial & Cyclic Conditions¶
Fixed initial state:
where \(E_0\) is Storage.prior_level (absolute MWh). If prior_level is None,
the initial level is unconstrained (the optimizer chooses).
Cyclic condition (when Storage.cyclic = True):
This ensures the storage ends at the same level it started.
Final level bounds (final_level_min / final_level_max) — absolute
bounds [MWh] on the level at the last timestep, per period:
They compose with cyclic (the prior level is then bounded too, since
\(E_{s,t_0} = E_{s,t_{\text{end}}}\)).
Simultaneous Charge & Discharge¶
With \(\eta^{\text{c}} \cdot \eta^{\text{d}} < 1\), charging and discharging
at once destroys energy — occasionally optimal (e.g. under must-run surplus)
but physically impossible for most devices. prevent_simultaneous = True
excludes it with a binary \(b_{s,t}\) per timestep:
where \(\mathrm{M}\) is the static size bound of the respective flow (fixed size, or the sizing/investment maximum). Both flows must therefore be sized.
Consider a soft penalty first
The binary turns the model into a MILP. In most models simultaneous
cycling only pays off under negative prices or must-run surplus — a
small penalty on the charge/discharge flows
(effects_per_flow_hour={'penalty': 1e-3}) usually discourages it
while keeping the problem linear and the real cost accounting clean
(this is PyPSA's recommended approach; cf.
Parzen et al. 2023 on
unintended storage cycling). Reach for prevent_simultaneous when
the exclusion must be exact.
Parameters¶
| Symbol | Description | Reference |
|---|---|---|
| \(E_{s,t}\) | Stored energy variable | storage--level[storage, time] |
| \(P^{\text{c}}_{s,t}\) | Charging flow rate | flow--rate[charge_flow, time] |
| \(P^{\text{d}}_{s,t}\) | Discharging flow rate | flow--rate[discharge_flow, time] |
| \(\bar{\mathrm{E}}_s\) | Storage capacity | Storage.capacity |
| \(\eta^{\text{c}}_s\) | Charging efficiency | Storage.eta_charge |
| \(\eta^{\text{d}}_s\) | Discharging efficiency | Storage.eta_discharge |
| \(\delta_s\) | Self-discharge rate | Storage.relative_loss_per_hour |
| \(\underline{\mathrm{e}}_s\) | Relative min SOC | Storage.relative_level_min |
| \(\bar{\mathrm{e}}_s\) | Relative max SOC | Storage.relative_level_max |
| \(\underline{\mathrm{E}}^{\text{end}}_s\) | Min final level [MWh] | Storage.final_level_min |
| \(\bar{\mathrm{E}}^{\text{end}}_s\) | Max final level [MWh] | Storage.final_level_max |
| \(b_{s,t}\) | Charging indicator binary | Storage.prevent_simultaneous |
| \(\Delta t_t\) | Timestep duration | dt |
See Notation for the full symbol table.
Example¶
A battery with \(\bar{\mathrm{E}} = 10\) MWh, \(\eta^{\text{c}} = 0.95\), \(\eta^{\text{d}} = 0.95\), \(\delta = 0.001\)/h, \(\Delta t = 1\) h:
Starting at \(E_0 = 5\) MWh, charging at \(P^{\text{c}} = 2\) MW: