Manpower Planning: How to Recruit, Retrain, Make Redundant, or Overman


PROC OPTMODEL Statements and Output

The first several index sets are one-dimensional, as in all the previous examples:

proc optmodel;
   set <str> WORKERS;
   num waste_new {WORKERS};
   num waste_old {WORKERS};
   num recruit_ub {WORKERS};
   num redundancy_cost {WORKERS};
   num overmanning_cost {WORKERS};
   num shorttime_ub {WORKERS};
   num shorttime_cost {WORKERS};
   read data worker_data into WORKERS=[worker]
      waste_new waste_old recruit_ub redundancy_cost overmanning_cost
      shorttime_ub shorttime_cost;

   set PERIODS0;
   num demand {WORKERS, PERIODS0};
   read data demand_data into PERIODS0=[period]
      {worker in WORKERS} <demand[worker,period]=col(worker)>;

   var NumWorkers {WORKERS, PERIODS0} >= 0;
   for {worker in WORKERS} fix NumWorkers[worker,0] = demand[worker,0];

   set PERIODS = PERIODS0 diff {0};
   var NumRecruits {worker in WORKERS, PERIODS} >= 0 <= recruit_ub[worker];
   var NumRedundant {WORKERS, PERIODS} >= 0;
   var NumShortTime {worker in WORKERS, PERIODS} >= 0 <= shorttime_ub[worker];
   var NumExcess {WORKERS, PERIODS} >= 0;

Both RETRAIN_PAIRS and DOWNGRADE_PAIRS are two-dimensional index sets, declared by using the optional <STR,STR> specification in the SET statement so that these sets contain pairs of strings. In general, a set can consist of tuples of any length and any combination of NUM and STR scalar-types.

   set <str,str> RETRAIN_PAIRS;
   num retrain_ub {RETRAIN_PAIRS};
   num retrain_cost {RETRAIN_PAIRS};
   read data retrain_data into RETRAIN_PAIRS=[worker1 worker2]
      retrain_ub retrain_cost;

   var NumRetrain {RETRAIN_PAIRS, PERIODS} >= 0;
   for {<i,j> in RETRAIN_PAIRS: retrain_ub[i,j] ne .}
      for {period in PERIODS} NumRetrain[i,j,period].ub = retrain_ub[i,j];

   set <str,str> DOWNGRADE_PAIRS;
   read data downgrade_data into DOWNGRADE_PAIRS=[worker1 worker2];
   var NumDowngrade {DOWNGRADE_PAIRS, PERIODS} >= 0;

   con Demand_con {worker in WORKERS, period in PERIODS}:
      NumWorkers[worker,period]
    - (1 - &shorttime_frac) * NumShortTime[worker,period]
    - NumExcess[worker,period]
    = demand[worker,period];

The following Flow_balance_con constraint uses an implicit slice to express a few of the summations compactly:

   con Flow_balance_con {worker in WORKERS, period in PERIODS}:
      NumWorkers[worker,period]
    = (1 - waste_old[worker]) * NumWorkers[worker,period-1]
    + (1 - waste_new[worker]) * NumRecruits[worker,period]
    + (1 - waste_old[worker]) *
         sum {<i,(worker)> in RETRAIN_PAIRS} NumRetrain[i,worker,period]
    + (1 - &downgrade_leave_frac) *
         sum {<i,(worker)> in DOWNGRADE_PAIRS} NumDowngrade[i,worker,period]
    - sum {<(worker),j> in RETRAIN_PAIRS} NumRetrain[worker,j,period]
    - sum {<(worker),j> in DOWNGRADE_PAIRS} NumDowngrade[worker,j,period]
    - NumRedundant[worker,period];

For example,

<i,(worker)> in RETRAIN_PAIRS

is equivalent to

i in slice(<*,worker>,RETRAIN_PAIRS)

which is equivalent to

i in WORKERS: <i,worker> in RETRAIN_PAIRS

The remaining two constraints are straightforward:

   con Semiskill_retrain_con {period in PERIODS}:
      NumRetrain['semiskilled','skilled',period]
   <= &semiskill_retrain_frac_ub * NumWorkers['skilled',period];

   con Overmanning_con {period in PERIODS}:
      sum {worker in WORKERS} NumExcess[worker,period] <= &overmanning_ub;

This example uses two objectives, Redundancy and Cost, declared in the following MIN statements:

   min Redundancy =
      sum {worker in WORKERS, period in PERIODS} NumRedundant[worker,period];
   min Cost =
      sum {worker in WORKERS, period in PERIODS} (
         redundancy_cost[worker] * NumRedundant[worker,period]
       + shorttime_cost[worker] * NumShorttime[worker,period]
       + overmanning_cost[worker] * NumExcess[worker,period])
    + sum {<i,j> in RETRAIN_PAIRS, period in PERIODS}
         retrain_cost[i,j] * NumRetrain[i,j,period];

The LP 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 Redundancy;
   print Redundancy Cost;
   print NumWorkers NumRecruits NumRedundant NumShortTime NumExcess;
   print NumRetrain;
   print NumDowngrade;
   create data sol_data1 from [worker period]
      NumWorkers NumRecruits NumRedundant NumShortTime NumExcess;
   create data sol_data2 from [worker1 worker2 period] NumRetrain NumDowngrade;

   solve obj Cost;
   print Redundancy Cost;
   print NumWorkers NumRecruits NumRedundant NumShortTime NumExcess;
   print NumRetrain;
   print NumDowngrade;
   create data sol_data3 from [worker period]
      NumWorkers NumRecruits NumRedundant NumShortTime NumExcess;
   create data sol_data4 from [worker1 worker2 period] NumRetrain NumDowngrade;
quit;

Figure 5.1 shows the output that results from the first SOLVE statement.

Figure 5.1: Output from First SOLVE Statement, Minimizing Redundancy

The OPTMODEL Procedure

Problem Summary
Objective Sense Minimization
Objective Function Redundancy
Objective Type Linear
   
Number of Variables 63
Bounded Above 0
Bounded Below 39
Bounded Below and Above 21
Free 0
Fixed 3
   
Number of Constraints 24
Linear LE (<=) 6
Linear EQ (=) 18
Linear GE (>=) 0
Linear Range 0
   
Constraint Coefficients 108

Performance Information
Execution Mode Single-Machine
Number of Threads 1

Solution Summary
Solver LP
Algorithm Dual Simplex
Objective Function Redundancy
Solution Status Optimal
Objective Value 841.796875
   
Primal Infeasibility 2.842171E-14
Dual Infeasibility 0
Bound Infeasibility 0
   
Iterations 14
Presolve Time 0.02
Solution Time 0.02

Redundancy Cost
841.8 1441390

[1] [2] NumWorkers NumRecruits NumRedundant NumShortTime NumExcess
semiskilled 0 1500        
semiskilled 1 1443 0.00 0.00 50 17.969
semiskilled 2 2000 649.30 0.00 0 0.000
semiskilled 3 2500 676.97 0.00 0 0.000
skilled 0 1000        
skilled 1 1025 0.00 0.00 50 0.000
skilled 2 1500 500.00 0.00 0 0.000
skilled 3 2000 500.00 0.00 0 0.000
unskilled 0 2000        
unskilled 1 1157 0.00 442.97 50 132.031
unskilled 2 675 0.00 166.33 50 150.000
unskilled 3 175 0.00 232.50 50 150.000

[1] [2] [3] NumRetrain
semiskilled skilled 1 256.250
semiskilled skilled 2 80.263
semiskilled skilled 3 131.579
unskilled semiskilled 1 200.000
unskilled semiskilled 2 200.000
unskilled semiskilled 3 200.000

[1] [2] [3] NumDowngrade
semiskilled unskilled 1 0.00
semiskilled unskilled 2 0.00
semiskilled unskilled 3 0.00
skilled semiskilled 1 168.44
skilled semiskilled 2 0.00
skilled semiskilled 3 0.00
skilled unskilled 1 0.00
skilled unskilled 2 0.00
skilled unskilled 3 0.00



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

Figure 5.2: Output from Second SOLVE Statement, Minimizing Cost

Problem Summary
Objective Sense Minimization
Objective Function Cost
Objective Type Linear
   
Number of Variables 63
Bounded Above 0
Bounded Below 39
Bounded Below and Above 21
Free 0
Fixed 3
   
Number of Constraints 24
Linear LE (<=) 6
Linear EQ (=) 18
Linear GE (>=) 0
Linear Range 0
   
Constraint Coefficients 108

Performance Information
Execution Mode Single-Machine
Number of Threads 1

Solution Summary
Solver LP
Algorithm Dual Simplex
Objective Function Cost
Solution Status Optimal
Objective Value 498677.28532
   
Primal Infeasibility 2.842171E-14
Dual Infeasibility 0
Bound Infeasibility 0
   
Iterations 9
Presolve Time 0.00
Solution Time 0.00

Redundancy Cost
1423.7 498677

[1] [2] NumWorkers NumRecruits NumRedundant NumShortTime NumExcess
semiskilled 0 1500        
semiskilled 1 1400 0.000 0.00 0 0
semiskilled 2 2000 800.000 0.00 0 0
semiskilled 3 2500 800.000 0.00 0 0
skilled 0 1000        
skilled 1 1000 55.556 0.00 0 0
skilled 2 1500 500.000 0.00 0 0
skilled 3 2000 500.000 0.00 0 0
unskilled 0 2000        
unskilled 1 1000 0.000 812.50 0 0
unskilled 2 500 0.000 257.62 0 0
unskilled 3 0 0.000 353.60 0 0

[1] [2] [3] NumRetrain
semiskilled skilled 1 0.000
semiskilled skilled 2 105.263
semiskilled skilled 3 131.579
unskilled semiskilled 1 0.000
unskilled semiskilled 2 142.382
unskilled semiskilled 3 96.399

[1] [2] [3] NumDowngrade
semiskilled unskilled 1 25
semiskilled unskilled 2 0
semiskilled unskilled 3 0
skilled semiskilled 1 0
skilled semiskilled 2 0
skilled semiskilled 3 0
skilled unskilled 1 0
skilled unskilled 2 0
skilled unskilled 3 0