Optimal operation of energy storage
Project description
Optimal Energy Storage (oes)
Multiple solutions for optimal energy storage control and scheduling.
Table of Contents
Install
To install this package, simply run:
pip install oes
If you want to develop this package, please install this locally by running:
make local-install
Usage
Some simple examples of how this library can be used are included in the jupyter notebook example_usage.ipynb.
A basic implementation requires a BatteryModel
, which specifies the battery parameters, a subclass
of AbstractBattery
, which represents the battery instance and has access to its state-of-charge, a
scenario, which is provided as Pandas DataFrame
and an optimisation controller. All these
are explained in more detail below.
Battery Model
A simple battery model is provided that keeps track of battery-specific parameters.
Here are some example parameters if you call get_default_battery_params()
:
{
'capacity': 13500, # battery capacity, in Wh
'max_charge_rate': 7000, # peak charge rate, in W
'max_discharge_rate': 7000, # peak discharge rate, in W
'max_soc': 94.0, # max soc we can charge to, in %
'min_soc': 20.0, # current soc, in %
'degradation_cost_per_kWh_charge': 0.0, # degradation cost per kWh of charge, in $
'degradation_cost_per_kWh_discharge': 0.0, # degradation cost per kWh of discharge, in $
'efficiency_charging': 100.0, # efficiency of charging, in %
'efficiency_discharging': 100.0, # efficiency of discharging, in %
}
A battery model only maintains parameters. To get an instance of an actual battery (which keeps track of state of
charge, and any other changes in state), we need to pass the model to an object that is a sub-class of AbstractBattery
such as SimulatedBattery
or your own implementation.
from oes import BatteryModel, get_default_battery_params, SimulatedBattery
battery_model = BatteryModel(get_default_battery_params())
battery = SimulatedBattery(battery_model, initial_soc=50.0)
If you want to control your own battery, you have to provide an implementation of AbstractBattery
that provides a
get_current_soc()
method. This method implementation (that you have to write) can access the actual hardware and
retrieve the current state-of-charge (provided as number between 0 and 100) and will be called by the optimisation
controller.
Scenario
A scenario is a set of values for some given horizon (for example the next 24 hours), and includes the following:
- demand (W)
- generation (W)
- import_tariff ($/kWh)
- export_tariff ($/kWh)
The scenario must be provided as a pandas DataFrame
having an index of timestamps.
Any handling of time varying import and export tariffs, or forecasts of generation and demand, must be done outside of this package. Likewise, this package assumes regularly spaced intervals, and assumes that any gaps or interpolation are handled outside of this package.
Here is some example data (provided with this package) showing how a "scenario" should look:
import pickle
scenario = pickle.load(open('oes/data/example_data.pickle', 'rb'))
scenario.head()
generation demand tariff_import tariff_export
timestamp
2017-11-29 00:00:00 0 1370 0.2 0.08
2017-11-29 00:01:00 0 1370 0.2 0.08
2017-11-29 00:02:00 0 1360 0.2 0.08
2017-11-29 00:03:00 0 1420 0.2 0.08
2017-11-29 00:04:00 0 1380 0.2 0.08
Units
The following conventions regarding units are used throughout this package (chosen to match common industry usage):
- All time series data related to energy demand and generation: W
- All import and export tariffs: $/kWh
- All charge / discharge decisions: W
- Battery degradation values: $/kWh
Any time series data representing energy over the course of an interval (Wh) needs to be converted to power (W) before being used within this package.
Controllers
A number of different "controllers" are provided. Each controller is instantiated with its relevant parameters, and can then be used to "solve" a provided scenario.
Controllers use the same temporal resolution as the provided scenario -- in other words, if half-hourly data is provided, the controller will provide half-hourly charge/discharge values.
The "solution" that a controller provides is a pandas DataFrame having the following structure:
- index of
timestamps
(same as the timestamps in the provided scenario) - column
charge_rate
: battery charge (positive) or discharge (negative) value at each interval (in W) - column
soc
: battery state of charge (in percentage) as a result of charge / discharge decisions
By default, a controller will keep track of battery SOC when generating a solution, and will
not return charging values that lead to battery exceeding max/min SOC. This can be
overridden by passing parameter 'constrain_charge_rate': False
. This can be useful, for example,
when calculating a schedule (see below).
Here is an example of how to create a very simple controller that only charges at a static rate:
from oes import ChargeController
charge_controller = ChargeController(battery=battery) # see battery definition above
If we want to set a specific charge rate (7000W), and avoid constraining it by battery max/min soc, we can instead instantiate it like this:
from oes import ChargeController
params = {
'charge_rate': 7000,
'constrain_charge_rate': False
}
charge_controller = ChargeController(params, battery=battery) # see battery definition above
The following controllers have been implemented:
Basic controllers
These are very simple controllers that can be used as baselines or to build more complex controllers or schedules.
Controller | Description |
---|---|
DoNothingController | Do nothing (no charge or discharge). This controller can be helpful as a baseline, e.g. to determine cost when battery is not used at all. |
ChargeController | Charge at a static rate |
DischargeController | Discharge at a static rate |
Rule-based controllers
Rule-based controllers make a decision in each interval based on information available in that interval. In other words, they do not conduct any kind of optimisation over a longer horizon.
All rule-based controllers (and all basic controllers) must implement a function, solve_one_interval(self, scenario_interval: pd.DataFrame) -> float
to ensure they can be used to build schedules of controllers later.
Controller | Description |
---|---|
SolarSelfConsumptionController | Charge rate is generation minus demand. In other words, when there is more generation than demand, charge with the excess generation; when there is more demand, discharge to meet this. |
ImportTariffOptimisationController | Discharge battery to meet demand when the import tariff is higher than average; charge battery at maximum possible rate when the import tariff is lower than average |
SpotPriceArbitrageNaiveController | Assumes that both import and export tariff represent whole sale market price (plus maybe a network charge). Takes the average of max export tariff and min import tariff, and discharges when the current price is below this value, and charges when the current price is above this value. It ignores demand and generation. |
Optimisation-based controllers
These controllers determine the best possible set of charge and discharge values to minimise cost across the full scenario.
Optimisation-based controllers do not need to implement the solve_one_interval
function that rule-based controllers do.
Controller | Description |
---|---|
DynamicProgramController | Full optimisation using dynamic programming |
SpotPriceArbitrageOptimalController | Optimisation taking only import and export tariffs into account. No consideration of demand and generation |
Schedulers
Optimal controllers will find the best possible set of charge / discharge rates in discrete intervals for the full scenario. However, in reality circumstances can change in seconds (loads being switched on and off, clouds passing over solar panels, etc.). Sometimes a discrete solution is not good enough, and what is really needed is real-time fast response using a basic controller.
That's where a scheduler comes in. The point of a scheduler is to take a number of very basic, simple controllers (that can respond to changes instantly), and to find a schedule that specifies which controller should be used when. The goal is to ultimately emulate an optimal solution, without needing to choose specific charge rates for every interval.
For now, just a single approach to scheduling has been provided as part of this package, which uses the output of the optimal dynamic program controller as a way to choose a schedule of simpler controllers. This can be instantiated simply:
from oes import DPScheduler
scheduler = DPScheduler()
The scheduler then needs to be passed the scenario and battery, but also a list of basic controllers that should be considered when determining the schedule. Finally, it also needs the outputs of the (previously calculated) optimal solution:
# Generate list of controllers to use when generating schedule
from oes import DoNothingController, ChargeController, DischargeController, \
SolarSelfConsumption, ImportTariffOptimisation, SpotPriceArbitrageNaive
controllers = [
('DN', DoNothingController),
('C', ChargeController),
('D', DischargeController),
('SSC', SolarSelfConsumption),
('TO', ImportTariffOptimisation),
('SPA', SpotPriceArbitrageNaive)
]
scheduler.solve(scenario, battery, controllers, solution_dp)
The scheduler subsequently:
- Determines the charge rates at every interval for all controllers
- Determines which controllers match optimal (DP) most closely in each interval
- Generates a full schedule (one specific controller for every interval)
- Conducts some clean up (handles intervals where no near-optimal controller was found)
- Converts to a short schedule
This schedule typically performs as well as an optimal solution -- or even sometimes better (since it can handle changes over very short intervals better).
For some examples, see the provided jupyter notebook example_usage.ipynb.
Testing
The following command will run the test suite (tests still to be written):
python -m pytest -s tests
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file oes-1.0.2.tar.gz
.
File metadata
- Download URL: oes-1.0.2.tar.gz
- Upload date:
- Size: 7.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.3.0 pkginfo/1.7.0 requests/2.25.1 setuptools/51.1.0 requests-toolbelt/0.9.1 tqdm/4.58.0 CPython/3.8.12
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | bfcf1e1888fda326b980ac678c34a156deaadf29b87d761f9175542345a33acc |
|
MD5 | 723ef917545c5cfe06d8786263859483 |
|
BLAKE2b-256 | 9ee9f84ed5585b7896e58f1336322c6aba621fe15ede67b7322c6a77e35718bd |
File details
Details for the file oes-1.0.2-py3-none-any.whl
.
File metadata
- Download URL: oes-1.0.2-py3-none-any.whl
- Upload date:
- Size: 6.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.3.0 pkginfo/1.7.0 requests/2.25.1 setuptools/51.1.0 requests-toolbelt/0.9.1 tqdm/4.58.0 CPython/3.8.12
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 04164b4f7d155d916cc70482c8068144b76413a8a6380f2ba7fa3f84c9499b4b |
|
MD5 | ff6197e8bdd81a3918c0af471379e40f |
|
BLAKE2b-256 | c57429d73a734242467b831f86cb4c28dfd901caa2b801c61378682c623fe523 |