Search code examples
optimizationbatterygekkodiscrete

How to use Gekko for trajectory optimization in discrete time


I am trying to use Gekko to optimize (dis)charging of a battery energy storage system. Electricity prices per hour EP, energy production from solar panels PV, and energy demand Dem are considered over the entire horizon (0-24h) to minize total costs TC. Arbitrage should take place as the battery is (dis)charged (Pbat_ch & Pbat_dis) to/from the grid (Pgrid_in & Pgrid_out) at the optimal moments.

As opposed to most of the examples online, the problem is not formulated as a state-space model, but mostly relies on exogenous data for price, consumption and production. 3 specific issues with reference to Gurobi are outlined below, the entire code which results in the following error, can be found at the bottom of this post.

Exception:  @error: Inequality Definition
 invalid inequalities: z > x < y
 at0x0000016c6b214040>
 STOPPING . . .
  1. The objective function is the sum of the costs resulting from buying/selling electricity to the grid over the entire horizon. I am used to Gurobi, which allows references to Manipulated Variables (PowerGridOut and PowerGridIn = m.MV(...)) at specific timesteps in this manner ([t]).
m.Obj(sum(ElectricityPrice[t]*PowerGridOut[t] - ElectricityPrice[t]*PowerGridIn[t]) for t in range(25))

Is this also possible in Gekko, or should this summation be recast as an integral? Is the following code correct?

ElectricityPrice = m.Param([..])
.
.
.
TotalCosts = m.integral(ElectricityPrice*(PowerGridOut - PowerGridIn))
m.Obj(TotalCosts)
m.options.IMODE = 6
m.solve()
  1. Gurobi allows this formulation of the constraint to the change in state of charge of the battery:
m.addConstrs(SoC[t+1] == (SoC[t] - ((1/(DischargeEfficiency*BatteryCapacity)) * (PowerBattery
Discharge[t+1]) * Delta_t - ChargeEfficiency/BatteryCapacity * (PowerBatteryCharge[t+1]) * Delta_t)) for t in range(24))

Based on a question on stackoverflow regarding a similar problem, I have reformulated this in a continuous manner as:

m.Equation(SoC.dt() == SoC - 1/(DischargeEfficiency*BatteryCapacity) * Pbattdis - (ChargeEfficiency/BatteryCapacity) * Pbattch)
  1. The final key constraint should the power balance, where Demand[t] & PV[t] are exogenous vectors, while the other variables are m.MV():
m.Equation(((Demand[t] + Pbat_ch + Pgrid_in) == (PV[t] + Pgrid_out + Pbat_dis)) for t in range(25))

Unfortunately, all of this has not worked so far. I would highly appreciate it if someone could give me some hints. Ideally I would like to formulate both the objective function and constraints in discrete terms.

entire code

m       = GEKKO()
# horizon
m.time  = list(range(0,25))
# data vectors
EP      = m.Param(list(Eprice))
Dem     = m.Param(list(demand))
PV      = m.Param(list(production))
# constants
bat_cap = 13.5
ch_eff  = 0.94
dis_eff = 0.94
# manipulated variables
Pbat_ch = m.MV(lb=0, ub=4)
Pbat_ch.DCOST   = 0
Pbat_ch.STATUS  = 1
Pbat_dis = m.MV(lb=0, ub=4)
Pbat_dis.DCOST  = 0
Pbat_dis.STATUS = 1
Pgrid_in = m.MV(lb=0, ub=3)    
Pgrid_in.DCOST  = 0
Pgrid_in.STATUS = 1
Pgrid_out = m.MV(lb=0, ub=3) 
Pgrid_out.DCOST  = 0
Pgrid_out.STATUS = 1
#State of Charge Battery
SoC = m.Var(value=0.5, lb=0.2, ub=1)
#Battery Balance
m.Equation(SoC.dt() == SoC - 1/(dis_eff*bat_cap) * Pbat_dis - (ch_eff/bat_cap) * Pbat_ch)
#Energy Balance
m.Equation(((Dem[t] + Pbat_ch + Pgrid_in) == (PV[t] + Pbat_dis + Pgrid_out)) for t in range(0,25))
#Objective
TC = m.Var()
m.Equation(TC == sum(EP[t]*(Pgrid_out-Pgrid_in) for t in range(0,25)))
m.Obj(TC)
m.options.IMODE=6
m.options.NODES=3
m.options.SOLVER=3 
m.solve()

Solution

  • Nice application! You can either write out all of your discrete equations yourself with m.options.IMODE=3 or else let Gekko manage the time dimension for you. When you include an objective or constraint, it applies them to all of the time points that you specify. With m.options.IMODE=6, there is no need to add the set indices in Gekko such as [t]. Here is a simplified model:

    from gekko import GEKKO
    import numpy as np
    
    m       = GEKKO()
    # horizon
    m.time  = np.linspace(0,3,4)
    # data vectors
    EP      = m.Param([0.1,0.05,0.2,0.25])
    Dem     = m.Param([10,12,9,8])
    PV      = m.Param([10,11,8,10])
    # constants
    bat_cap = 13.5
    ch_eff  = 0.94
    dis_eff = 0.94
    # manipulated variables
    Pbat_ch = m.MV(lb=0, ub=4)
    Pbat_ch.DCOST   = 0
    Pbat_ch.STATUS  = 1
    Pbat_dis = m.MV(lb=0, ub=4)
    Pbat_dis.DCOST  = 0
    Pbat_dis.STATUS = 1
    Pgrid_in = m.MV(lb=0, ub=3)    
    Pgrid_in.DCOST  = 0
    Pgrid_in.STATUS = 1
    Pgrid_out = m.MV(lb=0, ub=3) 
    Pgrid_out.DCOST  = 0
    Pgrid_out.STATUS = 1
    #State of Charge Battery
    SoC = m.Var(value=0.5, lb=0.2, ub=1)
    #Battery Balance
    m.Equation(bat_cap * SoC.dt() == -dis_eff*Pbat_dis + ch_eff*Pbat_ch)
    #Energy Balance
    m.Equation(Dem + Pbat_ch + Pgrid_in == PV + Pbat_dis + Pgrid_out)
    #Objective
    m.Minimize(EP*Pgrid_in)
    # sell power at 90% of purchase (in) price
    m.Maximize(0.9*EP*Pgrid_out)
    m.options.IMODE=6
    m.options.NODES=3
    m.options.SOLVER=3 
    m.solve()
    

    I modified your differential equation to remove SoC from the right hand side, otherwise you'll get an exponential increase. The energy balance differential equation is Accumulation=In-Out. Here is some additional code to visualize the solution.

    Battery State of Charge

    import matplotlib.pyplot as plt
    plt.subplot(3,1,1)
    plt.plot(m.time,SoC.value,'b--',label='State of Charge')
    plt.ylabel('SoC')
    plt.legend()
    plt.subplot(3,1,2)
    plt.plot(m.time,Dem.value,'r--',label='Demand')
    plt.plot(m.time,PV.value,'k:',label='PV Production')
    plt.legend()
    plt.subplot(3,1,3)
    plt.plot(m.time,Pbat_ch.value,'g--',label='Battery Charge')
    plt.plot(m.time,Pbat_dis.value,'r:',label='Battery Discharge')
    plt.plot(m.time,Pgrid_in.value,'k--',label='Grid Power In')
    plt.plot(m.time,Pgrid_in.value,':',color='orange',label='Grid Power Out')
    plt.ylabel('Power')
    plt.legend()
    plt.xlabel('Time')
    plt.show()