The OPTMODEL Procedure

Example 9.7 Sparse Modeling

This example demonstrates how to rewrite certain models for more efficient processing. Sometimes optimization models that run out of memory during problem generation can be rewritten to take advantage of sparsity to use memory more efficiently. This often occurs when a large array is modeled in a dense format but most of its entries are zeros. Usually, the array provides problem coefficients or it contains optimization variables.

The model for this example solves the facility location problem that is described in Example 14.3. This example is concerned with the resources that are required for PROC OPTMODEL problem generation and solver initialization. So the size of the problem has been increased, but the model has also been modified to make it easier to solve. In order to handle the larger problem size, the model eliminates a large number of the potential assignments of customers to facilities based on distance, making the problem sparse.

The following code generates a random instance of the facility location problem:

%let NumCustomers  = 1500;
%let NumSites      = 250;
%let SiteCapacity  = 50;
%let MaxDemand     = 10;
%let xmax          = 200;
%let ymax          = 100;
%let seed          = 938;

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

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

The following code uses a dense version of the facility location model. This model is equivalent to the model from Example 14.3 except for the added constraint distance_at_most_30. This constraint eliminates from consideration the assignment of customers to facilities over long distances by forcing the corresponding Assign variables to 0.

proc optmodel printlevel=2;
   profile on percent=0.1;
   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];

   /* do not assign customer to site more than 30 units away */
   con distance_at_most_30 {i in CUSTOMERS, j in SITES: dist[i,j] > 30}:
      Assign[i,j] = 0;

   /* solve the MILP */
   solve with milp/timetype=real;

quit;

If you inspect the log after running the preceding code, then you will see that the MILP presolver has pruned down the problem size considerably. If you also run the code with the SAS option FULLSTIMER enabled on a 64-bit system, then you will notice that about 1.1GB of memory is required for the PROC OPTMODEL step when you are running on a single CPU.

The solution and timing results for the dense model are shown in Output 9.7.1. The PROFILE ON statement requests further timing details, including evaluation time for declarations used by problem generation.

Output 9.7.1: Dense Model Results

Solution Summary
SolverMILP
AlgorithmBranch and Cut
Objective FunctionCostFixedCharge
Solution StatusOptimal within Relative Gap
Objective Value18001.140353
  
Relative Gap0.0000868876
Absolute Gap1.5639396138
Primal Infeasibility1.110223E-14
Bound Infeasibility1.998401E-15
Integer Infeasibility1.998401E-15
  
Best Bound17999.576414
Nodes1
Solutions Found5
Iterations24531
Presolve Time4.49
Solution Time32.52

Procedure Task Timing
TaskTime
(sec.)
% Time
Problem Generation4.1510.76%
Solver Initialization1.163.00%
Code Generation0.060.15%
Presolve4.4911.63%
Root Node Processing27.9972.50%
Branch And Cut0.000.00%
Synchronization0.020.06%
Idle0.000.00%
Other Tasks0.020.04%
Solver Postprocessing0.721.85%

Profile Information
ItemLineCol.Execution
Count
Net Time
(sec.)
Wait Time
(sec.)
% Total
Time
SOLVE39484135.271.2483.2%
Constraint distance_at_most_3039448 3.040.007.2%
Constraint link39358 2.080.004.9%
Number dist39168 0.550.001.3%
Min CostNoFixedCharge39258 0.400.000.9%
Constraint assign_def39318 0.360.000.8%
Var Assign39228 0.340.000.8%
Constraint capacity39398 0.340.000.8%
Other profiled items   0.010.000.0%

Note:Total profiled time is 42.39 seconds.



The best approach for reducing the memory requirements is to eliminate the Assign variables that are always going to be 0. This is accomplished in the following sparse version of the code. Instead of indexing Assign over the crossproduct of CUSTOMERS and SITES, now the code defines a new set of pairs that satisfy the distance requirement, CUSTOMERS_SITES. This set replaces the constraint distance_at_most_30 in the dense model. The objective and constraints have been modified to use the new indexing scheme, with implicit set slicing (as described in the section More on Index Sets) for constraints assign_def and capacity.

proc optmodel printlevel=2;
   profile on percent=0.1;
   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;

   set CUSTOMERS_SITES = {i in CUSTOMERS, j in SITES: dist[i,j] <= 30};
   var Assign {CUSTOMERS_SITES} binary;
   var Build {SITES} binary;

   min CostNoFixedCharge
       = sum {<i,j> in CUSTOMERS_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 {<(i),j> in CUSTOMERS_SITES} Assign[i,j] = 1;

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

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

   /* solve the MILP */
   solve with milp/timetype=real;

quit;

The log from running the preceding code shows that the MILP presolver does not find anything to improve with this version of the model. On a 64-bit system, the FULLSTIMER option shows that memory requirements have been reduced to about 320MB when you are running on a single CPU, less than half the requirements of the previous model.

The solution and timing results for the sparse model are shown in Output 9.7.2. Note that the dense model (Output 9.7.1) and the sparse model (Output 9.7.2) are equivalent after presolver processing and generate the same result using similar amounts of solver time. On the other hand, problem generation time is significantly reduced as are other times including presolve time. Both models used the solver option TIMETYPE=REAL so that all times are reported in seconds of real time. You can see from the "Profile Information" table that the overhead associated with problem declarations has been significantly reduced.

Output 9.7.2: Sparse Model Results

Solution Summary
SolverMILP
AlgorithmBranch and Cut
Objective FunctionCostFixedCharge
Solution StatusOptimal within Relative Gap
Objective Value18001.140353
  
Relative Gap0.0000868876
Absolute Gap1.5639396138
Primal Infeasibility1.110223E-14
Bound Infeasibility1.998401E-15
Integer Infeasibility1.998401E-15
  
Best Bound17999.576414
Nodes1
Solutions Found7
Iterations24531
Presolve Time2.84
Solution Time31.00

Procedure Task Timing
TaskTime
(sec.)
% Time
Problem Generation1.273.91%
Solver Initialization0.100.31%
Code Generation0.000.01%
Presolve2.848.77%
Root Node Processing28.0186.36%
Branch And Cut0.000.00%
Synchronization0.020.08%
Idle0.000.00%
Other Tasks0.130.39%
Solver Postprocessing0.050.16%

Profile Information
ItemLineCol.Execution
Count
Net Time
(sec.)
Wait Time
(sec.)
% Total
Time
SOLVE39974131.220.0995.4%
Number dist39688 0.550.001.7%
Set CUSTOMERS_SITES39748 0.480.001.5%
Constraint link39888 0.230.000.7%
Constraint assign_def39848 0.080.000.2%
Constraint capacity39928 0.060.000.2%
Min CostNoFixedCharge39788 0.050.000.1%
Var Assign39758 0.040.000.1%
Other profiled items   0.010.000.0%

Note:Total profiled time is 32.72 seconds.



Last updated: June 22, 2026