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 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:
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)
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)
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