Rolling max energy discharge limit on battery#

This example demonstrates how the battery rolling_max_discharge_limit works. The constraints impose a maximum total energy discharge within predefined time periods. The functionality can be used to e.g. enforce a maximum number of battery cycles within 24 hours.

The model setup for this example is available here:

#Necessary imports 
import pandas as pd
from pyshop import ShopSession
import plotly.graph_objs as go
from plotly.subplots import make_subplots
import numpy as np
from numpy.lib.stride_tricks import sliding_window_view

 #Functions used in this example for building a SHOP modeland running it
from battery_model import build_model, run_model

Create SHOP session and import basic model#

This example model has one battery that is connected to a busbar.

We will add rolling max discharge constraints that limit the battery energy discharge to maximum 2 MWh within each 2 hour time window.

#Create a standard ShopSession
shop=ShopSession()
#Build SHOP model
build_model(shop)

#Define variables
starttime = shop.get_time_resolution()['starttime']
max_discharge = 2
time_window = 120

Run model without rolling discharge constraints#

We start by running the model without any rolling max discharge constraints on the battery, and plot the results.

The upper plot shows the battery discharge for every time step within the optimization period. The lower plot shows the total energy discharge within each consecutive 2 hours, which is what the max rolling discharge limit will constrain. As the plot shows, without the rolling max discharge constraints, the total energy discharge is larger than the limit for all of the time intervals.

# Run model
run_model(shop)

# Get the battery discharge
battery = shop.model.battery.Battery
discharge = battery.power_discharge.get() #MW
discharge_within_time_windows = sliding_window_view(discharge.values, int(120/15)).sum(axis=1)*15/60 #MWh

#Plot battery discharge
fig = make_subplots()
fig.update_layout(title="Battery discharge")
fig.update_yaxes(title_text="Discharge [MW]")
fig.add_trace(go.Bar(x=discharge.index, y=discharge.values))
fig.show()

#Plot total battery discharge within each time window
fig = make_subplots()
fig.update_layout(title="Total battery discharge within each time window")
fig.update_yaxes(title_text="Total discharge [MWh]")
fig.add_trace(go.Bar(x=np.arange(0,len(discharge_within_time_windows), 1), y=discharge_within_time_windows, name = "discharge"))
fig.add_trace(go.Scatter(x=np.arange(0,len(discharge_within_time_windows), 1), y=np.zeros(len(discharge.values))+max_discharge, name = "max discharge limit"))
fig.show()

Add rolling max discharge constraints and rerun model#

We will now add the max rolling discharge constraint to our battery. We also add historical battery discharge data through the historical_discharge attribute, which affects the constraints for the first few time steps. We then rerun the model.

The upper plot shows that the discharge is lower than before applying the constraints. For the first few time steps, there is no battery discharge, due to the historical discharge data. The lower plot shows that now, the total discharge is within the maximum limit for all 2 hour time windows.

#Set max rolling discharge limits and historical discharge on battery
battery.rolling_max_discharge_limit.set([pd.Series([max_discharge], index=[time_window], name=starttime)])
battery.historical_discharge.set(pd.Series([2, 0.0], index=[starttime - pd.Timedelta(minutes=90), starttime - pd.Timedelta(minutes=30)]))

# Run model
run_model(shop)

# Get the battery discharge
battery = shop.model.battery.Battery
discharge = battery.power_discharge.get() #MW
historical_discharge = battery.historical_discharge.get().resample("15min").ffill() #MW
total_discharge = historical_discharge.add(discharge, fill_value=0).astype(historical_discharge.dtype) #MW
total_discharge_within_time_windows = sliding_window_view(total_discharge.values, int(120/15)).sum(axis=1)*15/60 #MWh

#Plot battery discharge
fig = make_subplots()
fig.update_layout(barmode="stack", title="Battery discharge")
fig.update_yaxes(title_text="Discharge [MW]")
fig.add_trace(go.Bar(x=total_discharge.index, y=total_discharge.values*15/60, name="discharge"))
fig.add_trace(go.Scatter(x=[starttime,starttime], y=[0,0.5],mode="lines", line = dict(color = 'grey', dash = 'dot'), name = "Optimization start"))
fig.show()

#Plot total battery discharge within each time window
fig = make_subplots()
fig.update_layout(barmode="stack", title="Total battery discharge within each time window")
fig.update_yaxes(title_text="Total discharge [MWh]")
fig.add_trace(go.Bar(x=np.arange(0,len(total_discharge_within_time_windows), 1), y=total_discharge_within_time_windows, name = "discharge"))
fig.add_trace(go.Scatter(x=np.arange(0,len(total_discharge_within_time_windows), 1), y=np.zeros(len(total_discharge.values))+max_discharge, name = "max discharge limit"))
fig.show()