Search code examples
pythonoptimizationpyomopulpscipy-optimize

Constrained Optimization of battery scheduling in microgrid


Given inputs such as electricity consumption, generation from solar panel, price, (All at a given time t), we have a battery, and we want to evaluate how much it should (dis)/charge at any given time. The Problem can be formulated as follows:

Pt = price of electricity at time t

Lt = consumption of electricity at time t

Zt = charge of battery at time t (how much is in the battery)

St = Electricity generated from solar generator at time t

Qt = amount the battery (dis)/charges at time t

the function we are trying to optimise is Ct = Pt *(Lt - St - Qt)

This aims to minimise the amount of electricity purchased

With the following constraints:

Lt - St - Qt >= 0 (our demand has to be non-negative)

Qmin <= Qt <= Qmax ( the battery can only (dis)/charge between certain values at any given time)

Zmin <= Zt <= Zmax. (the battery has to be within its capacity, i.e. you can't discharge more than the battery holders, and you can charge more than the battery can hold)

Zt+1 = Zt + Qt+1 ( this means that the battery level at the next time step is equal to the battery level at the previous time step plus the amount that was (dis)/charged from the battery)

The problem I am having how to formulate in python (Scipy) the problem, particularly updating the battery levels.

I know other library's (Pyomo, Pulp) exist, solutions in that would be welcome.


Solution

  • You're in luck, I was motivated by Giorgio's answer to learn pyomo (I mostly user PULP), so used your question as a chance to make sure I understood all the interfaces. I'll post it here so I can find it again myself in the future:

    import pyomo.environ as pyomo
    import numpy as np
    
    # create model
    m = pyomo.ConcreteModel()
    
    # Problem DATA
    T = 24
    
    Zmin = 0.0
    Zmax = 2.0
    
    Qmin = -1.0
    Qmax = 1.0
    
    # Generate prices, solar output and load signals
    np.random.seed(42)
    P = np.random.rand(T)*5.0
    S = np.random.rand(T)
    L = np.random.rand(T)*2.0
    
    # Indexes
    times = range(T)
    times_plus_1 = range(T+1)
    
    # Decisions variables
    m.Q = pyomo.Var(times, domain=pyomo.Reals)
    m.Z = pyomo.Var(times_plus_1, domain=pyomo.NonNegativeReals)
    
    # objective
    cost = sum(P[t]*(L[t] - S[t] - m.Q[t]) for t in times)
    m.cost = pyomo.Objective(expr = cost, sense=pyomo.minimize)
    
    # constraints
    m.cons = pyomo.ConstraintList()
    m.cons.add(m.Z[0] == 0.5*(Zmin + Zmax))
    
    for t in times:
        m.cons.add(pyomo.inequality(Qmin, m.Q[t], Qmax))
        m.cons.add(pyomo.inequality(Zmin, m.Z[t], Zmax))
        m.cons.add(m.Z[t+1] == m.Z[t] - m.Q[t])
        m.cons.add(L[t] - S[t] - m.Q[t] >= 0)
    
    # solve
    solver = pyomo.SolverFactory('cbc')
    solver.solve(m)
    
    # display results
    print("Total cost =", m.cost(), ".")
    
    for v in m.component_objects(pyomo.Var, active=True):
        print ("Variable component object",v)
        print ("Type of component object: ", str(type(v))[1:-1]) # Stripping <> for nbconvert
        varobject = getattr(m, str(v))
        print ("Type of object accessed via getattr: ", str(type(varobject))[1:-1])
    
        for index in varobject:
            print ("   ", index, varobject[index].value)