1080 lines
38 KiB
Matlab
Executable File
1080 lines
38 KiB
Matlab
Executable File
function output = cutsdp(p)
|
|
%CUTSDP Cutting plane solver for mixed integer SDP problems
|
|
%
|
|
% CUTSDP is never called by the user directly, but is called by
|
|
% YALMIP from OPTIMIZE, by choosing the solver tag 'cutsdp' in sdpsettings.
|
|
%
|
|
% The behaviour of CUTSDP can be altered using the fields
|
|
% in the field 'cutsdp' in SDPSETTINGS
|
|
%
|
|
% solver Solver for the relaxed problems (standard solver tag, see SDPSETTINGS)
|
|
%
|
|
% maxiter Maximum number of nodes explored
|
|
%
|
|
% maxtime Maximum time allowed
|
|
%
|
|
% feastol Tolerance for declaring constraints as feasible
|
|
%
|
|
% cutlimit Maximum number of LP cuts added in every iteration
|
|
%
|
|
% twophase Start by solving integer-relaxed LPs for a while
|
|
%
|
|
% See also OPTIMIZE, BNB, BINVAR, INTVAR, BINARY, INTEGER
|
|
|
|
% *************************************************************************
|
|
%% INITIALIZE DIAGNOSTICS IN YALMIP
|
|
% *************************************************************************
|
|
bnbsolvertime = clock;
|
|
showprogress('Cutting plane solver started',p.options.showprogress);
|
|
optionsIn = p.options;
|
|
|
|
% ********************************
|
|
%% Remove options if none has been changed
|
|
%% Improves peroformance when calling solver many times
|
|
%% (some solvers are really slow at checking options)
|
|
% ********************************
|
|
p.options = pruneOptions(p.options);
|
|
|
|
% ********************************
|
|
%% Always try to warm-start
|
|
% ********************************
|
|
p.options.usex0 = 1;
|
|
|
|
% Arg, new format
|
|
p.options.cutsdp.feastol = -p.options.cutsdp.feastol;
|
|
|
|
% *************************************************************************
|
|
%% If we want duals, we may not extract bounds. However, bounds must be
|
|
% extracted in discrete problems.
|
|
% *************************************************************************
|
|
p.noninteger_variables = setdiff(1:length(p.c),[p.integer_variables p.binary_variables p.semicont_variables]);
|
|
|
|
% *************************************************************************
|
|
%% Define infinite bounds
|
|
% *************************************************************************
|
|
if isempty(p.ub)
|
|
p.ub = repmat(inf,length(p.c),1);
|
|
end
|
|
if isempty(p.lb)
|
|
p.lb = repmat(-inf,length(p.c),1);
|
|
end
|
|
|
|
% *************************************************************************
|
|
%% ADD CONSTRAINTS 0<=x<=1 FOR BINARY
|
|
% *************************************************************************
|
|
if ~isempty(p.binary_variables)
|
|
p.ub(p.binary_variables) = min(p.ub(p.binary_variables),1);
|
|
p.lb(p.binary_variables) = max(p.lb(p.binary_variables),0);
|
|
end
|
|
|
|
% *************************************************************************
|
|
%% Extract better bounds from model
|
|
% *************************************************************************
|
|
if ~isempty(p.F_struc)
|
|
[lb,ub,used_rows_eq,used_rows_lp] = findulb(p.F_struc,p.K);
|
|
if ~isempty([used_rows_eq(:);used_rows_lp(:)])
|
|
lower_defined = find(~isinf(lb));
|
|
if ~isempty(lower_defined)
|
|
p.lb(lower_defined) = max(p.lb(lower_defined),lb(lower_defined));
|
|
end
|
|
upper_defined = find(~isinf(ub));
|
|
if ~isempty(upper_defined)
|
|
p.ub(upper_defined) = min(p.ub(upper_defined),ub(upper_defined));
|
|
end
|
|
p.F_struc(p.K.f+used_rows_lp,:)=[];
|
|
p.F_struc(used_rows_eq,:)=[];
|
|
p.K.l = p.K.l - length(used_rows_lp);
|
|
p.K.f = p.K.f - length(used_rows_eq);
|
|
end
|
|
end
|
|
|
|
% *************************************************************************
|
|
%% ADD CONSTRAINTS 0<x<1 FOR BINARY
|
|
% *************************************************************************
|
|
if ~isempty(p.binary_variables)
|
|
p.ub(p.binary_variables) = min(p.ub(p.binary_variables),1);
|
|
p.lb(p.binary_variables) = max(p.lb(p.binary_variables),0);
|
|
end
|
|
|
|
% *************************************************************************
|
|
%% PRE-SOLVE (nothing fancy coded)
|
|
% *************************************************************************
|
|
if isempty(find(isinf([p.ub;p.lb]))) & p.K.l>0
|
|
[p.lb,p.ub] = tightenbounds(-p.F_struc(1+p.K.f:p.K.f+p.K.l,2:end),p.F_struc(1+p.K.f:p.K.f+p.K.l,1),p.lb,p.ub,p.integer_variables);
|
|
end
|
|
|
|
% *************************************************************************
|
|
%% We don't need this
|
|
% *************************************************************************
|
|
p.options.savesolverinput = 0;
|
|
p.options.savesolveroutput = 0;
|
|
|
|
% *************************************************************************
|
|
%% Display logics
|
|
% 0 : Silent
|
|
% <1: display every 1/N th iteration
|
|
% 1 : Display cut progress
|
|
% 3 : Display node solver prints
|
|
% *************************************************************************
|
|
if p.options.verbose ~= fix(p.options.verbose)
|
|
p.options.print_interval = ceil(1/p.options.verbose);
|
|
p.options.verbose = ceil(p.options.verbose);
|
|
else
|
|
p.options.print_interval = 1;
|
|
end
|
|
switch max(min(p.options.verbose,3),0)
|
|
case 0
|
|
p.options.cutsdp.verbose = 0;
|
|
case 1
|
|
p.options.cutsdp.verbose = 1;
|
|
p.options.verbose = 0;
|
|
case 2
|
|
p.options.cutsdp.verbose = 2;
|
|
p.options.verbose = 0;
|
|
case 3
|
|
p.options.cutsdp.verbose = 2;
|
|
p.options.verbose = 1;
|
|
otherwise
|
|
p.options.cutsdp.verbose = 0;
|
|
p.options.verbose = 0;
|
|
end
|
|
|
|
% *************************************************************************
|
|
%% START CUTTING
|
|
% *************************************************************************
|
|
cutsdpsolvertime = clock;
|
|
[x_min,solved_nodes,lower,feasible,D_struc,interrupted] = cutting(p);
|
|
|
|
% *************************************************************************
|
|
%% CREATE SOLUTION
|
|
% *************************************************************************
|
|
if interrupted
|
|
output.problem = 16;
|
|
else
|
|
output.problem = 0;
|
|
if ~feasible
|
|
output.problem = 1;
|
|
end
|
|
if solved_nodes == p.options.cutsdp.maxiter
|
|
output.problem = 3;
|
|
elseif etime(clock,cutsdpsolvertime) > p.options.cutsdp.maxtime
|
|
output.problem = 3;
|
|
end
|
|
end
|
|
output.solved_nodes = solved_nodes;
|
|
output.Primal = x_min;
|
|
output.Dual = D_struc;
|
|
output.Slack = [];
|
|
output.solverinput = 0;
|
|
if optionsIn.savesolveroutput
|
|
output.solveroutput.solved_nodes =solved_nodes;
|
|
else
|
|
output.solveroutput =[];
|
|
end
|
|
output.solvertime = etime(clock,bnbsolvertime);
|
|
%% --
|
|
|
|
function [x,solved_nodes,lower,feasible,D_struc,interrupted] = cutting(p)
|
|
|
|
interrupted = 0;
|
|
% *************************************************************************
|
|
%% Sanity check
|
|
% *************************************************************************
|
|
if any(p.lb>p.ub)
|
|
x = zeros(length(p.c),1);
|
|
solved_nodes = 0;
|
|
lower = inf;
|
|
feasible = 0;
|
|
D_struc = [];
|
|
return
|
|
end
|
|
|
|
cutsdpsolvertime = clock;
|
|
|
|
% Solver feasibility too low can cause issues in the termination.
|
|
% However, some solver installations will fail if we do this, so it is not
|
|
% default (we have way to combat low precision anyway)
|
|
if p.options.cutsdp.adjustsolvertol
|
|
p = adjustSolverPrecision(p);
|
|
end
|
|
|
|
% *************************************************************************
|
|
%% Create function handle to solver
|
|
% *************************************************************************
|
|
cutsolver = p.solver.lower.call;
|
|
|
|
p = addImpliedSDP(p);
|
|
|
|
% *************************************************************************
|
|
%% Create copy of model without the Conic part
|
|
% *************************************************************************
|
|
p_lp = p;
|
|
p_lp.F_struc = p_lp.F_struc(1:p.K.l+p.K.f,:);
|
|
p_lp.K.s = 0;
|
|
p_lp.K.q = 0;
|
|
|
|
p_original = p;
|
|
|
|
% *************************************************************************
|
|
%% DISPLAY HEADER
|
|
% *************************************************************************
|
|
if p.options.cutsdp.verbose
|
|
disp('* Starting YALMIP cutting plane for MISDP based on MILP');
|
|
disp(['* Lower solver : ' p.solver.lower.tag]);
|
|
disp(['* Max iterations : ' num2str(p.options.cutsdp.maxiter)]);
|
|
disp(['* Max time : ' num2str(p.options.cutsdp.maxtime)]);
|
|
end
|
|
|
|
if p.options.cutsdp.verbose
|
|
if p.K.s(1)>0
|
|
disp(' Node Phase Cone infeas Integrality infeas Lower bound Upper bound LP cuts Elapsed time');
|
|
else
|
|
disp(' Node Phase Cone infeas Integrality infeas Lower bound Upper bound LP cuts Elapsed time');
|
|
end
|
|
end
|
|
|
|
% Rhs of SOCP has to be non-negative
|
|
if ~p.solver.lower.constraint.inequalities.secondordercone.linear
|
|
p_lp = addSOCPCut(p,p_lp);
|
|
end
|
|
|
|
% SDP diagonal has to be non-negative
|
|
p_lp = addDiagonalCuts(p,p_lp);
|
|
|
|
% Experimentation with activation cuts on 2x2 structures in problems with
|
|
% all binary variables 2x2 = constant not psd + M(x) means some x has to be
|
|
% non-zero
|
|
p_lp = addActivationCuts(p,p_lp);
|
|
p_lp = removeRedundant(p_lp);
|
|
|
|
only_solvelp = 0;
|
|
|
|
% *************************************************************************
|
|
% Crude lower bound
|
|
% FIX for quadratic case
|
|
% *************************************************************************
|
|
lower = 0;
|
|
if nnz(p.Q) == 0
|
|
for i = 1:length(p.c)
|
|
if p.c(i)>0
|
|
if isinf(p.lb(i))
|
|
lower = -inf;
|
|
break
|
|
else
|
|
lower = lower + p.c(i)*p.lb(i);
|
|
end
|
|
elseif p.c(i)<0
|
|
if isinf(p.ub(i))
|
|
lower = -inf;
|
|
break
|
|
else
|
|
lower = lower + p.c(i)*p.ub(i);
|
|
end
|
|
end
|
|
end
|
|
end
|
|
%lower = sum(sign(p.c).*(p.lb));
|
|
if isinf(lower) | nnz(p.Q)~=0
|
|
lower = -1e6;
|
|
end
|
|
|
|
% *************************************************************************
|
|
% Experimental stuff for variable fixing
|
|
% *************************************************************************
|
|
if p.options.cutsdp.nodefix & (p.K.s(1)>0)
|
|
top=1+p.K.f+p.K.l+sum(p.K.q);
|
|
for i=1:length(p.K.s)
|
|
n=p.K.s(i);
|
|
for j=1:size(p.F_struc,2)-1;
|
|
X=full(reshape(p.F_struc(top:top+n^2-1,j+1),p.K.s(i),p.K.s(i)));
|
|
X=(X+X')/2;
|
|
v=real(eig(X+sqrt(eps)*eye(length(X))));
|
|
if all(v>=0)
|
|
sdpmonotinicity(i,j)=-1;
|
|
elseif all(v<=0)
|
|
sdpmonotinicity(i,j)=1;
|
|
else
|
|
sdpmonotinicity(i,j)=nan;
|
|
end
|
|
end
|
|
top=top+n^2;
|
|
end
|
|
else
|
|
sdpmonotinicity=[];
|
|
end
|
|
|
|
% Avoid data shuffling later on when creating cuts for SDPs
|
|
top = 1 + p.K.l+p.K.f+sum(p.K.q);
|
|
% Slicing columns much faster
|
|
p.F_struc = p.F_struc';
|
|
for i = 1:length(p.K.s)
|
|
p.semidefinite{i}.F_struc = p.F_struc(:,top:top+p.K.s(i)^2-1)';
|
|
if nnz(p.semidefinite{i}.F_struc)/numel(p.semidefinite{i}.F_struc) > .2
|
|
p.semidefinite{i}.F_struc = full(p.semidefinite{i}.F_struc);
|
|
end
|
|
p.semidefinite{i}.index = 1:p.K.s(i)^2;
|
|
top = top + p.K.s(i)^2;
|
|
end
|
|
p.F_struc = p.F_struc';
|
|
p.F_struc = p.F_struc(1:p.K.f+p.K.l+sum(p.K.q),:);
|
|
p.sdpsymmetry = [];
|
|
p = detect3x3SymmetryGroups(p);
|
|
[p,p_lp] = addSymmetryCuts(p,p_lp);
|
|
|
|
integer_variables = [p.binary_variables p.integer_variables];
|
|
|
|
standard_options = p_lp.options;
|
|
if p.options.cutsdp.twophase || isempty(integer_variables)
|
|
integerPhase = 0;
|
|
else
|
|
integerPhase = 1;
|
|
end
|
|
|
|
% Some structures for keeping a pool of cuts
|
|
% silly but we include equalities for simplicity, always active of course)
|
|
activity = zeros(p_lp.K.f + p_lp.K.l,1);
|
|
prevRem = [];
|
|
|
|
%% Initialize
|
|
x = [];
|
|
saveduals = 1;
|
|
infeasibility = -inf;
|
|
solved_nodes = 0;
|
|
feasible = 1;
|
|
lower = -inf;
|
|
upper = inf;
|
|
lowerHistory = -inf;
|
|
phaseHistory = [];
|
|
goon = 1;
|
|
x_found_by_sdp_pump = [];
|
|
p_original.sdpPumpData = [];
|
|
p_lp_unused = p_lp;
|
|
p_lp_unused.F_struc = [];
|
|
p_lp_unused.K.f = 0;
|
|
p_lp_unused.K.l = 0;
|
|
while goon
|
|
|
|
% Keep history of what we have been doing. Used for some diagnostics
|
|
phaseHistory = [phaseHistory integerPhase ];
|
|
|
|
% Basic presolve etc (currently not used)
|
|
p_lp = nodeTight(p,p_lp);
|
|
p_lp = nodeFix(p,p_lp);
|
|
|
|
% Add upper bound constraint, speeds up the MILP solver slightly somtimes
|
|
if ~isinf(upper) && (nnz(p_lp.Q)==0)
|
|
p_lp = addLinearCut(p_lp,[upper -p_lp.c']);
|
|
end
|
|
|
|
p_lp.options = standard_options;
|
|
|
|
% Solve relaxed problem
|
|
ptemp = p_lp;
|
|
ptemp.x0 = x;
|
|
if p.solver.lower.constraint.inequalities.secondordercone.linear
|
|
ptemp = p_lp;
|
|
ptemp.F_struc = [p_lp.F_struc;p.F_struc(1+p.K.f+p.K.l:p.K.f+p.K.l+sum(p.K.q),:)];
|
|
ptemp.K.q = p.K.q;
|
|
end
|
|
ptemp = adjustMaxTime(ptemp,ptemp.options.cutsdp.maxtime,etime(clock,cutsdpsolvertime));
|
|
if integerPhase
|
|
output = feval(cutsolver,ptemp);
|
|
if min(ptemp.F_struc*[1;output.Primal]) < -abs(p.options.cutsdp.feastol)
|
|
% Ugly hack
|
|
ptemp.F_struc = 1000*ptemp.F_struc;
|
|
output = feval(cutsolver,ptemp);
|
|
ptemp.F_struc = 0.001*ptemp.F_struc;
|
|
end
|
|
else
|
|
ptemp.binary_variables = [];
|
|
ptemp.integer_variables = [];
|
|
output = feval(cutsolver,ptemp);
|
|
if output.problem == 12
|
|
ptemp.c = ptemp.c*0;
|
|
ptemp.Q = ptemp.Q*0;
|
|
output = feval(cutsolver,ptemp);
|
|
end
|
|
end
|
|
|
|
% Remove upper bounds if we added those (avoid accumulating them)
|
|
if ~isinf(upper) && (nnz(p_lp.Q)==0)
|
|
p_lp.K.l = p_lp.K.l - 1;
|
|
p_lp.F_struc = p_lp.F_struc(1:end-1,:);
|
|
end
|
|
|
|
% Detect inactive cuts during integer-relaxed phase. Drop these, but
|
|
% same them in a pool to add them later if required
|
|
% [p_lp,activity,prevRem] = handleCutPool(p_lp,activity,prevRem,output,integerPhase,15);
|
|
|
|
infeasible_socp_cones = ones(1,length(p.K.q));
|
|
infeasible_sdp_cones = ones(1,length(p.K.s));
|
|
|
|
eig_failure = 0;
|
|
currentPhase = integerPhase ;
|
|
if output.problem == 1 | output.problem == 12
|
|
% LP relaxation was infeasible, hence problem is infeasible
|
|
feasible = 0;
|
|
lower = inf;
|
|
goon = 0;
|
|
x = zeros(length(p.c),1);
|
|
lower = inf;
|
|
cost = inf;
|
|
else
|
|
% Relaxed solution
|
|
x = output.Primal;
|
|
cost = p.f+p.c'*x+x'*p.Q*x;
|
|
if output.problem == 0
|
|
lower = cost;
|
|
end
|
|
|
|
was_lp_really_feasible = checkfeasiblefast(ptemp,x,-p.options.cutsdp.feastol);
|
|
|
|
if p.options.cutsdp.sdppump & integerPhase
|
|
[xtemp,upptemp,pumpPossible,p_original,pumpSuccess] = sdpPump(p_original,x,-p.options.cutsdp.feastol);
|
|
else
|
|
upptemp = inf;
|
|
end
|
|
|
|
infeasibility = 0;
|
|
[p_lp,infeasibility,infeasible_socp_cones] = add_socp_cut(p,p_lp,x,infeasibility);
|
|
[p_lp,infeasibility,infeasible_sdp_cones,eig_failure] = add_sdp_cut(p,p_lp,x,infeasibility,p_original);
|
|
|
|
% Don't add no-good if we time out etc, for purely integer problems
|
|
% or problems where we analytically can compute the continuous from
|
|
% given integers
|
|
if was_lp_really_feasible && isinf(upptemp) && (output.problem == 0 && ((integerPhase && pumpPossible) || (integerPhase && (length(p.integer_variables)==length(p.c)) && output.problem == 0)) )
|
|
p_lp = add_nogood_cut(p,p_lp,x,infeasibility);
|
|
end
|
|
|
|
if output.problem == 0 && ((integerPhase && pumpPossible) || (integerPhase && (length(p.integer_variables)==length(p.c)) && output.problem == 0))
|
|
p_lp = add3x3sdpsymmetrycut(p,p_lp,x);
|
|
end
|
|
|
|
if upptemp < upper
|
|
upper = upptemp;
|
|
x_found_by_sdp_pump = xtemp;
|
|
end
|
|
|
|
if p.options.cutsdp.plot
|
|
plotP(p_lp);drawnow
|
|
end
|
|
|
|
% Track improvement in lower bound. We terminate the
|
|
% integrality-relaxed phase if no objective improvement
|
|
lowerHistory = [lowerHistory lower];
|
|
if length(lowerHistory) > 10
|
|
b = mean(lowerHistory(end-10:end-1))+0.01*abs(mean(lowerHistory(end-10:end-1))) ;
|
|
noprogress = lower <= b;
|
|
else
|
|
noprogress = 0;
|
|
end
|
|
|
|
sdpInfeasibility = infeasibility;
|
|
% We terminate integrality-relaxed phase if solution is close to
|
|
% SDP-feasible, or no progress in objective, or too many nodes
|
|
if ~isempty(integer_variables) && (infeasibility >= p.options.cutsdp.feastol || solved_nodes > 2000 || noprogress) && ~integerPhase
|
|
integerPhase = 1;
|
|
infeasibility = infeasibility - inf;
|
|
feasible = 1;
|
|
elseif infeasibility >= p.options.cutsdp.feastol && integerPhase
|
|
feasible = 1;
|
|
upper = cost;
|
|
end
|
|
|
|
goon = infeasibility <= p.options.cutsdp.feastol || output.problem ==3;
|
|
|
|
if length(lowerHistory)>1 && phaseHistory(end)==0
|
|
% We made a massive jump when turning on integrality. This
|
|
% solution is not affected by the possible numerical problems
|
|
% asscociated with adding a lower bound to the relaxed problem
|
|
bigImprovementbyInteger = phaseHistory(end)==1 && phaseHistory(end-1)==0 && lowerHistory(end) >= lowerHistory(end-1) + 0.01*(1e-3 + lowerHistory(end-1));
|
|
elseif length(lowerHistory)==1
|
|
bigImprovementbyInteger = 1;
|
|
else
|
|
bigImprovementbyInteger = 0;
|
|
end
|
|
|
|
goon = goon & feasible;
|
|
goon = goon || eig_failure;% not psd, but no interesting eigenvalue correctly computed
|
|
goon = goon & (solved_nodes < p.options.cutsdp.maxiter-1);
|
|
goon = goon & ~(upper <=lower);
|
|
goon = goon && lower < upper;
|
|
if ~isinf(upper) && ~isinf(lower)
|
|
gap = abs((upper-lower)/(1e-3+abs(upper)+abs(lower)));
|
|
else
|
|
gap = inf;
|
|
end
|
|
goon = goon && gap >= p.options.cutsdp.gaptol;
|
|
goon = goon && (etime(clock,cutsdpsolvertime) < p.options.cutsdp.maxtime);
|
|
goon = goon && ~(output.problem == 16);
|
|
end
|
|
|
|
solved_nodes = solved_nodes + 1;
|
|
if eig_failure
|
|
infeasibility = nan;
|
|
end
|
|
|
|
phaseString = {'Continuous','Integer '};
|
|
integerInfeasibility = sum(abs(x(p.integer_variables)-round(x(p.integer_variables)))) + sum(abs(x(p.binary_variables)-round(x(p.binary_variables))));
|
|
if p.options.cutsdp.verbose
|
|
if mod(solved_nodes-1,p.options.print_interval)==0 || goon == 0
|
|
fprintf(' %4.0f : %s %11.3E %12.3E %14.3E %11.3E %3.0f %5.1f\n',solved_nodes,phaseString{currentPhase+1},sdpInfeasibility,integerInfeasibility,lower,upper,p_lp.K.l-p.K.l,etime(clock,cutsdpsolvertime));
|
|
end
|
|
end
|
|
end
|
|
% We perhaps terminated before the LP problems found a feasible solution,
|
|
% but the SDP pump found a solution along the way, so return that feasible
|
|
% solution.
|
|
if ~checkfeasiblefast(p_lp,x,-p.options.cutsdp.feastol)
|
|
if ~isempty(x_found_by_sdp_pump)
|
|
x = x_found_by_sdp_pump;
|
|
end
|
|
end
|
|
D_struc = [];
|
|
if output.problem == 16
|
|
interrupted = 1;
|
|
end
|
|
|
|
|
|
function [p_lp,worstinfeasibility,infeasible_sdp_cones,eig_computation_failure] = add_sdp_cut(p,p_lp,x,infeasibility_in,p_original);
|
|
|
|
worstinfeasibility = infeasibility_in;
|
|
eig_computation_failure = 0;
|
|
infeasible_sdp_cones = zeros(1,length(p.K.s));
|
|
if p.K.s(1)>0
|
|
% Solution found by MILP solver
|
|
xsave = x;
|
|
infeasibility = -1;
|
|
eig_computation_failure = 1;
|
|
for i = 1:1:length(p.K.s)
|
|
x = xsave;
|
|
iter = 1;
|
|
keep_projecting = 1;
|
|
infeasibility = 0;
|
|
psave = p_lp;
|
|
while iter <= p.options.cutsdp.maxprojections+1 & (infeasibility(end) < -p.options.cutsdp.feastol) && keep_projecting
|
|
% Add cuts b + a'*x >= 0 (if x infeasible)
|
|
[X,p_lp,infeasibility(iter),a,b,failure] = add_one_sdp_cut(p,p_lp,x,i,p_original);
|
|
|
|
eig_computation_failure = eig_computation_failure & failure;
|
|
if ~isempty(a) && infeasibility(iter) < p_lp.options.cutsdp.feastol && p.options.cutsdp.cutlimit > 0
|
|
% Project current point on the hyper-plane associated with
|
|
% the most negative eigenvalue and move towards the SDP
|
|
% feasible region, and then iterate a couple of iterations
|
|
% to generate a deeper cut
|
|
x0 = x;
|
|
x = x + a*(-b-a'*x)/(a'*a);
|
|
x(x <= p.lb) = p.lb(x <= p.lb);
|
|
x(x >= p.ub) = p.ub(x >= p.ub);
|
|
keep_projecting = norm(x-x0)>= p.options.cutsdp.projectionthreshold;
|
|
else
|
|
keep_projecting = 0;
|
|
end
|
|
worstinfeasibility = min(worstinfeasibility,infeasibility(iter));
|
|
iter = iter + 1;
|
|
end
|
|
infeasible_sdp_cones(i) = infeasibility(1) < p_lp.options.cutsdp.feastol;
|
|
end
|
|
else
|
|
worstinfeasibility = min(worstinfeasibility,0);
|
|
end
|
|
|
|
|
|
function [X,p_lp,infeasibility,asave,bsave,failure] = add_one_sdp_cut(p,p_lp,x,i,p_original);
|
|
|
|
newcuts = 0;
|
|
newF = [];
|
|
n = p.K.s(i);
|
|
if numel(x)/length(x) < .1
|
|
X = p.semidefinite{i}.F_struc*sparse([1;x]);
|
|
else
|
|
X = p.semidefinite{i}.F_struc*[1;x];
|
|
end
|
|
X = reshape(X,n,n);X = (X+X')/2;
|
|
asave = [];
|
|
bsave = [];
|
|
% First check if it happens to be psd. Then we are done. Quicker
|
|
% than computing all eigenvalues
|
|
% This also acts as a slight safe-guard in case the sparse eigs
|
|
% fails to prove that the smallest eigenvalue is non-negative
|
|
%[R,indefinite] = chol(X+eye(length(X))*1e-12);
|
|
%if indefinite
|
|
|
|
% User is trying to solve by only generating no-good cuts
|
|
permutation = [];
|
|
failure = 0;
|
|
if p.options.cutsdp.cutlimit == 0
|
|
[d,v] = eig(full(X));
|
|
infeasibility = v(1,1);
|
|
return
|
|
end
|
|
|
|
% For not too large problems, we simply go with a dense
|
|
% eigenvalue/vector computation
|
|
if n <= p_lp.options.cutsdp.switchtosparse
|
|
[d,v] = eig(full(X));
|
|
failure = 0;
|
|
else
|
|
% Try to perform a block-diagonalization of the current solution,
|
|
% and compute eigenvalue/vectorsa for each block.
|
|
% Sparse eigenvalues can easily fails so we catch info about this
|
|
[d,v,permutation,failure] = dmpermblockeig(X,p_lp.options.cutsdp.switchtosparse);
|
|
end
|
|
d(abs(d)<1e-12)=0;
|
|
infeasibility = min(diag(v));
|
|
if infeasibility<0
|
|
[ii,jj] = sort(diag(v));
|
|
|
|
if ~isempty(permutation)
|
|
[~,inversepermutation] = ismember(1:length(permutation),permutation);
|
|
end
|
|
|
|
for m = jj(1:min(length(jj),p.options.cutsdp.cutlimit))'
|
|
if v(m,m)<=-1e-12
|
|
try
|
|
if ~isempty(permutation)
|
|
dhere = d(inversepermutation,m);
|
|
else
|
|
dhere = d(:,m);
|
|
end
|
|
dd = dhere*dhere';dd = dd(:);
|
|
bA = dd'*p.semidefinite{i}.F_struc;
|
|
if numel(bA)/nnz(bA) < .1
|
|
bA = sparse(bA);
|
|
end
|
|
end
|
|
b = bA(:,1);
|
|
A = -bA(:,2:end);
|
|
if isempty(p_lp.F_struc) || ~any(sum(abs(p_lp.F_struc-[b -A]),2)<= 1e-12)
|
|
newF = real([newF;[b -A]]);
|
|
newcuts = newcuts + 1;
|
|
if isempty(asave)
|
|
A(abs(A)<1e-12)=0;
|
|
b(abs(b)<1e-12)=0;
|
|
asave = -A(:);
|
|
bsave = b;
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
newF(abs(newF)<1e-12) = 0;
|
|
keep=find(any(newF(:,2:end),2));
|
|
newF = newF(keep,:);
|
|
if size(newF,1)>0
|
|
newF(:,1) = newF(:,1) + 0*0.02*abs(p_lp.options.cutsdp.feastol);
|
|
p_lp.F_struc = [p_lp.F_struc(1:p_lp.K.f,:);p_lp.F_struc(1+p_lp.K.f:end,:);newF];
|
|
p_lp.K.l = p_lp.K.l + size(newF,1);
|
|
end
|
|
|
|
function [p_lp] = add_nogood_cut(p,p_lp,x,infeasibility)
|
|
% Add a nogood cut. Might already have been generated by
|
|
% the SDP cuts, but it doesn't hurt to add it
|
|
if length(x) == length(p.binary_variables)
|
|
% Standard binary case
|
|
[b,a] = exclusionCut(x,1);
|
|
p_lp.F_struc = [p_lp.F_struc(1:p_lp.K.f,:);p_lp.F_struc(1+p_lp.K.f:end,:);b a];
|
|
p_lp.K.l = p_lp.K.l + 1;
|
|
elseif all(p_lp.ub(p.integer_variables) <= 0) && all(p_lp.lb(p.integer_variables) >= -1) && length(p.binary_variables)==0
|
|
% Not all variables are integer, but we've analytically showed that
|
|
% this solution isn't possible anyway. Also, this is the negated binary
|
|
% case. TODO: Generalize
|
|
x(p_lp.noninteger_variables) = [];
|
|
[b,atemp] = exclusionCut(x,-1);
|
|
a = zeros(1,length(p.c));
|
|
a(p.integer_variables) = atemp;
|
|
p_lp.F_struc = [p_lp.F_struc(1:p_lp.K.f,:);p_lp.F_struc(1+p_lp.K.f:end,:);b a];
|
|
p_lp.K.l=p_lp.K.l+1;
|
|
end
|
|
|
|
|
|
function [p_lp,infeasibility,infeasible_socp_cones] = add_socp_cut(p,p_lp,x,infeasibility);
|
|
infeasible_socp_cones = zeros(1,length(p.K.q));
|
|
% Only add these cuts if solver doesn't support SOCP cones
|
|
if ~p.solver.lower.constraint.inequalities.secondordercone.linear
|
|
if p.K.q(1)>0
|
|
% Add cuts
|
|
top = p.K.f+p.K.l+1;
|
|
for i = 1:1:length(p.K.q)
|
|
n = p.K.q(i);
|
|
X = p.F_struc(top:top+n-1,:)*[1;x];
|
|
X = [X(1) X(2:end)';X(2:end) eye(n-1)*X(1)];
|
|
Y = randn(n,n);
|
|
newcuts = 1;
|
|
newF = zeros(n,size(p.F_struc,2));
|
|
[d,v] = eig(X);
|
|
infeasibility = min(infeasibility,min(diag(v)));
|
|
dummy=[];
|
|
newF = [];
|
|
if infeasibility<0
|
|
[ii,jj] = sort(diag(v));
|
|
for m = jj(1:min(length(jj),p.options.cutsdp.cutlimit))'%find(diag(v<0))%1:1%length(v)
|
|
if v(m,m)<0
|
|
v1 = d(1,m);v2 = d(2:end,m);
|
|
newF = [newF;p.F_struc(top,:) + 2*v1*v2'*p.F_struc(top+1:top+n-1,:)];
|
|
newcuts = newcuts + 1;
|
|
end
|
|
end
|
|
end
|
|
newF(abs(newF)<1e-12) = 0;
|
|
keep= any(newF(:,2:end),2);
|
|
newF = newF(keep,:);
|
|
if size(newF,1)>0
|
|
p_lp.F_struc = [p_lp.F_struc;newF];
|
|
p_lp.K.l = p_lp.K.l + size(newF,1);
|
|
[i,j] = sort(p_lp.F_struc*[1;x]);
|
|
end
|
|
top = top+n;
|
|
end
|
|
end
|
|
end
|
|
|
|
function p_lp = addActivationCuts(p,p_lp)
|
|
if p.options.cutsdp.activationcut && p.K.s(1) > 0 && length(p.binary_variables) == length(p.c)
|
|
top = p.K.f + p.K.l+sum(p.K.q)+1;
|
|
for k = 1:length(p.K.s)
|
|
F0 = p.F_struc(top:top+p.K.s(k)^2-1,1);
|
|
% Fij = p.F_struc(top:top+p.K.s(k)^2-1,2:end);
|
|
% Fij = sum(Fij | Fij,2);
|
|
F0 = reshape(F0,p.K.s(k),p.K.s(k));
|
|
% Fij = reshape(Fij,p.K.s(k),p.K.s(k));
|
|
% Fall = F0 | Fij;
|
|
row = 1;
|
|
added = 0; % Avoid adding more than 2*n cuts (we hope for sparse model...)
|
|
while row <= p.K.s(k)-1 && added <= 2*p.K.s(k)
|
|
% if 1
|
|
j = find(F0(row,:));
|
|
if min(eig(F0(j,j)))<0
|
|
[ii,jj] = find(F0(j,j));
|
|
ii = j(ii);
|
|
jj = j(jj);
|
|
index = sub2ind([p.K.s(k),p.K.s(k)],ii,jj);
|
|
p.F_struc(top + index-1,2:end);
|
|
S = p.F_struc(top + index-1,2:end);
|
|
S = S | S;S = sum(S,1);S = S | S;
|
|
% Some of these have to be different from 0
|
|
p_lp.F_struc = [p_lp.F_struc;-1 S];
|
|
p_lp.K.l = p_lp.K.l + 1;
|
|
|
|
end
|
|
% else
|
|
j = find(F0(row,:));
|
|
j = j(j>row);
|
|
for col = j(:)'
|
|
if F0(row,row)*F0(col,col)-F0(row,col)^2<0
|
|
index = sub2ind([p.K.s(k),p.K.s(k)],[row row col],[row col col]);
|
|
p.F_struc(top + index-1,2:end);
|
|
S = p.F_struc(top + index-1,2:end);
|
|
S = S | S;S = sum(S,1);S = S | S;
|
|
% Some of these have to be different from 0
|
|
p_lp.F_struc = [p_lp.F_struc;-1 S];
|
|
added = added + 1;
|
|
p_lp.K.l = p_lp.K.l + 1;
|
|
end
|
|
end
|
|
row = row + 1;
|
|
end
|
|
end
|
|
end
|
|
|
|
function p_lp = addDiagonalCuts(p,p_lp)
|
|
if p.K.s(1)>0
|
|
top = p.K.f+p.K.l+sum(p.K.q)+1;
|
|
for i = 1:length(p.K.s)
|
|
n = p.K.s(i);
|
|
newF=[];
|
|
nouse = [];
|
|
for m = 1:p.K.s(i)
|
|
d = eyev(p.K.s(i),m);
|
|
index = (1+(m-1)*(p.K.s(i)+1));
|
|
ab = p.F_struc(top+index-1,:);
|
|
b = ab(1);
|
|
a = -ab(2:end);
|
|
% a*x <= b
|
|
pos = find(a>0);
|
|
neg = find(a<0);
|
|
if a(pos)*p.ub(pos) + a(neg)*p.lb(neg)>b
|
|
if length(p.binary_variables) == length(p.c)
|
|
if all(p.F_struc(top+index-1,2:end) == fix(p.F_struc(top+index-1,2:end)))
|
|
ab(1) = floor(ab(1));
|
|
if max(a)<=0 % Exclusive or in disguise
|
|
ab = sign(ab);
|
|
end
|
|
end
|
|
end
|
|
newF = [newF;ab];
|
|
else
|
|
nouse = [nouse m];
|
|
end
|
|
end
|
|
% Clean
|
|
newF(abs(newF)<1e-12) = 0;
|
|
keep=find(any(newF(:,2:end),2));
|
|
newF = newF(keep,:);
|
|
|
|
p_lp.F_struc = [p_lp.F_struc;newF];
|
|
p_lp.K.l = p_lp.K.l + size(newF,1);
|
|
top = top+n^2;
|
|
end
|
|
end
|
|
|
|
function p_lp = addSOCPCut(p,p_lp)
|
|
if p.K.q(1) > 0
|
|
top = p.K.f+p.K.l+1;
|
|
for i = 1:length(p.K.q)
|
|
n = p.K.q(i);
|
|
newF = p.F_struc(top,:);
|
|
|
|
% Clean
|
|
newF(abs(newF)<1e-12) = 0;
|
|
keep=find(any(newF(:,2:end),2));
|
|
newF = newF(keep,:);
|
|
|
|
p_lp.F_struc = [p_lp.F_struc;newF];
|
|
p_lp.K.l = p_lp.K.l + size(newF,1);
|
|
top = top+n;
|
|
end
|
|
end
|
|
|
|
function p_lp = nodeTight(p,p_lp);
|
|
if p.options.cutsdp.nodetight
|
|
% Extract LP part Ax<=b
|
|
A = -p_lp.F_struc(p_lp.K.f + (1:p_lp.K.l),2:end);
|
|
b = p_lp.F_struc(p_lp.K.f + (1:p_lp.K.l),1);
|
|
c = p_lp.c;
|
|
% Tighten bounds and find redundant constraints
|
|
[p_lp.lb,p_lp.ub,redundant,pss] = milppresolve(A,b,p_lp.lb,p_lp.ub,p.integer_variables,p.binary_variables,ones(length(p.lb),1));
|
|
A(redundant,:) = [];
|
|
b(redundant) = [];
|
|
p_lp.F_struc(p_lp.K.f+redundant,:) = [];
|
|
p_lp.K.l = p_lp.K.l-length(redundant);
|
|
end
|
|
|
|
function p_lp = nodeFix(p,p_lp);
|
|
if p.options.cutsdp.nodefix
|
|
% Try to find variables to fix w.l.o.g
|
|
[fix_up,fix_down] = presolve_fixvariables(A,b,c,p_lp.lb,p_lp.ub,sdpmonotinicity);
|
|
p_lp.lb(fix_up) = p_lp.ub(fix_up);
|
|
p_lp.ub(fix_down) = p_lp.lb(fix_down);
|
|
while ~(isempty(fix_up) & isempty(fix_down))
|
|
[p_lp.lb,p_lp.ub,redundant,pss] = milppresolve(A,b,p_lp.lb,p_lp.ub,p.integer_variables,p.binary_variables,ones(length(p.lb),1));
|
|
A(redundant,:) = [];
|
|
b(redundant) = [];
|
|
p_lp.F_struc(p_lp.K.f+redundant,:) = [];
|
|
p_lp.K.l = p_lp.K.l-length(redundant);
|
|
fix_up = [];
|
|
fix_down = [];
|
|
% Try to find variables to fix w.l.o.g
|
|
[fix_up,fix_down] = presolve_fixvariables(A,b,c,p_lp.lb,p_lp.ub,sdpmonotinicity);
|
|
p_lp.lb(fix_up) = p_lp.ub(fix_up);
|
|
p_lp.ub(fix_down) = p_lp.lb(fix_down);
|
|
end
|
|
end
|
|
|
|
function p_lp = removeRedundant(p_lp);
|
|
|
|
F = unique(p_lp.F_struc(1+p_lp.K.f:end,:),'rows');
|
|
if size(F,1) < p_lp.K.l
|
|
p_lp.F_struc = [p_lp.F_struc(1:p_lp.K.f,:);F];
|
|
p_lp.K.l = size(F,1);
|
|
end
|
|
|
|
|
|
function plotP(p)
|
|
|
|
b = p.F_struc(1+p.K.f:p.K.f+p.K.l,1);
|
|
A = -p.F_struc(1+p.K.f:p.K.f+p.K.l,2:end);
|
|
x = sdpvar(size(A,2),1);
|
|
plot([A*x <= b, p.lb <= x <= p.ub],x(1:2),'b',[],sdpsettings('plot.shade',.2));
|
|
|
|
|
|
function [p_lp,activity,prevRem] = handleCutPool(p_lp,activity,prevRem,output,integerPhase,activitylimit)
|
|
if length(activity) < p_lp.K.l + p_lp.K.f
|
|
activity(p_lp.K.l+p_lp.K.f) = 0;
|
|
end
|
|
if ~integerPhase
|
|
% Save info about active cuts
|
|
% (for how long have they been consequitively active)
|
|
Inactive_Here = (p_lp.F_struc*[1;output.Primal] >= 1e-3);
|
|
activity(find(Inactive_Here)) = activity(find(Inactive_Here)) + 1;
|
|
activity(find(~Inactive_Here)) = 0;
|
|
% Prune inactive cuts that have been inactive for many iterations
|
|
remove = find(activity(:)>activitylimit);
|
|
prevRem = [prevRem;p_lp.F_struc(remove,:)];
|
|
p_lp.F_struc(remove,:) = [];
|
|
p_lp.K.l = p_lp.K.l - length(remove);
|
|
activity(remove)=[];
|
|
end
|
|
if ~isempty(prevRem)
|
|
% We're keeping a pool of removed cuts, and if they turn active
|
|
% again, add them and define them as very likely to be kept
|
|
Violated_Here = find((prevRem*[1;output.Primal] <= 0));
|
|
if ~isempty(Violated_Here)
|
|
p_lp = addLinearCut(p_lp,prevRem(Violated_Here,:));
|
|
% p_lp.F_struc = [p_lp.F_struc;prevRem(Violated_Here,:)];
|
|
% p_lp.K.l = p_lp.K.l + length(Violated_Here);
|
|
activity = [activity(:);ones(length(Violated_Here),1)*-20];
|
|
prevRem(Violated_Here,:)=[];
|
|
end
|
|
end
|
|
|
|
function p = addLinearCut(p,row);
|
|
p.F_struc = [p.F_struc;row];
|
|
p.K.l = p.K.l + size(row,1);
|
|
|
|
|
|
function p = adjustSolverPrecision(p)
|
|
|
|
switch p.options.cutsdp.solver
|
|
case 'cplex'
|
|
try
|
|
% Case that user specified, catch error if empty
|
|
p.options.cplex.simplex.tolerances.feasibility = min([abs(p.options.cutsdp.feastol/10) p.options.cplex.simplex.tolerances.feasibility]);
|
|
catch
|
|
% Options has been removed, i.e. user has not specified?
|
|
try
|
|
p.options.cplex.simplex.tolerances.feasibility = min([abs(p.options.cutsdp.feastol/10) p.options.default.cplex.simplex.tolerances.feasibility]);
|
|
catch
|
|
% User is sitting on obsolete cplex/matlab
|
|
disp('WARNING: You should update CPLEX. THe options structure does not work as expected');
|
|
end
|
|
end
|
|
|
|
otherwise
|
|
end
|
|
|
|
|
|
function p = detect3x3SymmetryGroups(p)
|
|
|
|
good = zeros(1,length(p.c));
|
|
good(p.integer_variables) = 1;
|
|
good( (p.lb ~= -1) | (p.ub ~=0)) = 0;
|
|
if any(good)
|
|
groups = {};
|
|
for j = 1:length(p.K.s)
|
|
n = 3;
|
|
X = spalloc(p.K.s(j),p.K.s(j),p.K.s(j)^2);
|
|
X(1:n,1:n) = 1;
|
|
index0 = find(X);
|
|
index = index0;
|
|
corner = 0;
|
|
for block = 1:p.K.s(j)-n
|
|
dataBlock = p.semidefinite{j}.F_struc(index,:);
|
|
used = find(any(dataBlock,1));
|
|
dataBlock = dataBlock(:,used);
|
|
if used(1) == 0;dataBlock = [zeros(size(dataBlock,1),1) dataBlock];end
|
|
v = used;v = v(v>1)-1;
|
|
if all(good(v))
|
|
if isempty(groups)
|
|
groups{1}.dataBlock = dataBlock;
|
|
groups{1}.variables{1} = used;
|
|
else
|
|
found = 0;
|
|
for k = 1:length(groups)
|
|
if isequal(length(used),length(groups{1}.variables{1}))
|
|
if isequal(groups{1}.dataBlock,dataBlock)
|
|
found = 1;
|
|
groups{1}.variables{end+1} = used;
|
|
else
|
|
% s = groups{1}.dataBlock(:)\dataBlock(:);
|
|
% norm(s*groups{1}.dataBlock-dataBlock,inf)
|
|
end
|
|
end
|
|
end
|
|
if ~found
|
|
groups{end+1}.dataBlock = dataBlock;
|
|
groups{end}.variables{1} = used;
|
|
end
|
|
end
|
|
end
|
|
index = index + p.K.s(j)+1;
|
|
end
|
|
end
|
|
for i = 1:length(groups)
|
|
if length(groups{i}.variables) > 1
|
|
keep(i) = 1;
|
|
else
|
|
keep(i) = 0;
|
|
end
|
|
end
|
|
groups = {groups{find(keep)}};
|
|
if length(groups) > 0
|
|
for i = 1:length(groups)
|
|
for j = 1:length(groups{i}.variables);
|
|
v = groups{i}.variables{j};
|
|
v = v(v>1)-1;
|
|
groups{i}.variables{j} = v;
|
|
end
|
|
end
|
|
end
|
|
else
|
|
groups = {};
|
|
end
|
|
p.sdpsymmetry = groups;
|
|
|
|
|
|
function p_lp = add3x3sdpsymmetrycut(p,p_lp,x)
|
|
|
|
for j = 1:length(p.sdpsymmetry)
|
|
excludes = [];
|
|
n = sqrt(size(p.sdpsymmetry{j}.dataBlock,1));
|
|
for i = 1:length(p.sdpsymmetry{j}.variables)
|
|
if min(eig(reshape(p.sdpsymmetry{j}.dataBlock*[1;x(p.sdpsymmetry{j}.variables{i})],n,n))) < -abs(p_lp.options.cutsdp.feastol)
|
|
excludes = [excludes x(p.sdpsymmetry{j}.variables{i})];
|
|
end
|
|
end
|
|
if ~isempty(excludes)
|
|
newF = [];
|
|
infeasible_combinations = unique(excludes','rows')';
|
|
for k = 1:size(infeasible_combinations,2)
|
|
% Local cut for reduced set
|
|
[b,atemp] = exclusionCut(infeasible_combinations(:,k),-1);
|
|
% Add that cut for every variable groups
|
|
for s = 1:length(p.sdpsymmetry{j}.variables)
|
|
a = spalloc(1,length(p_lp.c),1);
|
|
a(p.sdpsymmetry{j}.variables{s}) = atemp;
|
|
if all(sum(abs(p_lp.F_struc - [b a]),2)<=1e-12)
|
|
newF = [newF;b a];
|
|
end
|
|
end
|
|
end
|
|
p_lp = addLinearCut(p_lp,newF);
|
|
end
|
|
end
|
|
|
|
|
|
function [p,p_lp] = addSymmetryCuts(p,p_lp)
|
|
|
|
for j = 1:length(p.sdpsymmetry)
|
|
if length(p.sdpsymmetry{j}.variables{1}) <= 4
|
|
% We can enumerate easily infeasibles
|
|
excludes = [];
|
|
n = sqrt(size(p.sdpsymmetry{j}.dataBlock,1));
|
|
combs = -dec2decbin(0:2^length(p.sdpsymmetry{j}.variables{1})-1,length(p.sdpsymmetry{j}.variables{1}))';
|
|
for i = 1:size(combs,2)
|
|
if min(eig(reshape(p.sdpsymmetry{j}.dataBlock*[1;combs(:,i)],n,n))) < -abs(p_lp.options.cutsdp.feastol)
|
|
excludes = [excludes combs(:,i)];
|
|
end
|
|
end
|
|
if ~isempty(excludes)
|
|
newF = [];
|
|
infeasible_combinations = unique(excludes','rows')';
|
|
for k = 1:size(infeasible_combinations,2)
|
|
% Local cut for reduced set
|
|
[b,atemp] = exclusionCut(infeasible_combinations(:,k),-1);
|
|
% Add that cut for every variable groups
|
|
for s = 1:length(p.sdpsymmetry{j}.variables)
|
|
a = spalloc(1,length(p_lp.c),1);
|
|
a(p.sdpsymmetry{j}.variables{s}) = atemp;
|
|
newF = [newF;b a];
|
|
end
|
|
end
|
|
p_lp = addLinearCut(p_lp,newF);
|
|
% Delete, won't need these in the future
|
|
p.sdpsymmetry{j}.dataBlock = [];
|
|
p.sdpsymmetry{j}.variables = [];
|
|
end
|
|
end
|
|
end
|