Search code examples
pythonoptimizationpyomo

Linear optimization of a battery storage


Im currently trying to optimize a battery storage with pyomo. I thought everything was working but for some reason the storage is always getting discharged at the start. This should be impossible because the storage is empty at the beginning...

def discharge_capacity_rule(model, t):
    return model.discharge[t] <= in_out_leistung
model.discharge_capacity_rule = Constraint(model.t, rule = discharge_capacity_rule)
def charge_capacity_rule(model, t):
    return model.charge[t] <= in_out_leistung
model.charge_capacity_rule = Constraint(model.t, rule = charge_capacity_rule)

def max_capacity_rule(model, t):
    return model.soe[t] <= battery_capacity
model.max_capacity_rule = Constraint(model.t, rule = max_capacity_rule)

def soe_start_rule(model, t):
    return model.soe[0] == soe_start 
model.soe_start_rule = Constraint(rule = soe_start_rule)
def soe_end_rule(model, t):
    return model.soe[n] == model.soe[0]
model.soe_end_rule = Constraint(rule = soe_end_rule)

def soe_rule(model, t):
    if t == 0:
        return model.soe[t] == soe_start
    else:
        return model.soe[t] == model.soe[t-1] + (model.charge[t] * in_out_efficiency) - (model.discharge[t] / in_out_efficiency)
model.soe_rule = Constraint(model.t, rule = soe_rule)

This is what the output looks like


Solution

  • This is a common issue in the BESS optimization. You're constraining model.discharge[t] just for power (i.e., any discharge can't surpass the nominal power output of BESS), but there is not explicit constraint to discharging beyond available energy. The model.soe computation is a common way to avoid discharging beyond the available stored energy, but during the initial time-step, your just avoiding the computation of state-of-charge for BESS and setting to initial value with return model.soe[t] == soe_start, therefore, discharging is not constrained to model.soe_rule in this time-step, since the following constraint applies for all time-step but initial one.

    model.soe[t] == model.soe[t-1] + (model.charge[t] * in_out_efficiency) - (model.discharge[t] / in_out_efficiency)
    

    You can try some approaches:

    1. Constraint discharge to available stored energy:

      You can constraint model.discharge[t] to be less or equal than available stored energy any time.step. This is equivalent to the SOE computation, since model.soe cannot be less than zero, but that wouls also apply during the initial time-step. The constraint would be something like this:

       def discharge_leq_soe(model, t):
           model.discharge[t] <= model.soe[t]
       model.discharge_leq_soe = pyo.Constraint(model.t)
      
    2. Fix the initial charge:

      As you comment, you can just say in the initial time-step, discharge in disallowed, that would set model.discharge to zero during initial time-step:

       model.initial_discharge_fixed = pyo.Constraint(expr=model.discharge[model.t.first()]==0)
      
    3. Not fixed initial soe:

      You can also not fix the initial state-of-charge, but compute it. In this escenario, model.charge and model.discharge are unfixed and model.soe[0] is computed from the results. This can give you some sort of insights about required initial SOE to improve de BESS dispatch. In this approach you just need to change your soe_rule to:

       def soe_rule(model, t):
           if t == 0:
               return model.soe[t] == (model.charge[t] * in_out_efficiency) - (model.discharge[t] / in_out_efficiency)
           else:
               return model.soe[t] == model.soe[t-1] + (model.charge[t] * in_out_efficiency) - (model.discharge[t] / in_out_efficiency)
       model.soe_rule = Constraint(model.t, rule = soe_rule