The Mixed Integer Linear Programming Solver

Example 14.3 Facility Location

Consider the classic facility location problem. Given a set L of customer locations and a set F of candidate facility sites, you must decide on which sites to build facilities and assign coverage of customer demand to these sites so as to minimize cost. All customer demand d Subscript i must be satisfied, and each facility has a demand capacity limit C. The total cost is the sum of the distances c Subscript i j between facility j and its assigned customer i, plus a fixed charge f Subscript j for building a facility at site j. Let y Subscript j Baseline equals 1 represent choosing site j to build a facility, and 0 otherwise. Also, let x Subscript i j Baseline equals 1 represent the assignment of customer i to facility j, and 0 otherwise. This model can be formulated as the following integer linear program:

StartLayout 1st Row 1st Column min 2nd Column sigma-summation Underscript i element-of upper L Endscripts sigma-summation Underscript j element-of upper F Endscripts c Subscript i j Baseline x Subscript i j plus sigma-summation Underscript j element-of upper F Endscripts f Subscript j Baseline y Subscript j 2nd Row 1st Column normal s period normal t period 2nd Column sigma-summation Underscript j element-of upper F Endscripts x Subscript i j 3rd Column equals 4th Column 1 5th Column for-all i element-of upper L 6th Column left-parenthesis normal a normal s normal s normal i normal g normal n normal bar normal d normal e normal f right-parenthesis 3rd Row 1st Column Blank 2nd Column x Subscript i j 3rd Column less-than-or-equal-to 4th Column y Subscript j 5th Column for-all i element-of upper L comma j element-of upper F 6th Column left-parenthesis normal l normal i normal n normal k right-parenthesis 4th Row 1st Column Blank 2nd Column sigma-summation Underscript i element-of upper L Endscripts d Subscript i Baseline x Subscript i j 3rd Column less-than-or-equal-to 4th Column upper C y Subscript j 5th Column for-all j element-of upper F 6th Column left-parenthesis normal c normal a normal p normal a normal c normal i normal t normal y right-parenthesis 5th Row 1st Column Blank 2nd Column x Subscript i j Baseline element-of StartSet 0 comma 1 EndSet 3rd Column Blank 4th Column Blank 5th Column for-all i element-of upper L comma j element-of upper F 6th Row 1st Column Blank 2nd Column y Subscript j Baseline element-of StartSet 0 comma 1 EndSet 3rd Column Blank 4th Column Blank 5th Column for-all j element-of upper F EndLayout

Constraint (assign_def) ensures that each customer is assigned to exactly one site. Constraint (link) forces a facility to be built if any customer has been assigned to that facility. Finally, constraint (capacity) enforces the capacity limit at each site.

Consider also a variation of this same problem where there is no cost for building a facility. This problem is typically easier to solve than the original problem. For this variant, let the objective be

StartLayout 1st Row 1st Column min 2nd Column sigma-summation Underscript i element-of upper L Endscripts sigma-summation Underscript j element-of upper F Endscripts c Subscript i j Baseline x Subscript i j EndLayout

First, construct a random instance of this problem by using the following DATA steps:

title 'Facility Location Problem';

%let NumCustomers  = 30;
%let NumSites      = 6;
%let SiteCapacity  = 35;
%let MaxDemand     = 10;
%let xmax          = 200;
%let ymax          = 100;
%let seed          = 546;

/* generate random customer locations */
data cdata(drop=i);
   call streaminit(&seed);
   length name $8;
   do i = 1 to &NumCustomers;
      name = compress('C'||put(i,best.));
      x = rand('UNIFORM') * &xmax;
      y = rand('UNIFORM') * &ymax;
      demand = rand('UNIFORM') * &MaxDemand;
      output;
   end;
run;

/* generate random site locations and fixed charge */
data sdata(drop=i);
   call streaminit(&seed);
   length name $8;
   do i = 1 to &NumSites;
      name = compress('SITE'||put(i,best.));
      x = rand('UNIFORM') * &xmax;
      y = rand('UNIFORM') * &ymax;
      fixed_charge = 30 * (abs(&xmax/2-x) + abs(&ymax/2-y));
      output;
   end;
run;

The following PROC OPTMODEL statements first generate and solve the model that contains the no-fixed-charge variant of the cost function. Next, they solve the fixed-charge model and demonstrate the usage of the MAXPOOLSOLS=, SOLTYPE=, and MAXPOOLGAP= options for outputting multiple solutions. Note that the solution to the model that has no fixed charge is feasible for the fixed-charge model and should provide a good starting point for the MILP solver. Use the PRIMALIN option to provide an incumbent solution (warm start).


proc optmodel;
   set <str> CUSTOMERS;
   set <str> SITES init {};
   /* x and y coordinates of CUSTOMERS and SITES */
   num x {CUSTOMERS union SITES};
   num y {CUSTOMERS union SITES};
   num demand {CUSTOMERS};
   num fixed_charge {SITES};
   /* distance from customer i to site j */
   num dist {i in CUSTOMERS, j in SITES}
       = sqrt((x[i] - x[j])^2 + (y[i] - y[j])^2);
   read data cdata into CUSTOMERS=[name] x y demand;
   read data sdata into SITES=[name] x y fixed_charge;
   var Assign {CUSTOMERS, SITES} binary;
   var Build {SITES} binary;
   min CostNoFixedCharge
       = sum {i in CUSTOMERS, j in SITES} dist[i,j] * Assign[i,j];
   min CostFixedCharge
       = CostNoFixedCharge + sum {j in SITES} fixed_charge[j] * Build[j];
   /* each customer assigned to exactly one site */
   con assign_def {i in CUSTOMERS}:
      sum {j in SITES} Assign[i,j] = 1;
   /* if customer i assigned to site j, then facility must be built at j */
   con link {i in CUSTOMERS, j in SITES}:
      Assign[i,j] <= Build[j];
   /* each site can handle at most &SiteCapacity demand */
   con capacity {j in SITES}:
      sum {i in CUSTOMERS} demand[i] * Assign[i,j] <=
         &SiteCapacity * Build[j];
   /* solve the MILP with no fixed charges */
   solve obj CostNoFixedCharge with milp;
   /* clean up the solution */
   for {i in CUSTOMERS, j in SITES} Assign[i,j] = round(Assign[i,j]);
   for {j in SITES} Build[j] = round(Build[j]);
   call symput('varcostNo',put(CostNoFixedCharge,6.1));
   /* create a data set for use by PROC SGPLOT */
   create data CostNoFixedCharge_Data from
     [customer site]={i in CUSTOMERS, j in SITES: Assign[i,j] = 1}
      x1=x[i] y1=y[i] x2=x[j] y2=y[j]
      function='line' drawspace='datavalue' linethickness=1 linecolor='black';
   submit;
      data csdata;
         set cdata(rename=(y=cy)) sdata(rename=(y=sy));
      run;
      title1 "Facility Location Problem";
      title2 "TotalCost = &varcostNo (Variable = &varcostNo, Fixed = 0)";
      proc sgplot data=csdata sganno=CostNoFixedCharge_Data noautolegend;
         scatter x=x y=cy / datalabel=name datalabelattrs=(size=6pt)
            markerattrs=(symbol=circlefilled color=black size=6pt);
         scatter x=x y=sy / datalabel=name datalabelattrs=(size=6pt)
            markerattrs=(symbol=diamond color=blue size=6pt);
         xaxis display=(nolabel);
         yaxis display=(nolabel);
      run;
   endsubmit;
   /* solve the MILP with fixed charges with warm start */
   solve obj CostFixedCharge with milp / primalin maxpoolsols=6 soltype=best
                                         maxpoolgap=1E-3;
   num varcost = sum {i in CUSTOMERS, j in SITES} dist[i,j] * Assign[i,j].sol;
   num fixcost = sum {j in SITES} fixed_charge[j] * Build[j].sol;
   for {s in 1.._NSOL_} do;
      /* clean up the solution */
      for {i in CUSTOMERS, j in SITES} Assign[i,j] = round(Assign[i,j].sol[s]);
      for {j in SITES} Build[j] = round(Build[j].sol[s]);
      call symput('varcost', put(varcost,6.1));
      call symput('fixcost', put(fixcost,5.1));
      call symput('totalcost', put(CostFixedCharge,6.1));
      /* create a data set for use by PROC SGPLOT */
      create data CostFixedCharge_Data from
         [customer site]={i in CUSTOMERS, j in SITES: Assign[i,j] = 1}
         x1=x[i] y1=y[i] x2=x[j] y2=y[j]
         function='line' drawspace='datavalue' linethickness=1 linecolor='black';
      submit s;
         title1 "Facility Location Problem: Solution &s";
         title2 "TotalCost = &totalcost (Variable = &varcost, Fixed = &fixcost)";
         proc sgplot data=csdata sganno=CostFixedCharge_Data noautolegend;
            scatter x=x y=cy / datalabel=name datalabelattrs=(size=6pt)
               markerattrs=(symbol=circlefilled color=black size=6pt);
            scatter x=x y=sy / datalabel=name datalabelattrs=(size=6pt)
               markerattrs=(symbol=diamond color=blue size=6pt);
            xaxis display=(nolabel);
            yaxis display=(nolabel);
         run;
      endsubmit;
   end;
quit;

Output 14.3.1 displays the information that is printed in the log for the facility location problem.

Output 14.3.1: OPTMODEL Log for Facility Location

NOTE: Problem generation will use 16 threads.                                   
NOTE: The problem has 186 variables (0 free, 0 fixed).                          
NOTE: The problem has 186 binary and 0 integer variables.                       
NOTE: The problem has 216 linear constraints (186 LE, 30 EQ, 0 GE, 0 range).    
NOTE: The problem has 726 linear constraint coefficients.                       
NOTE: The problem has 0 nonlinear constraints (0 LE, 0 EQ, 0 GE, 0 range).      
NOTE: The initial MILP heuristics are applied.                                  
NOTE: The MILP presolver value AUTOMATIC is applied.                            
NOTE: The MILP presolver removed 6 variables and 180 constraints.               
NOTE: The MILP presolver removed 366 constraint coefficients.                   
NOTE: The MILP presolver modified 0 constraint coefficients.                    
NOTE: The presolved problem has 180 variables, 36 constraints, and 360          
      constraint coefficients.                                                  
NOTE: The MILP solver is called.                                                
NOTE: The parallel Branch and Cut algorithm is used.                            
NOTE: The Branch and Cut algorithm is using up to 16 threads.                   
          Node   Active   Sols    BestInteger      BestBound      Gap    Time   
             0        1      1   1052.9414513    985.5035182    6.84%       0   
NOTE: The MILP presolver is applied again.                                      
             0        1      1   1052.9414513    985.5035182    6.84%       0   
             0        1      2   1045.3217618    985.5035182    6.07%       0   
             0        1      2   1045.3217618   1002.3380109    4.29%       0   
             0        1      5   1025.8272473   1002.3380109    2.34%       0   
             0        1      5   1025.8272473   1013.0691328    1.26%       0   
NOTE: The MILP presolver is applied again.                                      
             0        1      6   1021.5193163   1013.0691328    0.83%       0   
             0        1      6   1021.5193163   1013.0691328    0.83%       0   
             0        1      6   1021.5193163   1014.9738001    0.64%       0   
NOTE: The MILP presolver is applied again.                                      
             0        1      7   1021.5193163   1014.9738001    0.64%       0   
NOTE: The MILP solver added 3 cuts with 17 cut coefficients at the root.        
NOTE: Optimal.                                                                  
NOTE: Objective = 1021.5193163.                                                 
NOTE: Problem generation will use 16 threads.                                   
NOTE: The problem has 186 variables (0 free, 0 fixed).                          
NOTE: The problem uses 1 implicit variables.                                    
NOTE: The problem has 186 binary and 0 integer variables.                       
NOTE: The problem has 216 linear constraints (186 LE, 30 EQ, 0 GE, 0 range).    
NOTE: The problem has 726 linear constraint coefficients.                       
NOTE: The problem has 0 nonlinear constraints (0 LE, 0 EQ, 0 GE, 0 range).      
NOTE: The initial MILP heuristics are applied.                                  
NOTE: The MILP presolver value AUTOMATIC is applied.                            
NOTE: The MILP presolver removed 0 variables and 0 constraints.                 
NOTE: The MILP presolver removed 0 constraint coefficients.                     
NOTE: The MILP presolver modified 0 constraint coefficients.                    
NOTE: The presolved problem has 186 variables, 216 constraints, and 726         
      constraint coefficients.                                                  
NOTE: The MILP solver is called.                                                
NOTE: The parallel Branch and Cut algorithm is used.                            
NOTE: The Branch and Cut algorithm is using up to 16 threads.                   
          Node   Active   Sols    BestInteger      BestBound      Gap    Time   
             0        1      2   9113.7292152   6814.6940297   33.74%       0   
             0        1      2   9113.7292152   9063.8415787    0.55%       0   
             0        1      2   9113.7292152   9073.8049605    0.44%       0   
             0        1      5   9082.3070802   9073.8049605    0.09%       0   
             0        1      5   9082.3070802   9075.7685482    0.07%       0   
             0        1      5   9082.3070802   9077.8284517    0.05%       0   
             0        1      5   9082.3070802   9078.5883699    0.04%       0   
             0        1      5   9082.3070802   9079.7006060    0.03%       0   
NOTE: The MILP solver added 7 cuts with 103 cut coefficients at the root.       
             6        0      7   9082.3070802   9082.3070802    0.00%       0   
NOTE: Processing nodes for multiple solutions.                                  
             7        5      8   9082.3070802   9082.3070802    0.00%       0   
            10        5      9   9082.3070802   9082.3070802    0.00%       0   
            11        5     10   9082.3070802   9082.3070802    0.00%       0   
            12        5     11   9082.3070802   9082.3070802    0.00%       0   
            13        4     12   9082.3070802   9082.3070802    0.00%       0   
            17        4     13   9082.3070802   9082.3070802    0.00%       0   
            24        4     14   9082.3070802   9082.3070802    0.00%       0   
            29        6     15   9082.3070802   9082.3070802    0.00%       0   
            31        6     16   9082.3070802   9082.3070802    0.00%       0   
            53        0     16   9082.3070802   9082.3070802    0.00%       0   
NOTE: Optimal.                                                                  
NOTE: Objective = 9082.3070802.                                                 


The output of the program is shown in Output 14.3.2.

Output 14.3.2: Solution Plots for Facility Location

Solution Plots for Facility Location
External File:images/milpexfacloc4plot1.png
External File:images/milpexfacloc4plot2.png
External File:images/milpexfacloc4plot3.png
External File:images/milpexfacloc4plot4.png
External File:images/milpexfacloc4plot5.png
External File:images/milpexfacloc4plot6.png


The economic trade-off for the fixed-charge model forces you to build fewer sites and push more demand to each site. The six different solutions are the best possible solutions that are within a 0.001 gap of the optimal solution.

It is possible to expedite the solution of the fixed-charge facility location problem by choosing appropriate branching priorities for the decision variables. Recall that for each site j, the value of the variable y Subscript j determines whether a facility is built on that site. Suppose you decide to branch on the variables y Subscript j before the variables x Subscript i j. You can set a higher branching priority for y Subscript j by using the .priority suffix for the Build variables in PROC OPTMODEL, as follows:

   for{j in SITES} Build[j].priority=10;

Setting higher branching priorities for certain variables is not guaranteed to speed up the MILP solver, but it can be helpful in some instances. The following program creates and solves an instance of the facility location problem, giving higher priority to the variables y Subscript j. The LOGFREQ= option is used to limit the size of the node log.


%let NumCustomers  = 45;
%let NumSites      = 8;
%let SiteCapacity  = 35;
%let MaxDemand     = 10;
%let xmax          = 200;
%let ymax          = 100;
%let seed          = 2345;

/* generate random customer locations */
data cdata(drop=i);
   length name $8;
   do i = 1 to &NumCustomers;
      name = compress('C'||put(i,best.));
      x = rand('UNIFORM') * &xmax;
      y = rand('UNIFORM') * &ymax;
      demand = rand('UNIFORM') * &MaxDemand;
      output;
   end;
run;

/* generate random site locations and fixed charge */
data sdata(drop=i);
length name $8;
   do i = 1 to &NumSites;
      name = compress('SITE'||put(i,best.));
      x = rand('UNIFORM') * &xmax;
      y = rand('UNIFORM') * &ymax;
      fixed_charge = (abs(&xmax/2-x) + abs(&ymax/2-y)) / 2;
      output;
   end;
run;
proc optmodel;
   set <str> CUSTOMERS;
   set <str> SITES init {};

   /* x and y coordinates of CUSTOMERS and SITES */
   num x {CUSTOMERS union SITES};
   num y {CUSTOMERS union SITES};
   num demand {CUSTOMERS};
   num fixed_charge {SITES};

   /* distance from customer i to site j */
   num dist {i in CUSTOMERS, j in SITES}
       = sqrt((x[i] - x[j])^2 + (y[i] - y[j])^2);

   read data cdata into CUSTOMERS=[name] x y demand;
   read data sdata into SITES=[name] x y fixed_charge;

   var Assign {CUSTOMERS, SITES} binary;
   var Build {SITES} binary;

   min CostFixedCharge
       = sum {i in CUSTOMERS, j in SITES} dist[i,j] * Assign[i,j]
         + sum {j in SITES} fixed_charge[j] * Build[j];

   /* each customer assigned to exactly one site */
   con assign_def {i in CUSTOMERS}:
      sum {j in SITES} Assign[i,j] = 1;

   /* if customer i assigned to site j, then facility must be built at j */
   con link {i in CUSTOMERS, j in SITES}:
      Assign[i,j] <= Build[j];

   /* each site can handle at most &SiteCapacity demand */
   con capacity {j in SITES}:
      sum {i in CUSTOMERS} demand[i] * Assign[i,j] <= &SiteCapacity * Build[j];

   /* assign priority to Build variables (y) */
   for{j in SITES} Build[j].priority=10;

   /* solve the MILP with fixed charges, using branching priorities */
   solve obj CostFixedCharge with milp / logfreq=1000;
quit;

The resulting output is shown in Output 14.3.3.

Output 14.3.3: PROC OPTMODEL Log for Facility Location with Branching Priorities

NOTE: There were 45 observations read from the data set WORK.CDATA.             
NOTE: There were 8 observations read from the data set WORK.SDATA.              
NOTE: Problem generation will use 16 threads.                                   
NOTE: The problem has 368 variables (0 free, 0 fixed).                          
NOTE: The problem has 368 binary and 0 integer variables.                       
NOTE: The problem has 413 linear constraints (368 LE, 45 EQ, 0 GE, 0 range).    
NOTE: The problem has 1448 linear constraint coefficients.                      
NOTE: The problem has 0 nonlinear constraints (0 LE, 0 EQ, 0 GE, 0 range).      
NOTE: The initial MILP heuristics are applied.                                  
NOTE: The MILP presolver value AUTOMATIC is applied.                            
NOTE: The MILP presolver removed 0 variables and 0 constraints.                 
NOTE: The MILP presolver removed 0 constraint coefficients.                     
NOTE: The MILP presolver modified 0 constraint coefficients.                    
NOTE: The presolved problem has 368 variables, 413 constraints, and 1448        
      constraint coefficients.                                                  
NOTE: The MILP solver is called.                                                
NOTE: The parallel Branch and Cut algorithm is used.                            
NOTE: The Branch and Cut algorithm is using up to 16 threads.                   
          Node   Active   Sols    BestInteger      BestBound      Gap    Time   
             0        1      3   1760.2751189   1677.3196849    4.95%       0   
NOTE: The MILP presolver is applied again.                                      
             0        1      3   1760.2751189   1677.3196849    4.95%       0   
             0        1      3   1760.2751189   1682.1823495    4.64%       0   
             0        1      4   1696.2311955   1682.1823495    0.84%       0   
             0        1      4   1696.2311955   1689.4445102    0.40%       0   
NOTE: The MILP presolver is applied again.                                      
             0        1      5   1696.2311955   1689.4445102    0.40%       0   
             0        1      5   1696.2311955   1689.4445102    0.40%       0   
             0        1      5   1696.2311955   1689.4445102    0.40%       0   
             0        1      6   1690.4317599   1690.4317599    0.00%       0   
NOTE: The MILP solver added 5 cuts with 30 cut coefficients at the root.        
NOTE: Optimal.                                                                  
NOTE: Objective = 1690.4317599.                                                 


Last updated: June 22, 2026