Skip to content

Storage

A Storage models energy storage with charge/discharge flows, capacity, efficiency, and self-discharge.

See Storage (Math) for the formulation.

Basic Construction

A storage needs two flows (charging and discharging) on the same carrier:

from fluxopt import Flow, Storage

charge = Flow('elec', size=50)     # max charge rate 50 MW
discharge = Flow('elec', size=50)  # max discharge rate 50 MW

battery = Storage('battery', charging=charge, discharging=discharge, capacity=100.0)

Flow ids are auto-qualified: battery(charge) and battery(discharge).

Parameters

Capacity

capacity sets the maximum stored energy \(\bar{E}_s\) in MWh:

battery = Storage('battery', charging=charge, discharging=discharge, capacity=100.0)

Efficiency

eta_charge and eta_discharge set round-trip efficiency. Losses are applied during charging and discharging respectively:

battery = Storage(
    'battery', charging=charge, discharging=discharge,
    capacity=100.0,
    eta_charge=0.95,
    eta_discharge=0.95,
)

With these values, a full charge/discharge cycle retains 90.25% of the energy.

Self-Discharge

relative_loss_per_hour sets the fraction of stored energy lost per hour:

battery = Storage(
    'battery', charging=charge, discharging=discharge,
    capacity=100.0,
    relative_loss_per_hour=0.001,  # 0.1%/h
)

Prior Level and Cyclic Constraint

prior_level sets the energy level at the start of the horizon as an absolute value in MWh. cyclic enforces that the storage ends at the same level it started:

# Fixed initial level (absolute MWh), no cyclic constraint
battery = Storage(..., prior_level=50.0, cyclic=False)

# Unconstrained initial level (optimizer chooses), cyclic (default)
battery = Storage(..., prior_level=None, cyclic=True)

The default is prior_level=None (unconstrained) and cyclic=True.

Level Bounds

relative_minimum_level and relative_maximum_level limit the SOC as fractions of capacity:

battery = Storage(
    'battery', charging=charge, discharging=discharge,
    capacity=100.0,
    relative_minimum_level=0.2,  # never below 20%
    relative_maximum_level=0.9,  # never above 90%
)

Full Example

Battery arbitrage — charge in cheap hours, discharge in expensive hours:

from datetime import datetime
from fluxopt import Carrier, Effect, Flow, Port, Storage, optimize

timesteps = [datetime(2024, 1, 1, h) for h in range(4)]
prices = [0.02, 0.08, 0.02, 0.08]

source = Flow('elec', size=200, effects_per_flow_hour={'cost': prices})
demand = Flow('elec', size=100, fixed_relative_profile=[0.5, 0.5, 0.5, 0.5])

charge = Flow('elec', size=50)
discharge = Flow('elec', size=50)
battery = Storage('battery', charging=charge, discharging=discharge, capacity=100.0)

elec = Carrier('elec')

result = optimize(
    timesteps=timesteps,
    carriers=[elec],
    effects=[Effect('cost', is_objective=True)],
    ports=[Port('grid', imports=[source]), Port('demand', exports=[demand])],
    storages=[battery],
)

print(result.flow_rate('battery(charge)'))
print(result.flow_rate('battery(discharge)'))
print(result.storage_level('battery'))

Parameters Summary

Parameter Type Default Description
id str required Storage identifier
charging Flow required Charging flow
discharging Flow required Discharging flow
capacity float \| Sizing \| None None Maximum stored energy [MWh] or investment
eta_charge TimeSeries 1.0 Charging efficiency
eta_discharge TimeSeries 1.0 Discharging efficiency
relative_loss_per_hour TimeSeries 0.0 Self-discharge rate [1/h]
prior_level float \| None None Initial energy level [MWh], None = unconstrained
cyclic bool True End level must equal start level
relative_minimum_level TimeSeries 0.0 Min SOC as fraction of capacity
relative_maximum_level TimeSeries 1.0 Max SOC as fraction of capacity