Initializing help system before first use

User-defined enumeration strategies

The following problem description is taken from Section 14.1 of `Applications of optimization with Xpress-MP'.

An operator needs to be assigned to each of the six machines in a workshop. Six workers have been pre-selected. Everyone has undergone a test of her productivity on every machine. Table Productivity in pieces per hour lists the productivities in pieces per hour. The machines run in parallel, that is, the total productivity of the workshop is the sum of the productivities of the people assigned to the machines.

Table 4.1: Productivity in pieces per hour
Machines
Workers 1 2 3 4 5 6
1 13 24 31 19 40 29
2 18 25 30 15 43 22
3 20 20 27 25 34 33
4 23 26 28 18 37 30
5 28 33 34 17 38 20
6 19 36 25 27 45 24

The objective is to determine an assignment of workers to machines that maximizes the total productivity. We may start by calculating a (non-optimal) heuristic solution using the following fairly natural method: choose the assignment p → m with the highest productivity, cross out the line p and the column m (since the person has been placed and the machine has an operator), and restart this process until we have assigned all persons—the resulting assignment is highlighted in bold print in the data table. However, our aim really is to solve this problem to optimality for the case of parallel machines and also for machines working in series.

Model formulation

This problem type is well known under the name of the assignment problem. Let PERS be the set of workers, MACH the set of machines (both of the same size N), and OUTPpm the productivity of worker p on machine m. We define N integer variables assignp taking values in the set of machines, where assignp denotes the number of the machine to which the person p is assigned. The fact that a person p is assigned to a single machine m and a machine m is operated by a single person p is then expressed by the constraint that all variables assignp take different values.

∀ p ∈ PERS: assignp ∈ MACH
all-different(
m ∈ MACH
assignp)

Furthermore, let outputp denote the output produced by person p. The values of these variables are obtained as discrete functions in the assignp variables:

∀ p ∈ PERS: outputp = OUTPp,assignp

Parallel machine assignment

The objective function to be maximized sums the outputp variables.

maximize
p ∈ PERS
outputp

Certain assignments may be infeasible. In such a case, the value of the corresponding machine needs to be removed from the domain of the variable assignp.

Machines working in series

If the machines work in series, the least productive worker on the machine she has been assigned to determines the total productivity of the workshop. An assignment will still be described by N variables assignp and an all-different constraint on these variables. We also have the outputp variables with the constraints linking them to the values of the assignp variables from the previous model. To this we add a variable pmin for the minimum productivity. The objective is to maximize pmin. This type of optimization problem where one wants to maximize a minimum is called maximin, or bottleneck.

maximize pmin
pmin = minimump ∈ PERS(outputp)

Implementation

The following Mosel program first implements and solves the model for the case of parallel machines. Afterwards, we define the variable pmin that is required for solving the problem for the case that the machines work in series.

model "I-1 Personnel assignment (CP)"
 uses "kalis"

 forward procedure parallel_heur
 forward procedure print_sol(text1,text2:string, objval:integer)

 declarations
  PERS = 1..6                        ! Personnel
  MACH = 1..6                        ! Machines
  OUTP: array(PERS,MACH) of integer  ! Productivity
 end-declarations

 initializations from 'Data/i1assign.dat'
  OUTP
 end-initializations

! **** Exact solution for parallel machines ****

 declarations
  assign: array(PERS) of cpvar       ! Machine assigned to a person
  output: array(PERS) of cpvar       ! Productivity of every person
  totalProd: cpvar                   ! Total productivity
  O: array(MACH) of integer          ! Auxiliary array for constraint def.
  Strategy: cpbranching              ! Branching strategy
 end-declarations

 forall(p in PERS) setdomain(assign(p), MACH)

! Calculate productivity per worker
 forall(p in PERS) do
  forall(m in MACH) O(m):= OUTP(p,m)
  element(O, assign(p)) = output(p)
 end-do

! Calculate total productivity
 totalProd = sum(p in PERS) output(p)

! One person per machine
 all_different(assign)

! Branching strategy
 Strategy:= assign_var(KALIS_LARGEST_MAX, KALIS_MAX_TO_MIN, output)
 cp_set_branching(Strategy)

! Solve the problem
 if cp_maximize(totalProd) then
  print_sol("Exact solution (parallel assignment)", "Total", getsol(totalProd))
 end-if

! **** Exact solution for serial machines ****

 declarations
  pmin: cpvar                        ! Minimum productivity
 end-declarations

! Calculate minimum productivity
 pmin = minimum(output)

! Branching strategy
 Strategy:= assign_var(KALIS_SMALLEST_MIN, KALIS_MAX_TO_MIN, output)
 cp_set_branching(Strategy)

! Solve the problem
 if cp_maximize(pmin) then
  print_sol("Exact solution (serial machines)", "Minimum", getsol(pmin))
 end-if

When the solution to the parallel assignment problem is found, we print out the solution and re-start the search with a new branching strategy and a new objective function. Since the first search has finished completely (no interruption by a time limit, etc.) there is no need to reset the solver between the two runs.

The branching strategy chosen for the parallel assignment problem is inspired by the intuitive procedure described in the introduction to Section User-defined enumeration strategies: instead of enumerating the possible assignments of workers to machines (= enumeration of the assignp variables) we define an enumeration over the outputp variables, choosing the variable with the largest remaining value (KALIS_LARGEST_MAX) and branch on its values in decreasing order (KALIS_MAX_TO_MIN). For the second problem we need to proceed differently: to avoid being left with a small productivity value for some worker p we pick first the outputp variable with the smallest lower bound (KALIS_SMALLEST_MIN); again we enumerate the values starting with the largest one.

The following procedure parallel_heur may be added to the above program. It heuristically calculates a (non-optimal) solution to the parallel assignment problem using the intuitive procedure described in the introduction to Section User-defined enumeration strategies.

 procedure parallel_heur
  declarations
   ALLP, ALLM: set of integer        ! Copies of sets PERS and MACH
   HProd: integer                    ! Total productivity value
   pmax,omax,mmax: integer
  end-declarations

 ! Copy the sets of workers and machines
  forall(p in PERS) ALLP+={p}
  forall(m in MACH) ALLM+={m}

 ! Assign workers to machines as long as there are unassigned persons
  while (ALLP<>{}) do
   pmax:=0; mmax:=0; omax:=0

 ! Find the highest productivity among the remaining workers and machines
   forall(p in ALLP, m in ALLM)
    if OUTP(p,m) > omax then
     omax:=OUTP(p,m)
     pmax:=p; mmax:=m
    end-if

   assign(pmax) = mmax               ! Assign chosen machine to person pmax
   ALLP-={pmax}; ALLM-={mmax}        ! Remove person and machine from sets
  end-do

  writeln("Heuristic solution (parallel assignment):")
  forall(p in PERS)
   writeln("  ",p, " operates machine ", getval(assign(p)),
           " (",getval(output(p)), ")")
  writeln("  Total productivity: ", getval(totalProd))
 end-procedure

The model is completed with a procedure for printing out the solution in a properly formatted way.

 procedure print_sol(text1,text2:string, objval:integer)
  writeln(text1,":")
  forall(p in PERS)
   writeln("  ",p, " operates machine ", getsol(assign(p)),
           " (",getsol(output(p)), ")")
  writeln("  ", text2, " productivity: ", objval)
 end-procedure

end-model

User search

Instead of using predefined variable and value selection criteria as shown in all previous model implementations we may choose to define our own search heuristics. We now show how to implement the search strategies from the previous model `by hand.' In our implementation we add each time a second criterion for breaking ties in cases where the main criterion applies to several variables at a time.

Variable selection: the variable selection function has a fixed format—it receives as its argument a list of variables of type cpvarlist and it returns an integer, the list index of the chosen branching variable. The list of variables passed into the function may contain variables that are already instantiated. As a first step in our implementation we, therefore, determine the set Vset of yet uninstantiated variables—or to be more precise, the set of their indices in the list. The entries of the list of variables are accessed with the function getvar. Among the uninstantiated variables we calculate the set of variables Iset with the largest upper bound value (using function getub). Finally, among these variables, we choose the one with the smallest second-largest value (this corresponds to a maximum regret strategy). Predecessor (next-smallest) values in the domain of a decision variable are obtained with the function getprev. Inversely, we have function getnext for an enumeration of domain values in ascending order. The chosen index value is assigned to returned as the function's return value.

 function varchoice(Vars: cpvarlist): integer
  declarations
   Vset,Iset: set of integer
  end-declarations

 ! Set of uninstantiated variables
  forall(i in 1..getsize(Vars))
   if not is_fixed(getvar(Vars,i)) then Vset+= {i}; end-if

  if Vset={} then
   returned:= 0
  else
  ! Get the variable(s) with largest upper bound
   dmax:= max(i in Vset) getub(getvar(Vars,i))
   forall(i in Vset)
    if getub(getvar(Vars,i)) = dmax then Iset+= {i}; end-if
   dmin:= dmax

  ! Choose variable with smallest next-best value among those indexed by 'Iset'
   forall(i in Iset) do
    prev:= getprev(getvar(Vars,i),dmax)
    if prev < dmin then
     returned:= i
     dmin:= prev
    end-if
   end-do
  end-if
 end-function

The variable selection strategy varchoicemin for the second optimization run (serial machines) is implemented in a similar way. We first establish the set of variables with the smallest lower bound value (using getlb), Iset; among these we choose the variable with the smallest upper bound (getub).

 function varchoicemin(Vars: cpvarlist): integer
  declarations
   Vset,Iset: set of integer
  end-declarations

 ! Set of uninstantiated variables
  forall(i in 1..getsize(Vars))
   if not is_fixed(getvar(Vars,i)) then Vset+= {i}; end-if

  if Vset={} then
   returned:= 0
  else
  ! Get the variable(s) with smallest lower bound
   dmin:= min(i in Vset) getlb(getvar(Vars,i))
   forall(i in Vset)
    if getlb(getvar(Vars,i)) = dmin then Iset+= {i}; end-if

  ! Choose variable with smallest upper bound among those indexed by 'Iset'
   dmax:= getparam("kalis_default_ub")
   forall(i in Iset)
    if getub(getvar(Vars,i)) < dmax then
     returned:= i
     dmax:= getub(getvar(Vars,i))
    end-if
  end-if
 end-function

Value selection: the value selection function receives as its argument the chosen branching variable and returns a branching value for this variable. The value selection criterion we have chosen (corresponding to KALIS_MAX_TO_MIN) is to enumerate all values for the branching variable, starting with the largest remaining one (that is, the variable's upper bound):

 function valchoice(x: cpvar): integer
  returned:= getub(x)
 end-function

Notice that with an assign_var or assign_and_forbid strategy, the user's value selection strategy should make sure to return a value that is currently in the branching variable's domain (a value chosen between the lower and upper bound is not guaranteed to lie in the domain) by using function contains.

Setting user search strategies: to indicate that we wish to use our own variable or value selection strategy we simply need to replace the predefined constants by the name of our Mosel functions:

Strategy:= assign_var("varchoice", "valchoice", output)

defines the strategy for the first optimization run and

Strategy:= assign_var("varchoicemin", "valchoice", output)

re-defines it for the serial machine case. Since our function valchoice does just the same as the KALIS_MAX_TO_MIN criterion, we could also combine it with our variable choice function:

Strategy:= assign_var("varchoicemin", KALIS_MAX_TO_MIN, output)

Results

The following table summarizes the results found with the different solution methods for the two problems of parallel and serial machines. There is a notable difference between the heuristic method and the exact solution to the problem with parallel machines.

Table 4.2: Optimal assignments for different model versions
Person
Alg. 1 2 3 4 5 6 Productivity
Parallel Machines Heur. 4 (19)  1 (18)  6 (33)  2 (26)  3 (34)  5 (45) 175
Exact 3 (31) 5 (43) 4 (25) 6 (30) 1 (28) 2 (36) 193
Serial Machines Exact 5 (40) 3 (30) 6 (33) 2 (26) 1 (28) 4 (27) 26

By adding output of solver statistics to our model (cp_show_stats) we find that our user search strategies result in the same search trees and program execution durations as with the predefined strategies for the parallel assignment and arrive at a slightly different solution for the serial case.

© 2001-2019 Fair Isaac Corporation. All rights reserved. This documentation is the property of Fair Isaac Corporation (“FICO”). Receipt or possession of this documentation does not convey rights to disclose, reproduce, make derivative works, use, or allow others to use it except solely for internal evaluation purposes to determine whether to purchase a license to the software described in this documentation, or as otherwise set forth in a written software license agreement between you and FICO (or a FICO affiliate). Use of this documentation and the software described in it must conform strictly to the foregoing permitted uses, and no other use is permitted.