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 SenseMinimization
Objective FunctionRedundancy
Objective TypeLinear
  
Number of Variables63
Bounded Above0
Bounded Below39
Bounded Below and Above21
Free0
Fixed3
  
Number of Constraints24
Linear LE (<=)6
Linear EQ (=)18
Linear GE (>=)0
Linear Range0
  
Constraint Coefficients108

Performance Information
Execution ModeSingle-Machine
Number of Threads1

Solution Summary
SolverLP
AlgorithmDual Simplex
Objective FunctionRedundancy
Solution StatusOptimal
Objective Value841.796875
  
Primal Infeasibility1.421085E-14
Dual Infeasibility0
Bound Infeasibility0
  
Iterations13
Presolve Time0.00
Solution Time0.00

RedundancyCost
841.81462048

[1][2]NumWorkersNumRecruitsNumRedundantNumShortTimeNumExcess
semiskilled01500    
semiskilled114430.000.005017.969
semiskilled22000682.200.0000.000
semiskilled32500645.720.0000.000
skilled01000    
skilled110250.000.00500.000
skilled21525500.000.00500.000
skilled32000500.000.0000.000
unskilled02000    
unskilled111570.00442.9750132.031
unskilled26750.00166.3350150.000
unskilled31750.00232.5050150.000

[1][2][3]NumRetrain
semiskilledskilled1256.25
semiskilledskilled2106.58
semiskilledskilled3106.58
unskilledsemiskilled1200.00
unskilledsemiskilled2200.00
unskilledsemiskilled3200.00

[1][2][3]NumDowngrade
semiskilledunskilled10.00
semiskilledunskilled20.00
semiskilledunskilled30.00
skilledsemiskilled1168.44
skilledsemiskilled20.00
skilledsemiskilled30.00
skilledunskilled10.00
skilledunskilled20.00
skilledunskilled30.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 SenseMinimization
Objective FunctionCost
Objective TypeLinear
  
Number of Variables63
Bounded Above0
Bounded Below39
Bounded Below and Above21
Free0
Fixed3
  
Number of Constraints24
Linear LE (<=)6
Linear EQ (=)18
Linear GE (>=)0
Linear Range0
  
Constraint Coefficients108

Performance Information
Execution ModeSingle-Machine
Number of Threads1

Solution Summary
SolverLP
AlgorithmDual Simplex
Objective FunctionCost
Solution StatusOptimal
Objective Value498677.28532
  
Primal Infeasibility2.842171E-14
Dual Infeasibility0
Bound Infeasibility0
  
Iterations8
Presolve Time0.00
Solution Time0.00

RedundancyCost
1423.7498677

[1][2]NumWorkersNumRecruitsNumRedundantNumShortTimeNumExcess
semiskilled01500    
semiskilled114000.0000.0000
semiskilled22000800.0000.0000
semiskilled32500800.0000.0000
skilled01000    
skilled1100055.5560.0000
skilled21500500.0000.0000
skilled32000500.0000.0000
unskilled02000    
unskilled110000.000812.5000
unskilled25000.000257.6200
unskilled300.000353.6000

[1][2][3]NumRetrain
semiskilledskilled10.000
semiskilledskilled2105.263
semiskilledskilled3131.579
unskilledsemiskilled10.000
unskilledsemiskilled2142.382
unskilledsemiskilled396.399

[1][2][3]NumDowngrade
semiskilledunskilled125
semiskilledunskilled20
semiskilledunskilled30
skilledsemiskilled10
skilledsemiskilled20
skilledsemiskilled30
skilledunskilled10
skilledunskilled20
skilledunskilled30