Unit Commitment: Control Integer Programming Logic Programming Planning Simulation
Unit Commitment: Control Integer Programming Logic Programming Planning Simulation
Tip: The most important thing we learn in this example is that you never
multiply binary variables with continuous variables to model on/off
behavior. Instead, we derive equivalent linear presentations.
Note that this is a completely fictitious example created by someone (me) with
very little exposure and experience from this field. The purpose is to highlight
modeling tricks and YALMIP, not to give a tutorial or best-practice description
on unit commitment problems.
Before running these examples, you must install a strong MILP solver (if you
don’t have one installed, YALMIP will use its very naive internal integer
solver BNB which will fail to solve most problems here in reasonable time, if at
all).
Pmax = [100;50;25];
Pmin = [20;40;1];
We start building our model based on this logic (remember, this can be
vectorized, we use for-loops only to make the model easier to understand)
Constraints = [];
for k = 1:Horizon
Constraints = [Constraints, onoff(:,k).*Pmin <= P(:,k) <= onoff(:,k).*Pmax];
end
The total demand delivered at each time instant must be sufficiently large
for k = 1:Horizon
Constraints = [Constraints, sum(P(:,k)) >= Pforecast(k)];
end
Objective = 0;
for k = 1:Horizon
Objective = Objective + P(:,k)'*Q*P(:,k) + C*P(:,k);
end
The first simple model is complete, and we can solve the problem and display
the results (this requires that you have an efficient mixed-integer QP
solver installed.)
Tip: Always turn on full display and debug mode when developing new
models. Once you are certain everything works as expected, you can turn off
display for improved performance.
ops = sdpsettings('verbose',1,'debug',1);
optimize(Constraints,Objective,ops)
stairs(value(P)');
legend('Unit 1','Unit 2','Unit 3');
Adding minimum up- and down-time
The model above is far from realistic, and one of the main flaws is that it
assumes that we can turn on and off the plants arbitrarily. Most often, if a
plant is turned on, it has to run for a number of time units, and when turned
off, it will be gone for a number of time units (restarting is complicated etc).
We define two new variables describing these plant characteristics
minup = [6;30;1];
mindown = [3;6;3];
for k = 2:Horizon
for unit = 1:Nunits
% indicator will be 1 only when switched on
indicator = onoff(unit,k)-onoff(unit,k-1);
range = k:min(Horizon,k+minup(unit)-1);
% Constraints will be redundant unless indicator = 1
Constraints = [Constraints, onoff(unit,range) >= indicator];
end
end
for k = 2:Horizon
for unit = 1:Nunits
% indicator will be 1 only when switched off
indicator = onoff(unit,k-1)-onoff(unit,k);
range = k:min(Horizon,k+mindown(unit)-1);
% Constraints will be redundant unless indicator = 1
Constraints = [Constraints, onoff(unit,range) <= 1-indicator];
end
end
ops = sdpsettings('verbose',2,'debug',1);
optimize(Constraints,Objective,ops);
stairs(value(P)');
legend('Unit 1','Unit 2','Unit 3');
Quantized power-levels
Some plants can only be run in a finite number of configurations, thus making
the delivered power a quantized variable. Here, we assume plant 3 is limited in
such a way and the quantization is modelled using the ismember operator.
Unit3Levels = [0 1 6 10 12 20];
for k = 1:Horizon
Constraints = [Constraints, ismember(P(3,k),Unit3Levels)];
end
optimize(Constraints,Objective);
stairs(value(P)');
legend('Unit 1','Unit 2','Unit 3');
Efficient simulation
As a finale, let us simulate this plant control strategy in closed-loop. To do this
we have to make some changes. To begin with, we must introduce a history.
The action we take now depends on our past. If we turned on plant two 10
time units ago, we must still have it on, etc. The second thing we should do is
to make the simulation efficient, by avoiding a complete redefinition of the
whole optimization problem every time instant. To do so, we use
the optimizer command. Finally, to make it realistic, we should have some
disturbances on the power demand, and to cope with this, we add a simple
slack on the power demand constraint, and penalize this slack in the objective
function.
Nhist = max([minup;mindown]);
Pforecast = sdpvar(1,Horizon);
HistoryOnOff = sdpvar(Nunits,Nhist,'full');
DemandSlack = sdpvar(1,Horizon);
PreviusP = sdpvar(3,1);
DemandPenalty = 1000;
ChangePenalty = 1000;
Constraints = [];
Objective = 0;
for k = 1:Horizon
Constraints = [Constraints, onoff(:,k).*Pmin <= P(:,k) <= onoff(:,k).*Pmax];
Constraints = [Constraints, sum(P(:,k))+DemandSlack(k) >= Pforecast(k)];
Constraints = [Constraints, DemandSlack(k) >= 0];
Objective = Objective + P(:,k)'*Q*P(:,k) + C*P(:,k);
Objective = Objective + DemandPenalty*DemandSlack(k);
end
function C = consequtiveON(x,minup)
if min(size(x))==1
x = x(:)';
end
if size(x,1) ~= size(minup,1)
error('MINUP should have as many rows as X');
end
Horizon = size(x,2);
C = [];
for k = 2:size(x,2)
for unit = 1:size(x,1)
% indicator will be 1 only when switched on
indicator = x(unit,k)-x(unit,k-1);
range = k:min(Horizon,k+minup(unit)-1);
% Constraints will be redundant unless indicator = 1
affected = x(unit,range);
if strcmp(class(affected),'sdpvar')
% ISA behaves buggy, hence we use class+strcmp
C = [C, affected >= indicator];
end
end
end
This function is now used on our concatenated history and future sequences.
Note that a down-time constraint can be formulated by applying a suitably
defined up-time constraint
for k = 2:Horizon
Objective = Objective + ChangePenalty*norm(P(:,k)-P(:,k-1),1);
end
Objective = Objective + ChangePenalty*norm(P(:,1)-PreviusP,1);
We are now ready to create our optimizer object. Our optimizer object solves
the optimization problem for a particular instance of the forecast demand and
history, and returns the optimal powers and on-off sequences. We’re still
running with full diagnostic display to be able to detect any issues you might
have with your installation.
We create a history where we assume plant one has been running at 100,
plant two at 40, and the third plant has been turned off.
oldOnOff = repmat([1;1;0],1,Nhist);
oldP = repmat([100;40;0],1,Nhist);
Simulate!
for k = 1:500
% Base-line forecast
forecast = 100;
% Daily fluctuation
forecast = forecast + 50*sin((k:k+Horizon-1)*2*pi/24);
% Some other effect
forecast = forecast + 20*sin((k:k+Horizon-1)*2*pi/24/7);
% and yet some other effect
forecast = forecast + randn(1,Horizon)*5;
hold off
stairs([-Nhist+1:0 1:Horizon],[oldP optimalP]');
hold on
stairs(1:Horizon,forecast,'k+');
axis([-Nhist Horizon 0 170]);
drawnow
pause(0.05);
% Shift history
oldP = [oldP(:,2:end) optimalP(:,1)];
oldOnOff = [oldOnOff(:,2:end) optimalOnOff(:,1)];
end
Play around with the various prices and characteristics, and you will see that
you can create very different simulations.
If your browser supports animated png, you will see an animation of the
simulation in the figure below.
PreviousNext
LEAVE A COMMENT