Maximising Revenue of a Grid Connected Battery using Linear Programming Model (Pyomo) — Part 1: Perfect Foresight

Zhi Hern Tom
5 min readOct 16, 2021

--

Introduction

Assuming we have the following criteria:

  • We have a grid connected battery where maximum capacity is 580MWh
  • Maximum power = 300MW (maximum instantaneous rate of energy release or energy charging)
  • We can only choose charge, discharge or hold per period.
  • Electric prices are set every 30 minutes based on prevailing supply and demand (called the spot price)
  • Conversion rate = 0.90 (electricity to chemical and vice versa, aka charge and discharge efficiency)
  • Marginal Loss Factor = 0.991 (losses associated with energy transmission)
  • Fixed O&M = 8.10 AUD/kW/year (we ignore this)
An example of the spot price chart. We can see that spot prices are set every 30 minutes.

We need to develop an algorithm that determines the optimal charge and discharge behaviour of the grid connected battery based in Victoria, Australia.

Please note that the timeframe is 30 minutes, thus the battery will only charge 1/2 of the power rate. For example, if we charge the battery at maximum power rate (300MW), the battery will get 300 / 2 * 0.9 = 135MWh, where 15MWh were lost due to the conversion rate. Conversely, if we discharge the battery at maximum power rate (300MW again), the battery will deduct 150MWh but only release 300 / 2 * 0.9 = 135MWh (15MWh is lost due to the conversion rate).

To get a better understanding of this problem, you can try to think of the stock market. Assuming our portfolio can hold up to 580 units of stock A. We can buy/sell up 150 units per period and the transaction fee is 10% of the unit (not the value) bought/sold.

Dataset Overview

We have the following dataset:

  • spot price (30-minute interval, AUD/MWh)
  • demand (MW)
  • intermittent generation (MW)

Let’s have a look at the spot price pattern:

Mean and median of spot prices across 48 period

We can see that the spot price forms a very obvious pattern. The peaks occurs at around period 0 (12:00 am), 15 (7:30 am) and period 38 (7:00 pm).

To calculate the profit, we need to calculate the cost and revenue.

def calc_market_revenue(market_dispatch, spot_price, marginal_loss_factor=0.991):    # negative market_dispatch means charging
# positive market_dispatch means discharging
market_revenue = np.where(market_dispatch < 0, market_dispatch * spot_price / marginal_loss_factor, market_dispatch * spot_price * marginal_loss_factor) return np.round(market_revenue)

Baseline Algorithm

As we know the simplest method for arbitrage is that we charge at the current period when future prices are high and vice versa. Thus the baseline algorithm makes decisions on dispatch behaviour based on two condition:

  • When the current price is below 0.25 quantile of 10 future rolling period, we charge the battery at maximum power rate.
  • When the current price is above 0.75 quantile of 10 future rolling period, we discharge the battery at maximum power rate.

The code is shown below:

window = 10df['lower'] = df.spot_price[::-1].shift(1).rolling(window).quantile(lower_pctl, interpolation='linear')df['upper'] = df.spot_price[::-1].shift(1).rolling(window).quantile(upper_pctl, interpolation='linear')df['forecast'] = np.where(df['spot_price'] < df['lower'], -1, np.where(df['spot_price'] > df['upper'], 1, 0))

Then we charge and discharge when forecast is -1 and 1 respectively. This baseline has a lot of limitations. One if them might be that we are not able to choose the power rate when charging/discharging. Another limitation is that if we have a lot of consecutive charging/discharging signal, we are not able to choose to charge/discharge at lower/higher spot price period.

The revenue for this baseline is:

Train period revenue: 95678015.0
CV period revenue: 16328298.0
Test period revenue: 5311042.0
Total revenue: 117317335.0

Linear Programming Model

Linear programming is a mathematical optimisation technique for maximising or minimising an objective. To achieve this, we have used two python packages , Pyomo and GLPK. Pyomo is a python-based modelling language that allows us to formulate linear programming models by defining a set of decision variables, parameters, operational constraints and an objective function. And GLPK, is a package for solving large scale linear programming problems.

To formulate the battery model, parameters, decision variables, and operation constraints are defined based on the battery’s technical specification, so that we could resemble the real operation of a battery system, such as the limits of battery capacity, and the maximum charge discharge power.

# Define model and solver
battery = ConcreteModel()
opt = SolverFactory(solver)
# defining components of the objective model
# battery parameters
battery.Period = Set(initialize=list(df.period), ordered=True)
battery.Price = Param(initialize=list(df.spot_price), within=Any)
# battery varaibles
battery.Capacity = Var(battery.Period, bounds=(MIN_BATTERY_CAPACITY, MAX_BATTERY_CAPACITY))
battery.Charge_power = Var(battery.Period, bounds=(0, MAX_RAW_POWER))
battery.Discharge_power = Var(battery.Period, bounds=(0, MAX_RAW_POWER))
# Set constraints for the battery
# Defining capacity rule for the battery
def capacity_constraint(battery, i):
# Assigning battery capacity at the beginning of optimisation
if i == battery.Period.first():
return battery.Capacity[i] == INITIAL_CAPACITY
return battery.Capacity[i] == (battery.Capacity[i-1] + (battery.Charge_power[i-1] / 2 * EFFICIENCY) (battery.Discharge_power[i-1] / 2))
# Make sure the battery does not charge above the limit
def over_charge(battery, i):
return battery.Charge_power[i] <= (MAX_BATTERY_CAPACITY - battery.Capacity[i]) * 2 / EFFICIENCY
# Make sure the battery discharge the amount it actually has
def over_discharge(battery, i):
return battery.Discharge_power[i] <= battery.Capacity[i] * 2
# Make sure the battery do not discharge when price are not positive
def negative_discharge(battery, i):
if df.spot_price[i] <= 0:
return battery.Discharge_power[i] == 0
return Constraint.Skip

Additionally, we’ve defined the objective function to be maximising the revenue. which is calculated based on the net volume of energy charged or discharged in each half-hour period. This is then multiplied by the spot price of the corresponding period. In addition, we also need to consider the marginal loss factor and the discharge efficiency of the battery which are the losses of energy during the transmission process and chemical to electrical conversion rate during discharge. And since we have complete knowledge of future prices, an optimal dispatch behaviour could be simply determined by the linear programming solver.

# Defining the battery objective (function to be maximise)
def maximise_profit(battery):
rev = sum(df.spot_price[i] * (battery.Discharge_power[i] / 2 * EFFICIENCY) * MLF for i in battery.Period)
cost = sum(df.spot_price[i] * (battery.Charge_power[i] / 2) / MLF for i in battery.Period)
return rev - cost

The revenue for this baseline is:

CV period revenue: 17669191.0
Test period revenue: 5821757.0
Total revenue: 131327433.0

Final Words

It’s clear that the revenue generated by linear programming model (131327433.0) was approximately 12% higher than the baseline (117317335.0) and was the highest revenue achieved in the university competition. This project was done in group and I want to thank every group member for the contribution: Greyson Zhong, Cathy Yu, Lissy Xu, Caitlin Zhang.

Part 2 is to build a time series forecasting model to predict the spot price since building a profitable algorithm based on perfect foresight on prices is not realistic. Stay tune for part 2!

--

--

Zhi Hern Tom
Zhi Hern Tom

Written by Zhi Hern Tom

Tom is an undergraduate student studying BSc Data Science at The University of Melbourne.