This example uses both one-dimensional and dense two-dimensional data, as in Chapter 1 and Chapter 2:
proc optmodel; set <str> PRODUCTS; num profit {PRODUCTS}; read data product_data into PRODUCTS=[product] profit; set PERIODS; num demand {PRODUCTS, PERIODS}; read data demand_data into PERIODS=[_N_] {product in PRODUCTS} <demand[product,_N_]=col(product)>; set <str> MACHINE_TYPES; num num_machines {MACHINE_TYPES}; read data machine_type_data into MACHINE_TYPES=[machine_type] num_machines;
But this problem also has sparse two-dimensional data: for most pairs, the number of machines down is 0. In the following statements, the INIT option in the second NUM statement initializes
num_machines_down_per_period to 0. The read of the sparse data set machine_type_period_data
populates only the nonzero values. The subsequent computation of num_machines_per_period[machine_type,period] then uses the initial value of num_machines_down_per_period[machine_type,period] when no other value has been supplied:
num num_machines_per_period {machine_type in MACHINE_TYPES, PERIODS} init num_machines[machine_type]; num num_machines_down_per_period {MACHINE_TYPES, PERIODS} init 0; read data machine_type_period_data into [machine_type period] num_machines_down_per_period=num_down; for {machine_type in MACHINE_TYPES, period in PERIODS} num_machines_per_period[machine_type,period] = num_machines_per_period[machine_type,period] - num_machines_down_per_period[machine_type,period]; print num_machines_per_period;
Figure 3.1 shows the resulting values of num_machines_per_period.
Figure 3.1: num_machines_per_period Parameter
num_machines_per_period | ||||||
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | |
borer | 1 | 1 | 0 | 1 | 1 | 1 |
grinder | 3 | 4 | 4 | 4 | 3 | 4 |
hdrill | 3 | 1 | 3 | 3 | 3 | 2 |
planer | 1 | 1 | 1 | 1 | 1 | 0 |
vdrill | 2 | 2 | 2 | 1 | 1 | 2 |
The following statements declare and read dense two-dimensional data:
num production_time {PRODUCTS, MACHINE_TYPES}; read data machine_type_product_data into [machine_type] {product in PRODUCTS} <production_time[product,machine_type]=col(product)>;
The following statements are straightforward:
var Make {PRODUCTS, PERIODS} >= 0; var Sell {product in PRODUCTS, period in PERIODS} >= 0 <= demand[product,period]; num last_period = max {period in PERIODS} period; var Store {PRODUCTS, PERIODS} >= 0 <= &store_ub; for {product in PRODUCTS} fix Store[product,last_period] = &final_storage; impvar StorageCost = sum {product in PRODUCTS, period in PERIODS} &storage_cost_per_unit * Store[product,period]; max TotalProfit = sum {product in PRODUCTS, period in PERIODS} profit[product] * Sell[product,period] - StorageCost; con Machine_hours_con {machine_type in MACHINE_TYPES, period in PERIODS}: sum {product in PRODUCTS} production_time[product,machine_type] * Make[product,period] <= &num_hours_per_period * num_machines_per_period[machine_type,period];
The following Flow_balance_con constraint uses an IF-THEN/ELSE expression to handle the boundary conditions: if a previous period exists, the units in storage at the end of the previous period are available to be sold in the current period. (Because ELSE 0 is the default, you could use just an IF-THEN expression instead.)
con Flow_balance_con {product in PRODUCTS, period in PERIODS}: (if period - 1 in PERIODS then Store[product,period-1] else 0) + Make[product,period] = Sell[product,period] + Store[product,period]; solve; print Make Sell Store;
The maximum total profit is £93,715, as shown in Figure 3.2.
Figure 3.2: Output from Linear Programming Solver
Problem Summary | |
---|---|
Objective Sense | Maximization |
Objective Function | TotalProfit |
Objective Type | Linear |
Number of Variables | 126 |
Bounded Above | 0 |
Bounded Below | 42 |
Bounded Below and Above | 71 |
Free | 0 |
Fixed | 13 |
Number of Constraints | 72 |
Linear LE (<=) | 30 |
Linear EQ (=) | 42 |
Linear GE (>=) | 0 |
Linear Range | 0 |
Constraint Coefficients | 281 |
Performance Information | |
---|---|
Execution Mode | Single-Machine |
Number of Threads | 1 |
Solution Summary | |
---|---|
Solver | LP |
Algorithm | Dual Simplex |
Objective Function | TotalProfit |
Solution Status | Optimal |
Objective Value | 93715.178571 |
Primal Infeasibility | 1.421085E-14 |
Dual Infeasibility | 2.220446E-16 |
Bound Infeasibility | 0 |
Iterations | 32 |
Presolve Time | 0.00 |
Solution Time | 0.00 |
[1] | [2] | Make | Sell | Store |
---|---|---|---|---|
prod1 | 1 | 500.00 | 500.00 | 0.0 |
prod1 | 2 | 700.00 | 600.00 | 100.0 |
prod1 | 3 | 0.00 | 100.00 | 0.0 |
prod1 | 4 | 200.00 | 200.00 | 0.0 |
prod1 | 5 | 0.00 | 0.00 | 0.0 |
prod1 | 6 | 550.00 | 500.00 | 50.0 |
prod2 | 1 | 888.57 | 888.57 | 0.0 |
prod2 | 2 | 600.00 | 500.00 | 100.0 |
prod2 | 3 | 0.00 | 100.00 | 0.0 |
prod2 | 4 | 300.00 | 300.00 | 0.0 |
prod2 | 5 | 100.00 | 100.00 | 0.0 |
prod2 | 6 | 550.00 | 500.00 | 50.0 |
prod3 | 1 | 382.50 | 300.00 | 82.5 |
prod3 | 2 | 117.50 | 200.00 | 0.0 |
prod3 | 3 | 0.00 | 0.00 | 0.0 |
prod3 | 4 | 400.00 | 400.00 | 0.0 |
prod3 | 5 | 600.00 | 500.00 | 100.0 |
prod3 | 6 | 0.00 | 50.00 | 50.0 |
prod4 | 1 | 300.00 | 300.00 | 0.0 |
prod4 | 2 | 0.00 | 0.00 | 0.0 |
prod4 | 3 | 0.00 | 0.00 | 0.0 |
prod4 | 4 | 500.00 | 500.00 | 0.0 |
prod4 | 5 | 100.00 | 100.00 | 0.0 |
prod4 | 6 | 350.00 | 300.00 | 50.0 |
prod5 | 1 | 800.00 | 800.00 | 0.0 |
prod5 | 2 | 500.00 | 400.00 | 100.0 |
prod5 | 3 | 0.00 | 100.00 | 0.0 |
prod5 | 4 | 200.00 | 200.00 | 0.0 |
prod5 | 5 | 1100.00 | 1000.00 | 100.0 |
prod5 | 6 | 0.00 | 50.00 | 50.0 |
prod6 | 1 | 200.00 | 200.00 | 0.0 |
prod6 | 2 | 300.00 | 300.00 | 0.0 |
prod6 | 3 | 400.00 | 400.00 | 0.0 |
prod6 | 4 | 0.00 | 0.00 | 0.0 |
prod6 | 5 | 300.00 | 300.00 | 0.0 |
prod6 | 6 | 550.00 | 500.00 | 50.0 |
prod7 | 1 | 0.00 | 0.00 | 0.0 |
prod7 | 2 | 250.00 | 150.00 | 100.0 |
prod7 | 3 | 0.00 | 100.00 | 0.0 |
prod7 | 4 | 100.00 | 100.00 | 0.0 |
prod7 | 5 | 100.00 | 0.00 | 100.0 |
prod7 | 6 | 0.00 | 50.00 | 50.0 |
You can use the .dual
constraint suffix to access the optimal dual variables returned by the solver:
print Machine_hours_con.dual; create data sol_data1 from [product period] Make Sell Store; quit;
These values, shown in Figure 3.3, suggest the change in optimal objective value if the factory acquires an additional machine in that period. For this test instance, it turns out that the positive dual variables all correspond to machines that are down.
Figure 3.3: Optimal Dual Variables
Machine_hours_con.DUAL | ||||||
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | |
borer | 0.0000 | 0.0000 | 200.0000 | 0.0000 | 0.0000 | 0.0000 |
grinder | 8.5714 | 0.0000 | 0.0000 | 0.0000 | 0.0000 | 0.0000 |
hdrill | 0.0000 | 0.6250 | 0.0000 | 0.0000 | 0.0000 | 0.0000 |
planer | 0.0000 | 0.0000 | 0.0000 | 0.0000 | 0.0000 | 800.0000 |
vdrill | 0.0000 | 0.0000 | 0.0000 | 0.0000 | 0.0000 | 0.0000 |