The OPTMILP Procedure

Example 7.3 Facility Location

This advanced example demonstrates how to warm start PROC OPTMILP by using the PRIMALIN= option. The model is constructed in PROC OPTMODEL and saved in an MPS-format data table for use in PROC OPTMILP. This problem can also be solved from within PROC OPTMODEL; see Chapter 14 for details.

Consider the classical 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 must be satisfied, and each facility has a demand capacity limit C. The total cost is the sum of the distances between facility j and its assigned customer i, plus a fixed charge for building a facility at site j. Let represent choosing site j to build a facility, and 0 otherwise. Also, let represent the assignment of customer i to facility j, and 0 otherwise. This model can be formulated as the following integer linear program:

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

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

%let NumCustomers  = 50;
%let NumSites      = 10;
%let SiteCapacity  = 35;
%let MaxDemand     = 10;
%let xmax          = 200;
%let ymax          = 100;
%let seed          = 423;

/* generate random customer locations */
data mycas.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 mycas.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 generate the model and define both variants of the cost function:

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 mycas.cdata into CUSTOMERS=[name] x y demand;
   read data mycas.sdata into SITES=[name] x y fixed_charge;
   var Assign {CUSTOMERS, SITES} binary;
   var Build {SITES} binary;
   /* 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];
   min CostNoFixedCharge
      = sum {i in CUSTOMERS, j in SITES} dist[i,j] * Assign[i,j];
   save mps mycas.nofcdata;
   min CostFixedCharge
      = CostNoFixedCharge
        + sum {j in SITES} fixed_charge[j] * Build[j];
   save mps mycas.fcdata;
quit;

First solve the problem for the model with no fixed charge by using the following statements. The first PROC SQL call populates the macro variables varcostNo. This macro variable displays the objective value when the results are plotted. The second PROC SQL call generates a data table that is used to plot the results. The information printed in the log by PROC OPTMILP is displayed in Output 7.3.1.

proc optmilp data=mycas.nofcdata primalout=mycas.nofcout;
run;
proc sql noprint;
   select put(sum(_objcoef_ * _value_),6.1) into :varcostNo
   from mycas.nofcout;
quit;
proc sql;
   create table CostNoFixedCharge_Data as
   select
      scan(p._var_,2,'[],') as customer,
      scan(p._var_,3,'[],') as site,
      c.x as x1, c.y as y1, s.x as x2, s.y as y2
   from
      mycas.cdata as c,
      mycas.sdata as s,
      mycas.nofcout(where=(substr(_var_,1,6)='Assign' and
         round(_value_) = 1)) as p
   where calculated customer = c.name and calculated site = s.name;
quit;

Output 7.3.1: PROC OPTMILP Log for Facility Location with No Fixed Charges

NOTE: The problem nofcdata has 510 variables (510 binary, 0 integer, 0 free, 0  
      fixed).                                                                   
NOTE: The problem has 560 constraints (510 LE, 50 EQ, 0 GE, 0 range).           
NOTE: The problem has 2010 constraint coefficients.                             
NOTE: The initial MILP heuristics are applied.                                  
NOTE: The MILP presolver value AUTOMATIC is applied.                            
NOTE: The MILP presolver removed 10 variables and 500 constraints.              
NOTE: The MILP presolver removed 1010 constraint coefficients.                  
NOTE: The MILP presolver modified 0 constraint coefficients.                    
NOTE: The presolved problem has 500 variables, 60 constraints, and 1000         
      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 32 threads.                   
          Node   Active   Sols    BestInteger      BestBound      Gap    Time   
             0        1      2   1267.2110591   1177.1539196    7.65%       0   
NOTE: The MILP presolver is applied again.                                      
             0        1      4   1249.9063818   1177.1539196    6.18%       0   
             0        1      4   1249.9063818   1190.0972621    5.03%       0   
             0        1      5   1192.6273240   1190.0972621    0.21%       0   
NOTE: The MILP solver added 13 cuts with 305 cut coefficients at the root.      
NOTE: Optimal within relative gap.                                              
NOTE: Objective = 1192.627324.                                                  
NOTE: The Cloud Analytic Services server processed the request in 0.152469      
      seconds.                                                                  
NOTE: The data set MYCAS.NOFCOUT has 510 observations and 9 variables.          


Next, solve the fixed-charge model by using the following statements. Note that the solution to the model with no fixed charge is feasible for the fixed-charge model and should provide a good starting point for PROC OPTMILP. The PRIMALIN= option provides an incumbent solution ("warm start"). The two PROC SQL calls perform the same functions as in the case with no fixed charges. The results from this approach are shown in Output 7.3.2.

proc optmilp data=mycas.fcdata primalin=mycas.nofcout primalout=mycas.fcout;
run;
proc sql noprint;
   select put(sum(_objcoef_ * _value_), 6.1) into :varcost
   from mycas.fcout(where=(substr(_var_,1,6)='Assign'));
   select put(sum(_objcoef_ * _value_), 5.1) into :fixcost
   from mycas.fcout(where=(substr(_var_,1,5)='Build'));
   select put(sum(_objcoef_ * _value_), 6.1) into :totalcost
   from mycas.fcout;
quit;
proc sql;
   create table CostFixedCharge_Data as
   select
      scan(p._var_,2,'[],') as customer,
      scan(p._var_,3,'[],') as site,
      c.x as x1, c.y as y1, s.x as x2, s.y as y2
   from
      mycas.cdata as c,
      mycas.sdata as s,
      mycas.fcout(where=(substr(_var_,1,6)='Assign' and
         round(_value_) = 1)) as p
   where calculated customer = c.name and calculated site = s.name;
quit;

Output 7.3.2: PROC OPTMILP Log for Facility Location with Fixed Charges, Using Warm Start

NOTE: The problem fcdata has 510 variables (510 binary, 0 integer, 0 free, 0    
      fixed).                                                                   
NOTE: The problem has 560 constraints (510 LE, 50 EQ, 0 GE, 0 range).           
NOTE: The problem has 2010 constraint coefficients.                             
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 510 variables, 560 constraints, and 2010        
      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 32 threads.                   
          Node   Active   Sols    BestInteger      BestBound      Gap    Time   
             0        1      3  24086.8916716              0    24087       0   
             0        1      3  24086.8916716  19197.7909681   25.47%       0   
             0        1      3  24086.8916716  19206.4233772   25.41%       0   
             0        1      3  24086.8916716  19210.6350488   25.38%       0   
             0        1      3  24086.8916716  19215.5729975   25.35%       0   
             0        1      6  21598.5625350  19215.5729975   12.40%       0   
             0        1      6  21598.5625350  19223.0707237   12.36%       0   
             0        1      6  21598.5625350  19223.8657285   12.35%       0   
             0        1      7  21558.5191802  19223.8657285   12.14%       0   
             0        1      7  21558.5191802  19224.8824948   12.14%       0   
             0        1      7  21558.5191802  19227.7927133   12.12%       0   
             0        1      7  21558.5191802  19229.9736676   12.11%       0   
NOTE: The MILP solver added 24 cuts with 855 cut coefficients at the root.      
           100       23      8  21550.8232111  19835.9645633    8.65%       0   
           133       18      9  21550.7090288  20010.7850290    7.70%       0   
           163       28     10  21549.6785006  20158.5748939    6.90%       0   
           172       24     11  21549.5643182  20385.0559999    5.71%       0   
           242       49     12  21548.6159565  20438.0425736    5.43%       0   
           256       54     13  21548.1764622  20438.0425736    5.43%       0   
           402       37     13  21548.1764622  21546.0755057    0.01%       0   
NOTE: Optimal within relative gap.                                              
NOTE: Objective = 21548.176462.                                                 
NOTE: The Cloud Analytic Services server processed the request in 0.765393      
      seconds.                                                                  
NOTE: The data set MYCAS.FCOUT has 510 observations and 9 variables.            


The following two SAS programs produce a plot of the solutions for both variants of the model, using data tables produced by PROC SQL from the PRIMALOUT= data tables produced by PROC OPTMILP.

title1 "Facility Location Problem";
title2 "TotalCost = &varcostNo (Variable = &varcostNo, Fixed = 0)";
data mycas.csdata;
   set mycas.cdata(rename=(y=cy)) mycas.sdata(rename=(y=sy));
run;
/* create Annotate data set to draw line between customer and */
/* assigned site                                              */
data anno;
   retain function "line" drawspace "datavalue"
      linethickness 1 linecolor "black";
   set CostNoFixedCharge_Data(keep=x1 y1 x2 y2);
run;
proc sgplot data=mycas.csdata sganno=anno 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;

The output from the first program appears in Output 7.3.3.

Output 7.3.3: Solution Plot for Facility Location with No Fixed Charges

Solution Plot for Facility Location with No Fixed Charges


title1 "Facility Location Problem";
title2 "TotalCost = &totalcost (Variable = &varcost, Fixed = &fixcost)";
/* create Annotate data set to draw line between customer and */
/* assigned site                                              */
data anno;
   retain function "line" drawspace "datavalue"
      linethickness 1 linecolor "black";
   set CostFixedCharge_Data(keep=x1 y1 x2 y2);
run;
proc sgplot data=mycas.csdata sganno=anno 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;

The output from the second program appears in Output 7.3.4.

Output 7.3.4: Solution Plot for Facility Location with Fixed Charges

Solution Plot for Facility Location with Fixed Charges


The economic trade-off for the fixed-charge model forces you to build fewer sites and push more demand to each site.

Last updated: June 14, 2018