Hi everybody,
I am optimizing an integer (binary) linear model with proc optmilp. After many iterations, the solver runs out of memory. The process was moving in the right direction started to plateau with a large gap (>300%) so I need to find ways to optimize this further.
Here is what I have done so far:
This is not a complex or uncommon model (the heart of it is a simple assignment problem) so probably many ways exist to improve upon this. I thought that I would share this with the SAS optimization community as people with more experience in this particular area may have lots to suggest. I want to solve this with SAS… that would be my goal.
I created a “toy problem” with simulated test data (to protect the confidential details) but the structure of the model is essentially the same. I am posting 3 SAS programs with their respective logs:
My goal would be to make scenario2 work. That would probably be enough. But if there is a way to solve for something of the magnitude of scenario 3, that would be even better.
Here are the main questions in my mind:
Thanks to anybody that takes a look at this. I would appreciate any comments or feedback.
Carlos
Yes, Benders decomposition applies even when the LP subproblem has only one block. In fact, the original paper by Benders in 1962 treated only that case. But exploiting block structure can certainly boost performance.
For completeness, here is one implementation of Benders decomposition for the capacitated version of your problem. To avoid the complications of dynamically generated feasibility cuts, I made VALID_MAPS the fully dense cross product of ORDERS and LOCATIONS. That change and the introduction of the SufficientCapacity constraint together imply that the subproblem will always be feasible. With max_orders_per_location = 5 and an optimality gap of 5%, scenario 2 solves quickly on my laptop.
%let relobjgap = 0.05;
proc optmodel printlevel=0;
/* Benders decomposition parameters */
num infinity = constant('BIG');
num best_lower_bound init 0; /* some finite lower bound on the original objective */
num best_upper_bound init infinity;
num gap = (if best_lower_bound ne 0 and best_upper_bound < infinity
then (best_upper_bound - best_lower_bound) / abs(best_lower_bound)
else .);
/* declare MILP master problem over complicating variables */
*Define data elements;
put 'optmodel: Define data elements';
set <num> ORDERS;
set <num> LOCATIONS;
num distance {ORDERS, LOCATIONS} init &inf_dist.;
*Define variable 1;
var On {LOCATIONS} binary init 0;
*Read datasets 1;
put 'optmodel: Read datasets 1';
read data work.orders into ORDERS=[ORDER_ID];
read data work.points into LOCATIONS=[LOCATION_ID] On=on;
read data work.distances into [ORDER_ID LOCATION_ID] distance=&distance_var.;
*Define variable 2;
/* set VALID_MAPS = {p in ORDERS, w in LOCATIONS: distance[p,w] <= (&inf_dist.-10)};*/
set VALID_MAPS = ORDERS cross LOCATIONS; /* make dense to avoid feasibility cuts */
/* var Map {VALID_MAPS} binary init 0;*/
var Map {VALID_MAPS} >= 0; /* relax to get correct duals */
*Read datasets 2;
put 'optmodel: Read datasets 2';
read data work.distances into [ORDER_ID LOCATION_ID] Map=map;
*Calculations;
put 'optmodel: Calculations';
impvar total_LOCATIONs_on = sum{w in LOCATIONS} On[w];
impvar total_maps_on = sum{<p,w> in VALID_MAPS} Map[p,w];
impvar total_distance = sum{<p,w> in VALID_MAPS} Map[p,w]*distance[p,w];
impvar total_penalty = total_LOCATIONs_on * &LOCATION_distance_penalty.;
*Mechanical constraints;
put 'optmodel: Mechanical constraints';
con ORDERs_mapped_correctly {p in ORDERS}:
/* (sum{<(p),w> in VALID_MAPS} Map[p,w]) = 1;*/
sum{w in LOCATIONS} Map[p,w] = 1;
set SUPPORT init {};
* con LOCATIONs_mapped_correctly {w in LOCATIONS}:
(sum{<p,(w)> in VALID_MAPS} Map[p,w]) <= (w in SUPPORT) * &max_ORDERs_per_LOCATION.;
con LOCATIONs_mapped_correctly {<p,w> in VALID_MAPS}:
-Map[p,w] >= -(w in SUPPORT);
num capacity {w in LOCATIONS} = min(&max_ORDERs_per_LOCATION., card(ORDERS));
con CapacityCon {w in LOCATIONS}:
-sum{p in ORDERS} Map[p,w] >= -(w in SUPPORT) * capacity[w];
con SufficientCapacity:
sum {w in LOCATIONS} capacity[w] * On[w] >= card(ORDERS);
*Business constraints;
put 'optmodel: Business constraints';
con max_LOCATIONs_on:
total_LOCATIONs_on <= &final_max_LOCATIONs_on.;
*Minimize distance;
put 'optmodel: Set Goals';
min goal = total_distance + total_penalty;
*Initial solution check (seed);
print "VALUES BEFORE SOLVING" total_LOCATIONs_on total_maps_on goal total_distance total_penalty;
/* master objective */
var Eta >= 0;
num Eta_sol;
min MasterObjective = total_penalty + Eta;
/* optimality cuts */
num num_optcuts init 0;
set OPTCUTS = 1..num_optcuts;
num cut_constant {OPTCUTS};
num cut_coefficient {LOCATIONS, OPTCUTS} init 0;
con OptimalityCut {optcut in OPTCUTS}:
Eta >= cut_constant[optcut] + sum {w in LOCATIONS} cut_coefficient[w,optcut] * On[w];
/* omit feasibility cuts (assume SufficientCapacity implies feasibility of subproblem) */
/* additional valid cuts */
/* smallest positive distance from p */
num spd {p in ORDERS} = min {w in LOCATIONS diff {p}} distance[p,w];
/* if On[p] = 0 then Eta[p] >= spd[p] */
* con OnOrPositiveDistance {p in ORDERS}:
Eta[p] >= spd[p] * (1 - On[p]);
con OnOrPositiveDistance:
Eta >= sum {p in ORDERS} spd[p] * (1 - On[p]);
problem Master include
On Eta
MasterObjective
SufficientCapacity
max_LOCATIONs_on
OptimalityCut OnOrPositiveDistance;
/* declare subproblem over Map here (same as original but with On fixed) */
min SubproblemObjective = sum{<p,w> in VALID_MAPS} Map[p,w]*distance[p,w];
problem Subproblem include
Map
SubproblemObjective
ORDERs_mapped_correctly LOCATIONs_mapped_correctly CapacityCon;
num lower_bound;
num upper_bound = SubproblemObjective.sol + &LOCATION_distance_penalty. * card(SUPPORT);
set INCUMBENT init {};
num Map_sol {VALID_MAPS};
/* main loop */
do until (gap ne . and gap <= &relobjgap);
put 'Solving Master...';
use problem Master;
put num_optcuts=;
for {w in LOCATIONS} On[w] = (w in INCUMBENT);
Eta = max(Eta.lb,
sum {p in ORDERS} spd[p] * (1 - On[p]),
max {optcut in OPTCUTS}
(cut_constant[optcut] + sum {w in LOCATIONS} cut_coefficient[w,optcut] * On[w])
);
solve with MILP / primalin logfreq=10000 target=(best_lower_bound) relobjgap=&relobjgap;
if _solution_status_ = 'INFEASIBLE' then do;
put 'ERROR: master is infeasible';
stop;
end;
Eta_sol = Eta.sol;
lower_bound = _OROPTMODEL_NUM_['BEST_BOUND'];
best_lower_bound = max(best_lower_bound, lower_bound);
SUPPORT = {w in LOCATIONS: On[w].sol > 0.5};
put SUPPORT=;
put 'Solving Subproblem...';
use problem Subproblem;
solve with LP / algorithm=ip crossover=0 logfreq=0;
if _solution_status_ = 'INFEASIBLE' then do;
put 'ERROR: subproblem is infeasible';
stop;
end;
else do;
if best_upper_bound > upper_bound then do;
best_upper_bound = upper_bound;
INCUMBENT = SUPPORT;
for {<p,w> in VALID_MAPS} Map_sol[p,w] = Map[p,w].sol;
create data incumbent from [LOCATION_ID]=INCUMBENT;
end;
if lower_bound < upper_bound then do;
if Eta_sol < SubproblemObjective then do;
/* generate new optimality cut */
num_optcuts = num_optcuts + 1;
cut_constant[num_optcuts] = sum {p in ORDERS} ORDERs_mapped_correctly[p].dual;
for {w in LOCATIONS}
cut_coefficient[w,num_optcuts] =
- sum{p in ORDERS} LOCATIONs_mapped_correctly[p,w].dual
- capacity[w] * CapacityCon[w].dual;
end;
end;
end;
put "BOUNDS: " best_lower_bound= best_upper_bound= gap=;
end;
put INCUMBENT=;
/* recover optimal solution */
for {w in LOCATIONS} On[w] = (w in INCUMBENT);
for {<p,w> in VALID_MAPS} Map[p,w] = Map_sol[p,w];
quit;
I quickly looked at your model (but I haven't run it): I think there's no problem with it.
Probably you already have looked at these:
But these examples do the same as your code.
If you remove total_penalty from your model, does it run faster? (Of course, if it is part of the problem, then this is just a test.)
I would try to set the priority of on variabels higher then the map variables. Maybe that helps...
Also if you set the branching direction to "Round up to nearest integer", you probably will get faster a feasible solution.
160078 binary variables are a lot. Maybe reducing the number of links (VALID_MAPS set size) even more? I guess you already tried that.
@ gergely_batho: thanks for looking at the model. Yes, I had seen those articles and had used the tip shown in the second one in order to save memory. That's a great tip, indeed. Like you said, my problem is very similar to that model, just a bit larger.
If I remove the penalty and let the "MAX_LOCATIONS_ON" constraint kick-in, then the gap decreases (which is good) but still does not converge and the problem still runs out of memory. Reducing the number of links futher would be quivalent to reducing the area that a given location may cover, so I would prefer to not to attack this problem from that angle.
Thanks for the tip on priority and branching direction. I have added these changes to my model (after defining all the variables) and will now run it again:
*Set priority and branching direction (1=Round up to nearest integer);
for {w in LOCATIONS} on[w].priority = 5;
for {w in LOCATIONS} on[w].direction = 1;
for {<p,w> in VALID_MAPS} map[p,w].direction = 1;
If any other thoughts or suggestions, please advice!
I will take a look, but one thing I noticed right away is that you are not passing the initial solution to the solver. The most straightforward way to do this is with the PRIMALIN option in the SOLVE WITH MILP statement. Because you are instead calling PROC OPTMILP via a SUBMIT block, you should first save the solution to a data set:
create data primalin(drop=j) from [j] _VAR_=_VAR_.label _VALUE_=_VAR_.sol;
And then add PRIMALIN=primalin to the PROC OPTMILP call.
Rob, thanks for pointing out the initialization issue. Indeed, I was loading the initial solution to proc optmodel but was not passing that information along to proc optmilp. Thanks for the code snippet. I have implemented all the suggested changes in my models (see attached script) and will rerun them now. Will share the results when the run is over. I am not sure if the 2 inputs datasets (MPS & PRIMALIN) are passing everything including priority and branching direction. Perhaps we need to pass that explicitly too. If any other comment or suggestion, please let me know. Very much appreciated!
Yes, the branching priorities and directions are included in the BRANCH section of the MPS data set.
A few other things to try, alone or in combination:
1. Relax the map variables to be >= 0 instead of binary.
2. Relax the ORDERs_mapped_correctly constraints to be >= 1 instead of = 1.
3. You have omitted the link constraints that appear in the doc examples, presumably to save memory. They are not required but do strengthen the formulation. To recover some of the strength without bloating the problem size, include these constraints instead:
con Cover {p in ORDERS}:
sum{<(p),w> in VALID_MAPS} On[w] >= 1;
4. Use the OPTMILP option tuner.
I have spent some time on this and can solve scenario 2 (optimal objective value is 347,566) in a few minutes by exploiting the problem structure. Explicitly, I relaxed the Map variables to be >= 0 instead of binary and then applied classical Benders decomposition, with a MILP master problem and an LP subproblem. The motivation is that the problem is easy once the On variables are fixed. See also this SAS Global Forum paper for a simplified approach that does not use the .DUAL suffix or exploit the independent blocks in the subproblem.
%let relobjgap = 1e-4;
proc optmodel printlevel=0;
/* Benders decomposition parameters */
num infinity = constant('BIG');
num best_lower_bound init 0; /* some finite lower bound on the original objective */
num best_upper_bound init infinity;
num gap = (if best_lower_bound ne 0 and best_upper_bound < infinity
then (best_upper_bound - best_lower_bound) / abs(best_lower_bound)
else .);
/* declare MILP master problem over complicating variables */
*Define data elements;
put 'optmodel: Define data elements';
set <num> ORDERS;
set <num> LOCATIONS;
num distance {ORDERS, LOCATIONS} init &inf_dist.;
*Define variable 1;
var On {LOCATIONS} binary init 0;
*Read datasets 1;
put 'optmodel: Read datasets 1';
read data work.orders into ORDERS=[ORDER_ID];
read data work.points into LOCATIONS=[LOCATION_ID] On=on;
read data work.distances into [ORDER_ID LOCATION_ID] distance=&distance_var.;
*Define variable 2;
set VALID_MAPS = {p in ORDERS, w in LOCATIONS: distance[p,w] <= (&inf_dist.-10)};
/* var Map {VALID_MAPS} binary init 0;*/
var Map {VALID_MAPS} >= 0; /* relax to get correct duals */
*Read datasets 2;
put 'optmodel: Read datasets 2';
read data work.distances into [ORDER_ID LOCATION_ID] Map=map;
*Calculations;
put 'optmodel: Calculations';
impvar total_LOCATIONs_on = sum{w in LOCATIONS} On[w];
impvar total_maps_on = sum{<p,w> in VALID_MAPS} Map[p,w];
impvar total_distance = sum{<p,w> in VALID_MAPS} Map[p,w]*distance[p,w];
impvar total_penalty = total_LOCATIONs_on * &LOCATION_distance_penalty.;
set LOCATIONS_p {ORDERS} init {};
for {<p,w> in VALID_MAPS} LOCATIONS_p[p] = LOCATIONS_p[p] union {w};
*Mechanical constraints;
put 'optmodel: Mechanical constraints';
con ORDERs_mapped_correctly {p in ORDERS}:
/* (sum{<(p),w> in VALID_MAPS} Map[p,w]) = 1;*/
sum{w in LOCATIONS_p[p]} Map[p,w] = 1;
con Cover {p in ORDERS}:
/* sum{<(p),w> in VALID_MAPS} On[w] >= 1;*/
sum{w in LOCATIONS_p[p]} On[w] >= 1;
set SUPPORT init {};
* con LOCATIONs_mapped_correctly {w in LOCATIONS}:
(sum{<p,(w)> in VALID_MAPS} Map[p,w]) <= (w in SUPPORT) * &max_ORDERs_per_LOCATION.;
con LOCATIONs_mapped_correctly {<p,w> in VALID_MAPS}:
-Map[p,w] >= -(w in SUPPORT);
*Business constraints;
put 'optmodel: Business constraints';
con max_LOCATIONs_on:
total_LOCATIONs_on <= &final_max_LOCATIONs_on.;
*Minimize distance;
put 'optmodel: Set Goals';
min goal = total_distance + total_penalty;
*Initial solution check (seed);
print "VALUES BEFORE SOLVING" total_LOCATIONs_on total_maps_on goal total_distance total_penalty;
/* master objective */
var Eta {ORDERS} >= 0;
num Eta_sol {ORDERS};
min MasterObjective = total_penalty + sum {p in ORDERS} Eta[p];
/* optimality cuts */
num num_optcuts {ORDERS} init 0;
set OPTCUTS {p in ORDERS} = 1..num_optcuts[p];
num cut_constant {p in ORDERS, OPTCUTS[p]};
num cut_coefficient {p in ORDERS, w in LOCATIONS_p[p], OPTCUTS[p]} init 0;
con OptimalityCut {p in ORDERS, optcut in OPTCUTS[p]}:
Eta[p] >= cut_constant[p,optcut] + sum {w in LOCATIONS_p[p]} cut_coefficient[p,w,optcut] * On[w];
/* omit feasibility cuts (assume Cover implies feasibility of subproblem) */
problem Master include
On Eta
MasterObjective
Cover
max_LOCATIONs_on
OptimalityCut;
/* declare subproblem over Map here (same as original but with On fixed) */
impvar SubproblemObjectiveBlock {p in ORDERS} = sum{w in LOCATIONS_p[p]} Map[p,w]*distance[p,w];
min SubproblemObjective = sum {p in ORDERS} SubproblemObjectiveBlock[p];
problem Subproblem include
Map
SubproblemObjective
ORDERs_mapped_correctly LOCATIONs_mapped_correctly;
num lower_bound;
num upper_bound = SubproblemObjective.sol + &LOCATION_distance_penalty. * card(SUPPORT);
set INCUMBENT init {};
num Map_sol {VALID_MAPS};
/* main loop */
do until (gap ne . and gap <= &relobjgap);
put 'Solving Master...';
use problem Master;
/* put num_optcuts[*];*/
put (sum {p in ORDERS} num_optcuts[p])=;
for {w in LOCATIONS} On[w] = (w in INCUMBENT);
for {p in ORDERS} Eta[p] = max(Eta[p].lb,
max {optcut in OPTCUTS[p]}
(cut_constant[p,optcut] + sum {w in LOCATIONS_p[p]} cut_coefficient[p,w,optcut] * On[w])
);
solve with MILP / primalin logfreq=10000 target=(best_lower_bound) relobjgap=&relobjgap;
if _solution_status_ = 'INFEASIBLE' then do;
put 'ERROR: master is infeasible';
stop;
end;
for {p in ORDERS} Eta_sol[p] = Eta[p].sol;
lower_bound = _OROPTMODEL_NUM_['BEST_BOUND'];
best_lower_bound = max(best_lower_bound, lower_bound);
SUPPORT = {w in LOCATIONS: On[w].sol > 0.5};
put SUPPORT=;
put 'Solving Subproblem...';
use problem Subproblem;
solve with LP / algorithm=ip crossover=0 logfreq=0;
if _solution_status_ = 'INFEASIBLE' then do;
put 'ERROR: subproblem is infeasible';
stop;
end;
else do;
if best_upper_bound > upper_bound then do;
best_upper_bound = upper_bound;
INCUMBENT = SUPPORT;
for {<p,w> in VALID_MAPS} Map_sol[p,w] = Map[p,w].sol;
create data incumbent from [LOCATION_ID]=INCUMBENT;
end;
if lower_bound < upper_bound then do;
for {p in ORDERS: Eta_sol[p] < SubproblemObjectiveBlock[p]} do;
/* generate new optimality cut */
num_optcuts[p] = num_optcuts[p] + 1;
cut_constant[p,num_optcuts[p]] = ORDERs_mapped_correctly[p].dual;
for {w in LOCATIONS_p[p]}
cut_coefficient[p,w,num_optcuts[p]] = -LOCATIONs_mapped_correctly[p,w].dual;
end;
end;
end;
put "BOUNDS: " best_lower_bound= best_upper_bound= gap=;
end;
put INCUMBENT=;
/* recover optimal solution */
for {w in LOCATIONS} On[w] = (w in INCUMBENT);
for {<p,w> in VALID_MAPS} Map[p,w] = Map_sol[p,w];
quit;
Did you try this out? The following valid cuts seem to help by strengthening the master problem:
/* additional valid cuts */
/* smallest positive distance from p */
num spd {p in ORDERS} = min {w in LOCATIONS_p[p] diff {p}} distance[p,w];
/* if On[p] = 0 then Eta[p] >= spd[p] */
con OnOrPositiveDistance {p in ORDERS}:
Eta[p] >= spd[p] * (1 - On[p]);
problem Master include
On Eta
MasterObjective
Cover
max_LOCATIONs_on
OptimalityCut OnOrPositiveDistance;
For scenario 3, the optimal objective value is between 142.8M and 144.8M.
Rob,
Thank you again for this suggestion. I had to read a bunch to become familiar with the benders technique, but I get it know. Thank you sir!
If I wanted to put back the original constraint that limits how many orders can be served by each location:
/*Removing this: */
con LOCATIONs_mapped_correctly {<p,w> in VALID_MAPS}: -Map[p,w] >= -(w in ON_SUPPORT);
/*Going back to this: */
con LOCATIONs_mapped_correctly {w in LOCATIONS}:
sum{<p,(w)> in VALID_MAPS} map[p,w] <= (w in SUPPORT) * &max_ORDERs_per_LOCATION.;
What else would have to change in the model? I can see at least two changes on the optimality cuts. One, remove the negative sign from the dual value. Two, change how the "cut_coefficient" term is indexed (change from [p,w] to [w] so to speak) since the original constraint is indexed by locations and not by pairs. This secong change is what I am not sure how to implement, because it seems to break the block structure (which is based on orders.) Any thoughts?
/* In this part of the code: */
for {p in ORDERS: Eta_sol[p] < SubproblemObjectiveBlock[p]} do;
num_optcuts[p] = num_optcuts[p] + 1;
cut_constant[p,num_optcuts[p]] = ORDERs_mapped_correctly[p].dual;
/* replacing this code... */
for {w in VALID_MAP_LOCATIONS[p]}
cut_coefficient[p,w,num_optcuts[p]] = -LOCATIONs_mapped_correctly[p,w].dual
end;
/* replacing that with something of the form... */
for {w in LOCATIONS}
new_coefficient[w, new_num_optcuts[w]] = LOCATIONs_mapped_correctly[w].dual
/* In the part of the code where optimality constraints are defined, we would need something along these lines */
num new_num_optcuts {LOCATIONS} init 0;
set new_OPTCUTS {w in LOCATIONS} = 1..new_num_optcuts[w];
num new_coefficient {w in LOCATIONS, new_OPTCUTS[w]};
/* But then the structure of the optimality cut would have to change... this is where I am stuck... */
con OptimalityCut {p in ORDERS, optcut in OPTCUTS[p]}:
Eta[p] >= cut_constant[p,optcut] + sum {w in VALID_MAP_LOCATIONS[p]} cut_coefficient[p,w,optcut] * On[w]
+ sum {w in LOCATIONS} new_coefficient;
Looks like we would have to sacrifice the block structure in order to add that constraint in it's original form. You may see an easier way to handle this. Please let me know if you do.
Thank you sir. Hope you have a Happy New Year!
Yes, the capacity constraint destroys the block structure, and the corresponding optimality cuts are weaker. You would need a single Eta variable instead of Eta[p] for each p. And you would also need master Benders feasibility cuts because the subproblem would not necessarily be feasible without them. What values of max_ORDERs_per_LOCATION are you interested in for the three scenarios?
Yes, Benders decomposition applies even when the LP subproblem has only one block. In fact, the original paper by Benders in 1962 treated only that case. But exploiting block structure can certainly boost performance.
For completeness, here is one implementation of Benders decomposition for the capacitated version of your problem. To avoid the complications of dynamically generated feasibility cuts, I made VALID_MAPS the fully dense cross product of ORDERS and LOCATIONS. That change and the introduction of the SufficientCapacity constraint together imply that the subproblem will always be feasible. With max_orders_per_location = 5 and an optimality gap of 5%, scenario 2 solves quickly on my laptop.
%let relobjgap = 0.05;
proc optmodel printlevel=0;
/* Benders decomposition parameters */
num infinity = constant('BIG');
num best_lower_bound init 0; /* some finite lower bound on the original objective */
num best_upper_bound init infinity;
num gap = (if best_lower_bound ne 0 and best_upper_bound < infinity
then (best_upper_bound - best_lower_bound) / abs(best_lower_bound)
else .);
/* declare MILP master problem over complicating variables */
*Define data elements;
put 'optmodel: Define data elements';
set <num> ORDERS;
set <num> LOCATIONS;
num distance {ORDERS, LOCATIONS} init &inf_dist.;
*Define variable 1;
var On {LOCATIONS} binary init 0;
*Read datasets 1;
put 'optmodel: Read datasets 1';
read data work.orders into ORDERS=[ORDER_ID];
read data work.points into LOCATIONS=[LOCATION_ID] On=on;
read data work.distances into [ORDER_ID LOCATION_ID] distance=&distance_var.;
*Define variable 2;
/* set VALID_MAPS = {p in ORDERS, w in LOCATIONS: distance[p,w] <= (&inf_dist.-10)};*/
set VALID_MAPS = ORDERS cross LOCATIONS; /* make dense to avoid feasibility cuts */
/* var Map {VALID_MAPS} binary init 0;*/
var Map {VALID_MAPS} >= 0; /* relax to get correct duals */
*Read datasets 2;
put 'optmodel: Read datasets 2';
read data work.distances into [ORDER_ID LOCATION_ID] Map=map;
*Calculations;
put 'optmodel: Calculations';
impvar total_LOCATIONs_on = sum{w in LOCATIONS} On[w];
impvar total_maps_on = sum{<p,w> in VALID_MAPS} Map[p,w];
impvar total_distance = sum{<p,w> in VALID_MAPS} Map[p,w]*distance[p,w];
impvar total_penalty = total_LOCATIONs_on * &LOCATION_distance_penalty.;
*Mechanical constraints;
put 'optmodel: Mechanical constraints';
con ORDERs_mapped_correctly {p in ORDERS}:
/* (sum{<(p),w> in VALID_MAPS} Map[p,w]) = 1;*/
sum{w in LOCATIONS} Map[p,w] = 1;
set SUPPORT init {};
* con LOCATIONs_mapped_correctly {w in LOCATIONS}:
(sum{<p,(w)> in VALID_MAPS} Map[p,w]) <= (w in SUPPORT) * &max_ORDERs_per_LOCATION.;
con LOCATIONs_mapped_correctly {<p,w> in VALID_MAPS}:
-Map[p,w] >= -(w in SUPPORT);
num capacity {w in LOCATIONS} = min(&max_ORDERs_per_LOCATION., card(ORDERS));
con CapacityCon {w in LOCATIONS}:
-sum{p in ORDERS} Map[p,w] >= -(w in SUPPORT) * capacity[w];
con SufficientCapacity:
sum {w in LOCATIONS} capacity[w] * On[w] >= card(ORDERS);
*Business constraints;
put 'optmodel: Business constraints';
con max_LOCATIONs_on:
total_LOCATIONs_on <= &final_max_LOCATIONs_on.;
*Minimize distance;
put 'optmodel: Set Goals';
min goal = total_distance + total_penalty;
*Initial solution check (seed);
print "VALUES BEFORE SOLVING" total_LOCATIONs_on total_maps_on goal total_distance total_penalty;
/* master objective */
var Eta >= 0;
num Eta_sol;
min MasterObjective = total_penalty + Eta;
/* optimality cuts */
num num_optcuts init 0;
set OPTCUTS = 1..num_optcuts;
num cut_constant {OPTCUTS};
num cut_coefficient {LOCATIONS, OPTCUTS} init 0;
con OptimalityCut {optcut in OPTCUTS}:
Eta >= cut_constant[optcut] + sum {w in LOCATIONS} cut_coefficient[w,optcut] * On[w];
/* omit feasibility cuts (assume SufficientCapacity implies feasibility of subproblem) */
/* additional valid cuts */
/* smallest positive distance from p */
num spd {p in ORDERS} = min {w in LOCATIONS diff {p}} distance[p,w];
/* if On[p] = 0 then Eta[p] >= spd[p] */
* con OnOrPositiveDistance {p in ORDERS}:
Eta[p] >= spd[p] * (1 - On[p]);
con OnOrPositiveDistance:
Eta >= sum {p in ORDERS} spd[p] * (1 - On[p]);
problem Master include
On Eta
MasterObjective
SufficientCapacity
max_LOCATIONs_on
OptimalityCut OnOrPositiveDistance;
/* declare subproblem over Map here (same as original but with On fixed) */
min SubproblemObjective = sum{<p,w> in VALID_MAPS} Map[p,w]*distance[p,w];
problem Subproblem include
Map
SubproblemObjective
ORDERs_mapped_correctly LOCATIONs_mapped_correctly CapacityCon;
num lower_bound;
num upper_bound = SubproblemObjective.sol + &LOCATION_distance_penalty. * card(SUPPORT);
set INCUMBENT init {};
num Map_sol {VALID_MAPS};
/* main loop */
do until (gap ne . and gap <= &relobjgap);
put 'Solving Master...';
use problem Master;
put num_optcuts=;
for {w in LOCATIONS} On[w] = (w in INCUMBENT);
Eta = max(Eta.lb,
sum {p in ORDERS} spd[p] * (1 - On[p]),
max {optcut in OPTCUTS}
(cut_constant[optcut] + sum {w in LOCATIONS} cut_coefficient[w,optcut] * On[w])
);
solve with MILP / primalin logfreq=10000 target=(best_lower_bound) relobjgap=&relobjgap;
if _solution_status_ = 'INFEASIBLE' then do;
put 'ERROR: master is infeasible';
stop;
end;
Eta_sol = Eta.sol;
lower_bound = _OROPTMODEL_NUM_['BEST_BOUND'];
best_lower_bound = max(best_lower_bound, lower_bound);
SUPPORT = {w in LOCATIONS: On[w].sol > 0.5};
put SUPPORT=;
put 'Solving Subproblem...';
use problem Subproblem;
solve with LP / algorithm=ip crossover=0 logfreq=0;
if _solution_status_ = 'INFEASIBLE' then do;
put 'ERROR: subproblem is infeasible';
stop;
end;
else do;
if best_upper_bound > upper_bound then do;
best_upper_bound = upper_bound;
INCUMBENT = SUPPORT;
for {<p,w> in VALID_MAPS} Map_sol[p,w] = Map[p,w].sol;
create data incumbent from [LOCATION_ID]=INCUMBENT;
end;
if lower_bound < upper_bound then do;
if Eta_sol < SubproblemObjective then do;
/* generate new optimality cut */
num_optcuts = num_optcuts + 1;
cut_constant[num_optcuts] = sum {p in ORDERS} ORDERs_mapped_correctly[p].dual;
for {w in LOCATIONS}
cut_coefficient[w,num_optcuts] =
- sum{p in ORDERS} LOCATIONs_mapped_correctly[p,w].dual
- capacity[w] * CapacityCon[w].dual;
end;
end;
end;
put "BOUNDS: " best_lower_bound= best_upper_bound= gap=;
end;
put INCUMBENT=;
/* recover optimal solution */
for {w in LOCATIONS} On[w] = (w in INCUMBENT);
for {<p,w> in VALID_MAPS} Map[p,w] = Map_sol[p,w];
quit;
SAS Innovate 2025 is scheduled for May 6-9 in Orlando, FL. Sign up to be first to learn about the agenda and registration!
Learn how to run multiple linear regression models with and without interactions, presented by SAS user Alex Chaplin.
Find more tutorials on the SAS Users YouTube channel.