I am currently building a simple optimisation tool for battery storage. At time t, the battery is loaded by Q_charge[t], which is the electricity drawn from the electric grid Q_el[t] multiplied by an efficiency factor eta_charge.
The discharge power Q_discharge[t] is multiplied by an efficiency factor eta_discharge and gives the demand Q_demand[t]. Demand profile is known for every hour of the year.
The battery SOC(t) is defined as state of charge at previous time step + power input - power output. No losses considered.
The state of charge at t=0 should be equal to the last hour of the period considered.
Charge and discharge cannot occurr simultaneously.
Charge and discharge have min and max limits.
Knowing the price of electricity Price[t] for every hour of the year, the objective function will decide when to charge, minimizing the cost of purchase. The demand is used internally, no electricity sold.
Demand[t] is known (imported from Excel). Price[t] is known (imported from Excel).
Here the code.
model = pyo.ConcreteModel()
##Parameters
tot_time=8760 #Hours in a year
model.T=pyo.Set(initialize=[t for t in range(0,tot_time)])
model.EtaCharge=pyo.Param(initialize=1.0, mutable=True) #-
model.PMinCharge=0 #MW
model.PMaxCharge=2 #MW
model.EtaDischarge=pyo.Param(initialize=1.0, mutable=True) #-
model.PMinDischarge=2 #MW
model.PMaxDischarge=4 #MW
model.S0=pyo.Param(initialize=0.0, mutable=True) #MWh SOC at t=0
##Variables
model.QEl = pyo.Var(model.T, within=NonNegativeReals, initialize=0)
model.QCharge= pyo.Var(model.T, within=NonNegativeReals)
model.QDischarge= pyo.Var(model.T, within=NonNegativeReals)
model.Charge=pyo.Var(model.T, within = pyo.Binary)
model.Discharge=pyo.Var(model.T, within = pyo.Binary)
model.SOC=pyo.Var(model.T, bounds=(0,100)) #Size of the battery limited to 100 MWh
##Constraints
model.C1 = pyo.ConstraintList()
model.C2 = pyo.ConstraintList()
model.C3 = pyo.ConstraintList()
model.C4 = pyo.ConstraintList()
model.C5 = pyo.ConstraintList()
model.C6 = pyo.ConstraintList()
model.C7 = pyo.ConstraintList()
for t in model.T:
model.C1.add(model.QEl[t] * model.EtaCharge == model.QCharge[t])
model.C2.add(model.QDischarge[t] * model.EtaDischarge == Demand[t])
model.C3.add(model.Charge[t] + model.Discharge[t] <= 1)
model.C4.add(model.QCharge[t] >= model.PMinCharge*model.Charge[t])
model.C5.add(model.QCharge[t] <= model.PMaxCharge*model.Charge[t])
model.C6.add(model.QDischarge[t] >= model.PMinDischarge*model.Discharge[t])
model.C7.add(model.QDischarge[t] <= model.PMaxDischarge*model.Discharge[t])
def SOC_storage(model,t):
if t == 0:
return (model.SOC[t] == model.S0+ model.QCharge[t]-model.QDischarge[t])
else:
return (model.SOC[t] == model.SOC[t-1] + model.QCharge[t] - model.QDischarge[t])
model.C8 = pyo.Constraint(model.T,rule = SOC_storage)
model.C9 = pyo.Constraint(expr = model.S0 == model.SOC[len(tot_time)-1])
##Objective
model.objective = pyo.Objective(expr = sum(model.QEl[t]*Price[t] for t in model.T), sense = pyo.minimize)
opt=SolverFactory('glpk')
results = opt.solve(model, tee = True)
model.write("myfile_lp.lp", io_options={'symbolic_solver_labels':True})
results.solver.status
results.solver.termination_condition
model.pprint()
value(model.objective)
The code, as it is, runs smoothly. However, how can I insert the condition to NOT buy electricity (Charge[t]=0) only during specific hours? Let's say the battery cannot be charged between 08.00 and 15.00 every day? I tried with:
day_stop=np.arange(8,15,1) # Index of hours When we don't charge
week_stop= np.concatenate([day_stop+24*j for j in range(0,7)]) # Index of hours When we don't charge for the first week
year_stop = np.concatenate([week_stop+168*j for j in range(0,52)]) # Index of hours When we don't charge for the all year
def NoCharge(model,t):
if t in year_stop :
return model.Charge[t]==0
else:
return Constraint.Skip
model.C10 = pyo.Constraint(model.T,rule = NoCharge)
All the variables after running the code are equal to zero or None. Am I formulating the constraint in a wrong way?
Any idea how to formulate the latest constraint in a better way? Thanks so much for your help!
Your approach with the NoCharge
function appears to be correct. It is a bit difficult to troubleshoot without the data. So... I made some up.
Some recommendations:
pprint
and display
the model and see what is going on and x-check itcharge = 1 - discharge
is true and you allow that charge could equal zero, so "idle" is included. You could just include 1 of the 2 binaries and use the "1 minus" construct where needed.Anyhow, the below works and produces a result. I used your C10 constraint and you can toggle it and C11 on/off to see that it is working in the output. C11 is redundant with C10 and just shows an alternate construct... You can use either.
import pyomo.environ as pyo
model = pyo.ConcreteModel()
##Parameters
tot_time=10 #Hours in a year
Demand = [0, 2, 3, 0, 0, 3, 3, 0, 0, 3]
Price = [0.5 for t in range(tot_time)]
model.T=pyo.Set(initialize=[t for t in range(0,tot_time)])
model.EtaCharge=pyo.Param(initialize=1.0, mutable=True) #-
model.PMinCharge=0 #MW
model.PMaxCharge=5 #MW
model.EtaDischarge=pyo.Param(initialize=1.0, mutable=True) #-
model.PMinDischarge=2 #MW
model.PMaxDischarge=4 #MW
model.S0=pyo.Param(initialize=10, mutable=True) #MWh SOC at t=0
##Variables
model.QEl = pyo.Var(model.T, within=pyo.NonNegativeReals, initialize=0)
model.QCharge= pyo.Var(model.T, within=pyo.NonNegativeReals)
model.QDischarge= pyo.Var(model.T, within=pyo.NonNegativeReals)
model.Charge=pyo.Var(model.T, within = pyo.Binary)
model.Discharge=pyo.Var(model.T, within = pyo.Binary)
model.SOC=pyo.Var(model.T, bounds=(0,100)) #Size of the battery limited to 100 MWh
##Constraints
model.C1 = pyo.ConstraintList()
model.C2 = pyo.ConstraintList()
model.C3 = pyo.ConstraintList()
model.C4 = pyo.ConstraintList()
model.C5 = pyo.ConstraintList()
model.C6 = pyo.ConstraintList()
model.C7 = pyo.ConstraintList()
for t in model.T:
model.C1.add(model.QEl[t] * model.EtaCharge == model.QCharge[t])
model.C2.add(model.QDischarge[t] * model.EtaDischarge == Demand[t])
model.C3.add(model.Charge[t] + model.Discharge[t] <= 1)
model.C4.add(model.QCharge[t] >= model.PMinCharge*model.Charge[t])
model.C5.add(model.QCharge[t] <= model.PMaxCharge*model.Charge[t])
model.C6.add(model.QDischarge[t] >= model.PMinDischarge*model.Discharge[t])
model.C7.add(model.QDischarge[t] <= model.PMaxDischarge*model.Discharge[t])
def SOC_storage(model,t):
if t == 0:
return (model.SOC[t] == model.S0 + model.QCharge[t] - model.QDischarge[t])
else:
return (model.SOC[t] == model.SOC[t-1] + model.QCharge[t] - model.QDischarge[t])
model.C8 = pyo.Constraint(model.T,rule = SOC_storage)
model.C9 = pyo.Constraint(expr = model.S0 == model.SOC[tot_time-1])
def NoCharge(model,t):
if t in no_charge_zone :
return model.Charge[t]==0
else:
return pyo.Constraint.Skip
no_charge_zone = [7, 8]
model.C10 = pyo.Constraint(model.T,rule = NoCharge)
# The below C11 is redundant with C10... It just shows a different approach.
# I think this is cleaner.... Just make a subset from your "prohibited" time periods
# and pass that in...
def NoCharge2(model, t):
return model.Charge[t] == 0
model.C11 = pyo.Constraint(no_charge_zone, rule=NoCharge2)
##Objective
model.objective = pyo.Objective(expr = sum(model.QEl[t]*Price[t] for t in model.T), sense = pyo.minimize)
opt=pyo.SolverFactory('glpk')
results = opt.solve(model, tee = True)
#model.write("myfile_lp.lp", io_options={'symbolic_solver_labels':True})
results.solver.status
results.solver.termination_condition
#model.pprint()
model.Charge.display()
model.SOC.display()
#print(pyo.value(model.objective))
INTEGER OPTIMAL SOLUTION FOUND
Time used: 0.0 secs
Memory used: 0.1 Mb (87515 bytes)
Writing MIP solution to '/var/folders/rt/5gc2md7s7y5bw8ddt74tz5vw0000gp/T/tmpi82nq7rg.glpk.raw'...
154 lines were written
Charge : Size=10, Index=T
Key : Lower : Value : Upper : Fixed : Stale : Domain
0 : 0 : 1.0 : 1 : False : False : Binary
1 : 0 : 0.0 : 1 : False : False : Binary
2 : 0 : 0.0 : 1 : False : False : Binary
3 : 0 : 1.0 : 1 : False : False : Binary
4 : 0 : 1.0 : 1 : False : False : Binary
5 : 0 : 0.0 : 1 : False : False : Binary
6 : 0 : 0.0 : 1 : False : False : Binary
7 : 0 : 0.0 : 1 : False : False : Binary
8 : 0 : 0.0 : 1 : False : False : Binary
9 : 0 : 0.0 : 1 : False : False : Binary
SOC : Size=10, Index=T
Key : Lower : Value : Upper : Fixed : Stale : Domain
0 : 0 : 14.0 : 100 : False : False : Reals
1 : 0 : 12.0 : 100 : False : False : Reals
2 : 0 : 9.0 : 100 : False : False : Reals
3 : 0 : 14.0 : 100 : False : False : Reals
4 : 0 : 19.0 : 100 : False : False : Reals
5 : 0 : 16.0 : 100 : False : False : Reals
6 : 0 : 13.0 : 100 : False : False : Reals
7 : 0 : 13.0 : 100 : False : False : Reals
8 : 0 : 13.0 : 100 : False : False : Reals
9 : 0 : 10.0 : 100 : False : False : Reals