PROC OPTMODEL Statements and Output

The following PROC OPTMODEL statements declare an index set and parameters and then read the input data:

proc optmodel;
   set RETAILERS;
   num region {RETAILERS};
   num oil {RETAILERS};
   num delivery {RETAILERS};
   num spirit {RETAILERS};
   str growth {RETAILERS};
   read data retailer_data into RETAILERS=[_N_] 
      region oil delivery spirit growth;

The following statements declare parameters and index sets, which are initialized to be empty and then populated within a FOR loop. Note that both RETAILERS_region and RETAILERS_group are sets that are indexed by other sets:

   set REGIONS init {};
   set RETAILERS_region {REGIONS} init {};
   num r;
   set <str> GROUPS init {};
   set RETAILERS_group {GROUPS} init {};
   str g;
   for {retailer in RETAILERS} do;
      r = region[retailer];
      REGIONS = REGIONS union {r};
      RETAILERS_region[r] = RETAILERS_region[r] union {retailer};
      g = growth[retailer];
      GROUPS = GROUPS union {g};
      RETAILERS_group[g] = RETAILERS_group[g] union {retailer};
   end;

   set DIVISIONS;
   num target {DIVISIONS};
   read data division_data into DIVISIONS=[_N_] target;

   num tolerance = &tolerance;

The following declarations are straightforward:

   var Assign {RETAILERS, DIVISIONS} binary;

   con Assign_con {retailer in RETAILERS}:
      sum {division in DIVISIONS} Assign[retailer,division] = 1;

   set CATEGORIES = {'delivery','spirit'} 
      union (setof {reg in REGIONS} 'oil'||reg)
      union (setof {group in GROUPS} 'growth'||group);
   var MarketShare {CATEGORIES, DIVISIONS};
   var Surplus     {CATEGORIES, DIVISIONS} >= 0 <= tolerance;
   var Slack       {CATEGORIES, DIVISIONS} >= 0 <= tolerance;

   min Objective1 =
      sum {category in CATEGORIES, division in DIVISIONS}
         (Surplus[category,division] + Slack[category,division]);

   con Delivery_con {division in DIVISIONS}:
      MarketShare['delivery',division]
    = (sum {retailer in RETAILERS} delivery[retailer] * 
         Assign[retailer,division])
         / (sum {retailer in RETAILERS} delivery[retailer]);

   con Spirit_con {division in DIVISIONS}:
      MarketShare['spirit',division]
    = (sum {retailer in RETAILERS} spirit[retailer] * 
         Assign[retailer,division])
         / (sum {retailer in RETAILERS} spirit[retailer]);

   con Oil_con {reg in REGIONS, division in DIVISIONS}:
      MarketShare['oil'||reg,division]
    = (sum {retailer in RETAILERS_region[reg]} 
         oil[retailer] * Assign[retailer,division])
         / (sum {retailer in RETAILERS_region[reg]} oil[retailer]);

   con Growth_con {group in GROUPS, division in DIVISIONS}:
      MarketShare['growth'||group,division]
    = (sum {retailer in RETAILERS_group[group]} Assign[retailer,division])
         / card(RETAILERS_group[group]);
      
   con Abs_dev_con {category in CATEGORIES, division in DIVISIONS}:
      MarketShare[category,division] 
    - Surplus[category,division] + Slack[category,division] 
    = target[division];

The following NUM statements use the ABS function, the .sol variable suffix, and the MAX aggregation operator to compute the two norms from the optimal solution:

   num sum_abs_dev =
      sum {category in CATEGORIES, division in DIVISIONS} 
         abs(MarketShare[category,division].sol - target[division]);
   num max_abs_dev =
      max {category in CATEGORIES, division in DIVISIONS} 
         abs(MarketShare[category,division].sol - target[division]);

The MILP solver is called twice, and each SOLVE statement includes the OBJ option to specify which objective to optimize. The first PRINT statement after each SOLVE statement reports the values of both objectives even though only one objective is optimized at a time:

   solve obj Objective1;
   print sum_abs_dev max_abs_dev;
   print Assign;
   print MarketShare Surplus Slack;

Figure 14.1 shows the output that results from the first SOLVE statement. The optimal objective value disagrees with the value 0.0453 presented in Williams, even after you account for the fact that the formulation here counts each deviation twice (once per division).

Figure 14.1: Output from First SOLVE Statement, Minimizing Sum of Deviations

The OPTMODEL Procedure

Problem Summary
Objective Sense Minimization
Objective Function Objective1
Objective Type Linear
   
Number of Variables 88
Bounded Above 0
Bounded Below 0
Bounded Below and Above 74
Free 14
Fixed 0
Binary 46
Integer 0
   
Number of Constraints 51
Linear LE (<=) 0
Linear EQ (=) 51
Linear GE (>=) 0
Linear Range 0
   
Constraint Coefficients 284

Performance Information
Execution Mode Single-Machine
Number of Threads 1

Solution Summary
Solver MILP
Algorithm Branch and Cut
Objective Function Objective1
Solution Status Optimal
Objective Value 0.1059523594
   
Relative Gap 0
Absolute Gap 0
Primal Infeasibility 1.110223E-16
Bound Infeasibility 0
Integer Infeasibility 0
   
Best Bound 0.1059523594
Nodes 548
Iterations 4186
Presolve Time 0.00
Solution Time 0.39

sum_abs_dev max_abs_dev
0.10595 0.025

Assign
  1 2
1 1 0
2 1 0
3 1 0
4 1 0
5 0 1
6 0 1
7 0 1
8 0 1
9 0 1
10 0 1
11 1 0
12 0 1
13 0 1
14 0 1
15 0 1
16 1 0
17 0 1
18 1 0
19 0 1
20 0 1
21 1 0
22 1 0
23 0 1

[1] [2] MarketShare Surplus Slack
delivery 1 0.40000 0.0000000 0.0000000
delivery 2 0.60000 0.0000000 0.0000000
growthA 1 0.37500 0.0000000 0.0250000
growthA 2 0.62500 0.0250000 0.0000000
growthB 1 0.40000 0.0000000 0.0000000
growthB 2 0.60000 0.0000000 0.0000000
oil1 1 0.39552 0.0000000 0.0044776
oil1 2 0.60448 0.0044776 0.0000000
oil2 1 0.39070 0.0000000 0.0093023
oil2 2 0.60930 0.0093023 0.0000000
oil3 1 0.40000 0.0000000 0.0000000
oil3 2 0.60000 0.0000000 0.0000000
spirit 1 0.41420 0.0141962 0.0000000
spirit 2 0.58580 0.0000000 0.0141962


The following statements declare the additional variable, objective, and constraints for problem (ii), with the tighter linearization of the $L_\infty $ norm as in Chapter 12:

   var MinMax >= 0 init max_abs_dev;
   min Objective2 = MinMax;
   con MinMax_con {category in CATEGORIES, division in DIVISIONS}:
      MinMax >= Surplus[category,division] + Slack[category,division];

The following statements call the MILP solver to solve problem (ii). The PRIMALIN option is used to initialize the MILP solver with the optimal solution that was obtained for problem (i):

   solve obj Objective2 with MILP / primalin;
   print sum_abs_dev max_abs_dev;
   print Assign;
   print MarketShare Surplus Slack;
quit;

Figure 14.2 shows the output that results from the second SOLVE statement.

Figure 14.2: Output from Second SOLVE Statement, Minimizing Maximum Deviation

Problem Summary
Objective Sense Minimization
Objective Function Objective2
Objective Type Linear
   
Number of Variables 89
Bounded Above 0
Bounded Below 1
Bounded Below and Above 74
Free 14
Fixed 0
Binary 46
Integer 0
   
Number of Constraints 65
Linear LE (<=) 0
Linear EQ (=) 51
Linear GE (>=) 14
Linear Range 0
   
Constraint Coefficients 326

Performance Information
Execution Mode Single-Machine
Number of Threads 1

Solution Summary
Solver MILP
Algorithm Branch and Cut
Objective Function Objective2
Solution Status Optimal
Objective Value 0.025
   
Relative Gap 0
Absolute Gap 0
Primal Infeasibility 1.110223E-16
Bound Infeasibility 0
Integer Infeasibility 0
   
Best Bound 0.025
Nodes 1
Iterations 47
Presolve Time 0.00
Solution Time 0.02

sum_abs_dev max_abs_dev
0.10595 0.025

Assign
  1 2
1 1 0
2 1 0
3 1 0
4 1 0
5 0 1
6 0 1
7 0 1
8 0 1
9 0 1
10 0 1
11 1 0
12 0 1
13 0 1
14 0 1
15 0 1
16 1 0
17 0 1
18 1 0
19 0 1
20 0 1
21 1 0
22 1 0
23 0 1

[1] [2] MarketShare Surplus Slack
delivery 1 0.40000 0.0000000 0.0000000
delivery 2 0.60000 0.0000000 0.0000000
growthA 1 0.37500 0.0000000 0.0250000
growthA 2 0.62500 0.0250000 0.0000000
growthB 1 0.40000 0.0000000 0.0000000
growthB 2 0.60000 0.0000000 0.0000000
oil1 1 0.39552 0.0000000 0.0044776
oil1 2 0.60448 0.0044776 0.0000000
oil2 1 0.39070 0.0000000 0.0093023
oil2 2 0.60930 0.0093023 0.0000000
oil3 1 0.40000 0.0000000 0.0000000
oil3 2 0.60000 0.0000000 0.0000000
spirit 1 0.41420 0.0141962 0.0000000
spirit 2 0.58580 0.0000000 0.0141962