Initializing help system before first use

Scheduling

Topics covered in this chapter:

This chapter shows how to

  • define and setup the modeling objects cptask and cpresource,
  • formulate and solve scheduling problems using these objects,
  • access information from the modeling objects.
  • parameterize search strategies involving the scheduling objects,

Tasks and resources

Scheduling and planning problems are concerned with determining a plan for the execution of a given set of tasks. The objective may be to generate a feasible schedule that satisfies the given constraints (such as sequence of tasks or limited resource availability) or to optimize a given criterion such as the makespan of the schedule.

Xpress Kalis defines several aggregate modeling objects to simplify the formulation of standard scheduling problems: tasks (processing operations, activities) are represented in Mosel by the type cptask and resources (machines, raw material etc.) by the type cpresource. When working with these scheduling objects it is often sufficient to state the objects and their properties, such as task duration or resource use; the necessary constraint relations are set up automatically by Xpress Kalis (referred to as implicit constraints). In the following sections we show a number of examples using this mechanism:

If the enumeration is started with the function cp_schedule the solver will employ specialized search strategies suitable for the corresponding (scheduling) problem type. It is possible to parameterize these strategies or to define user search strategies for scheduling objects (see Section Enumeration). Alternatively, the standard optimization functions cp_minimize / cp_maximize may be used. In this case the enumeration does not exploit the structural information provided by the scheduling objects and works simply with decision variables.

The properties of scheduling objects (such as start time or duration of tasks) can be accessed and employed, for instance, in the definition of constraints, thus giving the user the possibility to extend the predefined standard problems with other types of constraints. For even greater flexibility Xpress Kalis also enables the user to formulate his scheduling problems without the aggregate modeling objects, using dedicated global constraints on decision variables of type cpvar. Most examples in this chapter are therefore given with two different implementations, one using the scheduling objects and another without these objects.

Precedences

Probably the most basic type of a scheduling problem is to plan a set of tasks that are linked by precedence constraints.

The problem described in this section is taken from Section 7.1 `Construction of a stadium' of the book `Applications of optimization with Xpress-MP'

A construction company has been awarded a contract to construct a stadium and wishes to complete it within the shortest possible time. Table Data for stadium construction lists the major tasks and their durations in weeks. Some tasks can only start after the completion of certain other tasks, equally indicated in the table.

Table 5.1: Data for stadium construction
Task Description Duration Predecessors
1 Installing the construction site 2 none
2 Terracing 16 1
3 Constructing the foundations 9 2
4 Access roads and other networks 8 2
5 Erecting the basement 10 3
6 Main floor 6 4,5
7 Dividing up the changing rooms 2 4
8 Electrifying the terraces 2 6
9 Constructing the roof 9 4,6
10 Lighting of the stadium 5 4
11 Installing the terraces 3 6
12 Sealing the roof 2 9
13 Finishing the changing rooms 1 7
14 Constructing the ticket office 7 2
15 Secondary access roads 4 4,14
16 Means of signalling 3 8,11,14
17 Lawn and sport accessories 9 12
18 Handing over the building 1 17

Model formulation

This problem is a classical project scheduling problem. We add a fictitious task with 0 duration that corresponds to the end of the project. We thus consider the set of tasks TASKS = {1,...,N} where N is the fictitious end task.

Every construction task j (j ∈ TASKS) is represented by a task object taskj with variable start time taskj.start and a duration fixed to the given value DURj. The precedences between tasks are represented by a precedence graph with arcs (i,j) symbolizing that task i precedes task j.

The objective is to minimize the completion time of the project, that is the start time of the last, fictitious task N. We thus obtain the following model where an upper bound HORIZON on the start times is given by the sum of all task durations:

tasks taskj (j ∈ TASKS)
minimize taskN.start
∀ j ∈ TASKS: taskj.start ∈ {0,...,HORIZON}
∀ j ∈ TASKS: taskj.duration = DURj
∀ j ∈ TASKS: taskj.predecessors = i ∈ TASKS, s.t. ∃ARCij {taski}

Implementation

The following Mosel model shows the implementation of this problem with Xpress Kalis. Since there are no side-constraints, the earliest possible completion time of the schedule is the earliest start of the fictitious task N. To trigger the propagation of task-related constraints we call the function cp_propagate followed by a call to cp_shave that performs some additional tests to remove infeasible values from the task variables' domains. At this point, constraining the start of the fictitious end task to its lower bound reduces all task start times to their feasible intervals through the effect of constraint propagation. The start times of tasks on the critical path are fixed to a single value. The subsequent call to minimization only serves for instantiating all variables with a single value so as to enable the graphical representation of the solution.

model "B-1 Stadium construction (CP)"
 uses "kalis"

 declarations
  N = 19                              ! Number of tasks in the project
                                      ! (last = fictitious end task)
  TASKS = 1..N
  ARC: dynamic array(range,range) of integer  ! Matrix of the adjacency graph
  DUR: array(TASKS) of integer        ! Duration of tasks
  HORIZON : integer                   ! Time horizon

  task: array(TASKS) of cptask        ! Tasks to be planned
  bestend: integer
 end-declarations

 initializations from 'Data/b1stadium.dat'
  DUR ARC
 end-initializations

 HORIZON:= sum(j in TASKS) DUR(j)

! Setting up the tasks
 forall(j in TASKS) do
  setdomain(getstart(task(j)), 0, HORIZON)   ! Time horizon for start times
  set_task_attributes(task(j), DUR(j))       ! Duration
  setsuccessors(task(j), union(i in TASKS | exists(ARC(j,i))) {task(i)})
 end-do                                      ! Precedences

 if not cp_propagate or not cp_shave then
  writeln("Problem is infeasible")
  exit(1)
 end-if

! Since there are no side-constraints, the earliest possible completion
! time is the earliest start of the fictitious task N
 bestend:= getlb(getstart(task(N)))
 getstart(task(N)) <= bestend
 writeln("Earliest possible completion time: ", bestend)

! For tasks on the critical path the start/completion times have been fixed
! by setting the bound on the last task. For all other tasks the range of
! possible start/completion times gets displayed.
 forall(j in TASKS) writeln(j, ": ", getstart(task(j)))

! Complete enumeration: schedule every task at the earliest possible date
 if cp_minimize(getstart(task(N))) then
  writeln("Solution after optimization: ")
  forall(j in TASKS) writeln(j, ": ", getsol(getstart(task(j))))
 end-if
end-model

Instead of indicating the predecessors of a task, we may just as well state the precedence constraints by indicating the sets of successors for every task:

setsuccessors(task(j), union(i in TASKS | exists(ARC(j,i))) {task(i)})

Results

The earliest completion time of the stadium construction is 64 weeks.

Alternative formulation without scheduling objects

As for the previous formulation, we work with a set of tasks TASKS = {1,...,N} where N is the fictitious end task. For every task j we introduce a decision variable startj to denote its start time. With DURj the duration of task j, the precedence relation `task i precedes task j' is stated by the constraint

starti + DURi ≤ startj

We therefore obtain the following model for our project scheduling problem:

minimize startN
∀ j ∈ TASKS: startj ∈ {0,...,HORIZON}
∀ i,j ∈ TASKS, ∃ARCij: starti + DURi ≤ startj

The corresponding Mosel model is printed in full below. Notice that we have used explicit posting of the precedence constraints—in the case of an infeasible data instance this may help tracing the cause of the infeasibility.

model "B-1 Stadium construction (CP)"
 uses "kalis"

 declarations
  N = 19                              ! Number of tasks in the project
                                      ! (last = fictitious end task)
  TASKS = 1..N
  ARC: dynamic array(range,range) of integer  ! Matrix of the adjacency graph
  DUR: array(TASKS) of integer        ! Duration of tasks
  HORIZON : integer                   ! Time horizon

  start: array(TASKS) of cpvar        ! Start dates of tasks
  bestend: integer
 end-declarations

 initializations from 'Data/b1stadium.dat'
  DUR ARC
 end-initializations

 HORIZON:= sum(j in TASKS) DUR(j)

 forall(j in TASKS) do
  0 <= start(j); start(j) <= HORIZON
 end-do

! Task i precedes task j
 forall(i, j in TASKS | exists(ARC(i, j))) do
  Prec(i,j):= start(i) + DUR(i) <= start(j)
  if not cp_post(Prec(i,j)) then
   writeln("Posting precedence ", i, "-", j, " failed")
   exit(1)
  end-if
 end-do

! Since there are no side-constraints, the earliest possible completion
! time is the earliest start of the fictitious task N
 bestend:= getlb(start(N))
 start(N) <= bestend
 writeln("Earliest possible completion time: ", bestend)

! For tasks on the critical path the start/completion times have been fixed
! by setting the bound on the last task. For all other tasks the range of
! possible start/completion times gets displayed.
 forall(j in TASKS) writeln(j, ": ", start(j))

end-model

Disjunctive scheduling: unary resources

The problem of sequencing jobs on a single machine described in Section element: Sequencing jobs on a single machine may be represented as a disjunctive scheduling problem using the `task' and `resource' modeling objects.

The reader is reminded that the problem is to schedule the processing of a set of non-preemptive tasks (or jobs) on a single machine. For every task j its release date, duration, and due date are given. The problem is to be solved with three different objectives, minimizing the makespan, the average completion time, or the total tardiness.

Model formulation

The major part of the model formulation consists of the definition of the scheduling objects `tasks' and `resources'.

Every job j (j ∈ JOBS = {1,...,NJ}) is represented by a task object taskj, with a start time taskj.start in {RELj,...,MAXTIME} (where MAXTIME is a sufficiently large value, such as the sum of all release dates and all durations, and RELj the release date of job j) and the task duration taskj.duration fixed to the given processing time DURj. All jobs use the same resource res of unitary capacity. This means that at most one job may be processed at any one time, we thus implicitly state the disjunctions between the jobs.

Another implicit constraint established by the task objects is the relation between the start, duration, and completion time of a job j.

∀ j ∈ JOBS: taskj.end = taskj.start + taskj.duration

Objective 1: The first objective is to minimize the makespan (completion time of the schedule) or, equivalently, to minimize the completion time finish of the last job. The complete model is then given by the following (where MAXTIME is a sufficiently large value, such as the sum of all release dates and all durations):

resource res
tasks taskj (j ∈ JOBS)
minimize finish
finish = maximumj ∈ JOBS(taskj.end)
res.capacity = 1
∀ j ∈ JOBS: taskj.end ∈ {0,...,MAXTIME}
∀ j ∈ JOBS: taskj.start ∈ {RELj,...,MAXTIME}
∀ j ∈ JOBS: taskj.duration = DURj
∀ j ∈ JOBS: taskj.requirementres = 1

Objective 2: The formulation of the second objective (minimizing the average processing time or, equivalently, minimizing the sum of the job completion times) remains unchanged from the first model—we introduce an additional variable totComp representing the sum of the completion times of all jobs.

minimize totComp
totComp =
j ∈ JOBS
taskj.end

Objective 3: To formulate the objective of minimizing the total tardiness, we introduce new variables latej to measure the amount of time that a job finishes after its due date. The value of these variables corresponds to the difference between the completion time of a job j and its due date DUEj. If the job finishes before its due date, the value must be zero. The objective now is to minimize the sum of these tardiness variables:

minimize totLate
totLate =
j ∈ JOBS
latej
∀ j ∈ JOBS: latej ∈ {0,...,MAXTIME}
∀ j ∈ JOBS: latej ≥ taskj.end - DUEj

Implementation

The following Mosel implementation with kalis (file b4seq3_ka.mos) shows how to set up the necessary task and resource modeling objects. The resource capacity is set with procedure set_resource_attributes (the resource is of the type KALIS_UNARY_RESOURCE meaning that it processes at most one task at a time), for the tasks we use the procedure set_task_attributes. The latter exists in several overloaded versions for different combinations of arguments (task attributes)—the reader is referred to the Xpress Kalis Mosel Reference Manual for further detail.

For the formulation of the maximum constraint we use an (auxiliary) list of variables: kalis does not allow the user to employ the access functions to modeling objects (getstart, getduration, etc.) in set expressions such as union(j in JOBS) {getend(task(j))}.

model "B-4 Sequencing (CP)"
 uses "kalis"

 forward procedure print_sol
 forward procedure print_sol3

 declarations
  NJ = 7                          ! Number of jobs
  JOBS=1..NJ

  REL: array(JOBS) of integer     ! Release dates of jobs
  DUR: array(JOBS) of integer     ! Durations of jobs
  DUE: array(JOBS) of integer     ! Due dates of jobs

  task: array(JOBS) of cptask     ! Tasks (jobs to be scheduled)
  res: cpresource                 ! Resource (machine)

  finish: cpvar                   ! Completion time of the entire schedule
 end-declarations

 initializations from 'Data/b4seq.dat'
  DUR REL DUE
 end-initializations

! Setting up the resource (formulation of the disjunction of tasks)
 set_resource_attributes(res, KALIS_UNARY_RESOURCE, 1)

! Setting up the tasks (durations and disjunctions)
 forall(j in JOBS) set_task_attributes(task(j), DUR(j), res)

 MAXTIME:= max(j in JOBS) REL(j) + sum(j in JOBS) DUR(j)

 forall(j in JOBS) do
  0 <= getstart(task(j)); getstart(task(j)) <= MAXTIME
  0 <= getend(task(j)); getend(task(j)) <= MAXTIME
 end-do

! Start times
 forall(j in JOBS) getstart(task(j)) >= REL(j)

!**** Objective function 1: minimize latest completion time ****
 declarations
  L: cpvarlist
 end-declarations

 forall(j in JOBS) L += getend(task(j))
 finish = maximum(L)

 if cp_schedule(finish) >0 then
  print_sol
 end-if


!**** Objective function 2: minimize average completion time ****
 declarations
  totComp: cpvar
 end-declarations

 totComp = sum(j in JOBS) getend(task(j))

 if cp_schedule(totComp) > 0 then
  print_sol
 end-if


!**** Objective function 3: minimize total tardiness ****
 declarations
  late: array(JOBS) of cpvar      ! Lateness of jobs
  totLate: cpvar
 end-declarations

 forall(j in JOBS) do
  0 <= late(j); late(j) <= MAXTIME
 end-do

! Late jobs: completion time exceeds the due date
 forall(j in JOBS) late(j) >= getend(task(j)) - DUE(j)

 totLate = sum(j in JOBS) late(j)
 if cp_schedule(totLate) > 0 then
  writeln("Tardiness: ", getsol(totLate))
  print_sol
  print_sol3
 end-if


!-----------------------------------------------------------------

! Solution printing
 procedure print_sol
  writeln("Completion time: ", getsol(finish) ,
          "  average: ", getsol(sum(j in JOBS) getend(task(j))))
  write("Rel\t")
  forall(j in JOBS) write(strfmt(REL(j),4))
  write("\nDur\t")
  forall(j in JOBS) write(strfmt(DUR(j),4))
  write("\nStart\t")
  forall(j in JOBS) write(strfmt(getsol(getstart(task(j))),4))
  write("\nEnd\t")
  forall(j in JOBS) write(strfmt(getsol(getend(task(j))),4))
  writeln
 end-procedure

 procedure print_sol3
  write("Due\t")
  forall(j in JOBS) write(strfmt(DUE(j),4))
  write("\nLate\t")
  forall(j in JOBS) write(strfmt(getsol(late(j)),4))
  writeln
 end-procedure

end-model

Results

This model produces similar results as those reported for the model versions in Section element: Sequencing jobs on a single machine. Figure Solution Gantt chart shows a Gantt chart display of the solution. Above the Gantt chart we can see the resource usage display: the machine is used without interruption by the tasks, that is, even if we relaxed the constraints given by the release times and due dates it would not have been possible to generate a schedule terminating earlier.

KalisUG/ivescheddisj.png

Figure 5.1: Solution Gantt chart

Cumulative scheduling: discrete resources

The problem described in this section is taken from Section 9.4 `Backing up files' of the book `Applications of optimization with Xpress-MP'

Our task is to save sixteen files of the following sizes: 46kb, 55kb, 62kb, 87kb, 108kb, 114kb, 137kb, 164kb, 253kb, 364kb, 372kb, 388kb, 406kb, 432kb, 461kb, and 851kb onto empty disks of 1.44Mb capacity. How should the files be distributed in order to minimize the number of floppy disks used?

Model formulation

This problem belongs to the class of binpacking problems. We show here how it may be formulated and solved as a cumulative scheduling problem, where the disks are the resource and the files the tasks to be scheduled.

The floppy disks may be represented as a single, discrete resource disks, where every time unit stands for one disk. The resource capacity corresponds to the disk capacity.

We represent every file f (f ∈ FILES) by a task object filef, with a fixed duration of 1 unit and a resource requirement that corresponds to the given size SIZEf of the file. The `start' field of the task then indicates the choice of the disk for saving this file.

The objective is to minimize the number of disks that are used, which corresponds to minimizing the largest value taken by the `start' fields of the tasks (that is, the number of the disk used for saving a file). We thus have the following model.

resource disks
tasks filesf (f ∈ FILES)
minimize diskuse
diskuse = maximumf ∈ FILES(filef.start)
disks.capacity = CAP
∀ f ∈ FILES: filef.start ≥ 1
∀ f ∈ FILES: filef.duration = 1
∀ f ∈ FILES: filef.requirementdisks = SIZEf

Implementation

The Mosel implementation with Xpress Kalis is quite straightforward. We define a resource of the type KALIS_DISCRETE_RESOURCE, indicating its total capacity. The definition of the tasks is similar to what we have seen in the previous example.

model "D-4 Bin packing (CP)"
 uses "kalis"

 declarations
  ND: integer                        ! Number of floppy disks
  FILES = 1..16                      ! Set of files
  DISKS: range                       ! Set of disks

  CAP:  integer                      ! Floppy disk size
  SIZE: array(FILES) of integer      ! Size of files to be saved

  file: array(FILES) of cptask       ! Tasks (= files to be saved)
  disks: cpresource                  ! Resource representing disks
  L: cpvarlist
  diskuse: cpvar                     ! Number of disks used
 end-declarations

 initializations from 'Data/d4backup.dat'
  CAP SIZE
 end-initializations

! Provide a sufficiently large number of disks
 ND:= ceil((sum(f in FILES) SIZE(f))/CAP)
 DISKS:= 1..ND

! Setting up the resource (capacity limit of disks)
 set_resource_attributes(disks, KALIS_DISCRETE_RESOURCE, CAP)

! Setting up the tasks
 forall(f in FILES) do
  setdomain(getstart(file(f)), DISKS)           ! Start time (= choice of disk)
  set_task_attributes(file(f), disks, SIZE(f))  ! Resource (disk space) req.
  set_task_attributes(file(f), 1)    ! Duration (= number of disks used)
 end-do

! Limit the number of disks used
 forall(f in FILES) L += getstart(file(f))
 diskuse = maximum(L)

! Minimize the total number of disks used
 if cp_schedule(diskuse) = 0 then
  writeln("Problem infeasible")
 end-if

! Solution printing
 writeln("Number of disks used: ", getsol(diskuse))
 forall(d in 1..getsol(diskuse)) do
  write(d, ":")
  forall(f in FILES | getsol(file(f).start)=d) write(" ",SIZE(f))
  writeln("  space used: ",
          sum(f in FILES | getsol(file(f).start)=d) SIZE(f))
 end-do
 cp_show_stats

end-model

Results

Running the model results in the solution shown in Table Distribution of files to disks, that is, 3 disks are needed for backing up all the files.

Table 5.2: Distribution of files to disks
Disk File sizes (in kb) Used space (in Mb)
1 46 87 137 164 253 364 388 1.439
2 55 62 108 372 406 432 1.435
3 114 461 851 1.426

Alternative formulation without scheduling objects

Instead of defining task and resource objects it is also possible to formulate this problem with a `cumulative' constraint over standard finite domain variables that represent the different attributes of tasks without being grouped into a predefined object. A single `cumulative' constraint expresses the problem of scheduling a set of tasks on one discrete resource by establishing the following relations between its arguments (five arrays of decision variables for the properties related to tasks—start, duration, end, resource use and size— all indexed by the same set R and a constant or time-indexed resource capacity):

∀ j ∈ R: startj + durationj = endj
∀ j ∈ R: usej · durationj = sizej
∀ t ∈ TIMES:
j ∈ R | t ∈ [UB(startj)..LB(endj)]
usej ≤ CAPt

where UB stands for `upper bound' and LB for `lower bound' of a decision variable.

Let savef denote the disk used for saving a file f and usef the space used by the file (f ∈ FILES). As with scheduling objects, the `start' property of a task corresponds to the disk chosen for saving the file, and the resource requirement of a task is the file size. Since we want to save every file onto a single disk, the `duration' durf is fixed to 1. The remaining task properties `end' and `size' (ef and sf) that need to be provided in the formulation of `cumulative' constraints are not really required for our problem; their values are determined by the other three properties.

Implementation

The following Mosel model implements the second model version using the cumulative constraint.

model "D-4 Bin packing (CP)"
 uses "kalis", "mmsystem"

 setparam("kalis_default_lb", 0)

 declarations
  ND: integer                        ! Number of floppy disks
  FILES = 1..16                      ! Set of files
  DISKS: range                       ! Set of disks

  CAP:  integer                      ! Floppy disk size
  SIZE: array(FILES) of integer      ! Size of files to be saved

  save: array(FILES) of cpvar       ! Disk a file is saved on
  use: array(FILES) of cpvar        ! Space used by file on disk
  dur,e,s: array(FILES) of cpvar    ! Auxiliary arrays for 'cumulative'
  diskuse: cpvar                    ! Number of disks used

  Strategy: array(FILES) of cpbranching ! Enumeration
  FSORT: array(FILES) of integer
 end-declarations

 initializations from 'Data/d4backup.dat'
  CAP SIZE
 end-initializations

! Provide a sufficiently large number of disks
 ND:= ceil((sum(f in FILES) SIZE(f))/CAP)
 DISKS:= 1..ND
 finalize(DISKS)

! Limit the number of disks used
 diskuse = maximum(save)

 forall(f in FILES) do
  setdomain(save(f), DISKS)         ! Every file onto a single disk
  use(f) = SIZE(f)
  dur(f) = 1
 end-do

! Capacity limit of disks
 cumulative(save, dur, e, use, s, CAP)

! Definition of search (place largest files first)
 qsort(SYS_DOWN, SIZE, FSORT)       ! Sort files in decreasing order of size
 forall(f in FILES)
  Strategy(f):= assign_var(KALIS_SMALLEST_MIN, KALIS_MIN_TO_MAX, {save(FSORT(f))})
 cp_set_branching(Strategy)

! Minimize the total number of disks used
 if not cp_minimize(diskuse) then
  writeln("Problem infeasible")
 end-if

! Solution printing
 writeln("Number of disks used: ", getsol(diskuse))
 forall(d in 1..getsol(diskuse)) do
  write(d, ":")
  forall(f in FILES | getsol(save(f))=d) write(" ",SIZE(f))
  writeln("  space used: ", sum(f in FILES | getsol(save(f))=d) SIZE(f))
 end-do

end-model

The solution produced by the execution of this model has the same objective function value, but the distribution of the files to the disks is not exactly the same: this problem has several different optimal solutions, in particular those that may be obtained be interchanging the order numbers of the disks. To shorten the search in such a case it may be useful to add some symmetry breaking constraints that reduce the size of the search space by removing a part of the feasible solutions. In the present example we may, for instance, assign the biggest file to the first disk and the second largest to one of the first two disks, and so on, until we reach a lower bound on the number of disks required (a save lower bound estimate is given by rounding up to the next larger integer the sum of files sizes divided by the disk capacity).

Renewable and non-renewable resources

Besides the distinction `disjunctive–cumulative' or `unary–discrete' that we have encountered in the previous sections there are other ways of describing or classifying resources. Another important property is the concept of renewable versus non-renewable resources. The previous examples have shown instances of renewable resources (machine capacity, manpower etc.): the amount of resource used by a task is released at its completion and becomes available for other tasks. In the case of non-renewable resources (e.g. money, raw material, intermediate products), the tasks using the resource consume it, that is, the available quantity of the resource is diminuished by the amount used up by processing a task.

Instead of using resources tasks may also produce certain quantities of resource. Again, we may have tasks that provide an amount of resource during their execution (renewable resources) or tasks that add the result of their production to a stock of resource (non-renewable resources).

Let us now see how to formulate the following problem: we wish to schedule five jobs P1 to P5 representing two stages of a production process. P1 and P2 produce an intermediate product that is needed by the jobs of the final stage (P3 to P5). For every job we are given its minimum and maximum duration, its cost or, for the jobs of the final stage, its profit contribution. There may be two cases, namely model A: the jobs of the first stage produce a given quantity of intermediate product (such as electricity, heat, steam) at every point of time during their execution, this intermediate product is consumed immediately by the jobs of the final stage. Model B: the intermediate product results as ouput from the jobs of the first stage and is required as input to start the jobs of the final stage. The intermediate product in model A is a renewable resource and in model B we have the case of a non-renewable resource.

Model formulation

Let FIRST = {P1,P2} be the set of jobs in the first stage, FINAL = {P3,P4,P5} the jobs of the second stage, and the set JOBS the union of all jobs. For every job j we are given its minimum and maximum duration MINDj and MAXDj respectively. RESAMTj is the amount of resource needed as input or resulting as output from a job. Furthermore we have a cost COSTj for jobs j of the first stage and a profit PROFITj for jobs j of the final stage.

Model A (renewable resource)

The case of a renewable resource is formulated by the following model. Notice that the resource capacity is set to 0 indicating that the only available quantities of resource are those produced by the jobs of the first production stage.

resource res
tasks taskj (j ∈ JOBS)
maximize
j ∈ JOBS
(PROFITj-COSTj)×taskj.duration
res.capacity = 0
∀ j ∈ JOBS: taskj.start,taskj.end ∈ {0,...,HORIZON}
∀ j ∈ JOBS: taskj.duration ∈ {MINDj,...,MAXDj}
∀ j ∈ FIRST: taskj.provisionres = RESAMTj
∀ j ∈ FINAL: taskj.requirementres = RESAMTj

Model B (non-renewable resource)

In analogy to the model A we formulate the second case as follows.

resource res
tasks taskj (j ∈ JOBS)
maximize
j ∈ JOBS
(PROFITj-COSTj)×taskj.duration
res.capacity = 0
∀ j ∈ JOBS: taskj.start,taskj.end ∈ {0,...,HORIZON}
∀ j ∈ JOBS: taskj.duration ∈ {MINDj,...,MAXDj}
∀ j ∈ FIRST: taskj.productionres = RESAMTj
∀ j ∈ FINAL: taskj.consumptionres = RESAMTj

However, this model does not entirely correspond to the problem description above since the production of the intermediate product occurs at the start of a task. To remedy this problem we may introduce an auxiliary task Endj for every job j in the first stage. The auxiliary job has duration 0, the same completion time as the original job and produces the intermediate product in the place of the original job.

∀ j ∈ FIRST: taskEndj.end = taskj.end
∀ j ∈ FIRST: taskEndj.duration = 0
∀ j ∈ FIRST: taskj.productionres = 0
∀ j ∈ FIRST: taskEndj.productionres = RESAMTj

Implementation

The following Mosel model implements case A. We use the default scheduling solver (function cp_schedule) indicating by the value true for the optional second argument that we wish to maximize the objective function.

model "Renewable resource"
 uses "kalis", "mmsystem"

 forward procedure solution_found

 declarations
  FIRST = {'P1','P2'}
  FINAL = {'P3','P4','P5'}
  JOBS = FIRST+FINAL

  MIND,MAXD: array(JOBS) of integer  ! Limits on job durations
  RESAMT: array(JOBS) of integer  ! Resource use/production
  HORIZON: integer                ! Time horizon
  PROFIT: array(FINAL) of real    ! Profit from production
  COST: array(JOBS) of real       ! Cost of production
  CAP: integer                    ! Available resource quantity

  totalProfit: cpfloatvar
  task: array(JOBS) of cptask     ! Task objects for jobs
  intermProd: cpresource          ! Non-renewable resource (intermediate prod.)
 end-declarations

 initializations from 'Data/renewa.dat'
  [MIND,MAXD] as 'DUR' RESAMT HORIZON PROFIT COST CAP
 end-initializations

! Setting up resources
 set_resource_attributes(intermProd, KALIS_DISCRETE_RESOURCE, CAP)
 setname(intermProd, "IntP")

! Setting up the tasks
 forall(j in JOBS) do
  setname(task(j), j)
  setduration(task(j), MIND(j), MAXD(j))
  setdomain(getend(task(j)), 0, HORIZON)
 end-do

! Providing tasks
 forall(j in FIRST) provides(task(j), RESAMT(j), intermProd)

! Requiring tasks
 forall(j in FINAL) requires(task(j), RESAMT(j), intermProd)

! Objective function: total profit
 totalProfit = sum(j in FINAL) PROFIT(j)*getduration(task(j)) -
               sum(j in JOBS) COST(j)*getduration(task(j))
 cp_set_solution_callback(->solution_found)
 setparam("KALIS_MAX_COMPUTATION_TIME", 30)

! Solve the problem
 starttime:= gettime	
 if cp_schedule(totalProfit,true)=0 then
  exit(1)
 end-if

! Solution printing
 writeln("Total profit: ", getsol(totalProfit))
 writeln("Job\tStart\tEnd\tDuration")
 forall(j in JOBS)
  writeln(j, "\t ", getsol(getstart(task(j))), "\t ", getsol(getend(task(j))),
          "\t ", getsol(getduration(task(j))))

 procedure solution_found
  writeln(gettime-starttime , " sec. Solution found with total profit = ",
          getsol(totalProfit))
  forall(j in JOBS)
   write(j, ": ", getsol(getstart(task(j))), "-", getsol(getend(task(j))),
          "(", getsol(getduration(task(j))), "), ")
  writeln
 end-procedure

end-model

The model for case B adds the two auxiliary tasks (forming the set ENDFIRST) that mark the completion of the jobs in the first stage. The only other difference are the task properties produces and consumes that define the resource constraints. We only repeat the relevant part of the model:

 declarations
  FIRST = {'P1','P2'}
  ENDFIRST = {'EndP1', 'EndP2'}
  FINAL = {'P3','P4','P5'}
  JOBS = FIRST+ENDFIRST+FINAL

  MIND,MAXD: array(JOBS) of integer  ! Limits on job durations
  RESAMT: array(JOBS) of integer  ! Resource use/production
  HORIZON: integer                ! Time horizon
  PROFIT: array(FINAL) of real    ! Profit from production
  COST: array(JOBS) of real       ! Cost of production
  CAP: integer                    ! Available resource quantity

  totalProfit: cpfloatvar
  task: array(JOBS) of cptask     ! Task objects for jobs
  intermProd: cpresource          ! Non-renewable resource (intermediate prod.)
 end-declarations

 initializations from 'Data/renewb.dat'
  [MIND,MAXD] as 'DUR' RESAMT HORIZON PROFIT COST CAP
 end-initializations

! Setting up resources
 set_resource_attributes(intermProd, KALIS_DISCRETE_RESOURCE, CAP)
 setname(intermProd, "IntP")

! Setting up the tasks
 forall(j in JOBS) do
  setname(task(j), j)
  setduration(task(j), MIND(j), MAXD(j))
  setdomain(getend(task(j)), 0, HORIZON)
 end-do

! Production tasks
 forall(j in ENDFIRST) produces(task(j), RESAMT(j), intermProd)
 forall(j in FIRST) getend(task(j)) = getend(task("End"+j))

! Consumer tasks
 forall(j in FINAL) consumes(task(j), RESAMT(j), intermProd)

Results

The behavior of the (default) search and the results of the two models are considerably different. The optimal solution with an objective of 344.9 for case B represented in Figure Solution for case B (resource production and consumption) is proven within a fraction of a second. Finding a good solution for case A takes several seconds on a standard PC; finding the optimal solution (see Figure Solution for case A (resource provision and requirement)) and proving its optimality requires several minutes of running time. The main reason for this poor behavior of the search is our choice of the objective function: the cost-based objective function does not propagate well and therefore does not help with pruning the search tree. A better choice for objective functions in scheduling problems generally are criteria involving the task decision variables (start, duration, or completion time, particularly the latter).

KalisUG/renewasol.png

Figure 5.2: Solution for case A (resource provision and requirement)

KalisUG/renewbsol.png

Figure 5.3: Solution for case B (resource production and consumption)

Alternative formulation without scheduling objects

Instead of defining task and resource objects we may equally formulate this problem with a `producer-consumer' constraint over standard finite domain variables that represent the different attributes of tasks without being grouped into a predefined object. A single `producer-consumer' constraint expresses the problem of scheduling a set of tasks producing or consuming a non-renewable resource by establishing the following relations between its arguments (seven arrays of decision variables for the properties related to tasks—start, duration, end, per unit and cumulated resource production, per unit and cumulated resource consumption— all indexed by the same set R):

∀ j ∈ R: startj + durationj = endj
∀ j ∈ R: producej · durationj = psizej
∀ j ∈ R: consumej · durationj = csizej
∀ t ∈ TIMES:
j ∈ R | t ∈ [UB(startj)..LB(endj)]
(producej - consumej) ≥ 0

where UB stands for `upper bound' and LB for `lower bound' of a decision variable.

Implementation

The following Mosel model implements the second model version using the producer_consumer constraint.

model "Non-renewable resource"
 uses "kalis"

 setparam("KALIS_DEFAULT_LB", 0)

 declarations
  FIRST = {'P1','P2'}
  ENDFIRST = {'EndP1', 'EndP2'}
  FINAL = {'P3','P4','P5'}
  JOBS = FIRST+ENDFIRST+FINAL
  PCJOBS = ENDFIRST+FINAL

  MIND,MAXD: array(JOBS) of integer  ! Limits on job durations
  RESAMT: array(JOBS) of integer  ! Resource use/production
  HORIZON: integer                ! Time horizon
  PROFIT: array(FINAL) of real    ! Profit from production
  COST: array(JOBS) of real       ! Cost of production
  CAP: integer                    ! Available resource quantity

  totalProfit: cpfloatvar
  fstart,fdur,fcomp: array(FIRST) of cpvar! Start, duration & completion of jobs
  start,dur,comp: array(PCJOBS) of cpvar  ! Start, duration & completion of jobs
  produce,consume: array(PCJOBS) of cpvar ! Production/consumption per time unit
  psize,csize: array(PCJOBS) of cpvar     ! Cumulated production/consumption
 end-declarations

 initializations from 'Data/renewb.dat'
  [MIND,MAXD] as 'DUR' RESAMT HORIZON PROFIT COST CAP
 end-initializations

! Setting up the tasks
 forall(j in PCJOBS) do
  setname(start(j), j)
  setdomain(dur(j), MIND(j), MAXD(j))
  setdomain(comp(j), 0, HORIZON)
  start(j) + dur(j) = comp(j)
 end-do
 forall(j in FIRST) do
  setname(fstart(j), j)
  setdomain(fdur(j), MIND(j), MAXD(j))
  setdomain(fcomp(j), 0, HORIZON)
  fstart(j) + fdur(j) = fcomp(j)
 end-do

! Production tasks
 forall(j in ENDFIRST) do
  produce(j) = RESAMT(j)
  consume(j) = 0
 end-do
 forall(j in FIRST) fcomp(j) = comp("End"+j)

! Consumer tasks
 forall(j in FINAL) do
  consume(j) = RESAMT(j)
  produce(j) = 0
 end-do

! Resource constraint
 producer_consumer(start, comp, dur, produce, psize, consume, csize)

! Objective function: total profit
 totalProfit = sum(j in FINAL) PROFIT(j)*dur(j) -
               sum(j in FIRST) COST(j)*fdur(j)

 if not cp_maximize(totalProfit) then
  exit(1)
 end-if

 writeln("Total profit: ", getsol(totalProfit))
 writeln("Job\tStart\tEnd\tDuration")
 forall(j in FIRST)
  writeln(j, "\t ", getsol(fstart(j)), "\t ", getsol(fcomp(j)),
          "\t ", getsol(fdur(j)))
 forall(j in PCJOBS)
  writeln(j, "\t ", getsol(start(j)), "\t ", getsol(comp(j)),
          "\t ", getsol(dur(j)))

end-model

This model generates the same solution as the previous model version with a slightly longer running time (though still just a fraction of a second on a standard PC).

Extensions: setup times, resource choice, usage profiles

Setup times

Consider once more the problem of planning the production of paint batches introduced in Section implies: Paint production. Between the processing of two batches the machine needs to be cleaned. The cleaning (or setup) times incurred are sequence-dependent and asymetric. The objective is to determine a production cycle of the shortest length.

Model formulation

For every job j (j ∈ JOBS = {1,...,NJ}), represented by a task object taskj, we are given its processing duration DURj. We also have a matrix of cleaning times CLEAN with entries CLEANjk indicating the duration of the cleaning operation if task k succedes task j. The machine processing the jobs is modeled as a resource res of unitary capacity, thus stating the disjunctions between the jobs.

With the objective to minimize the makespan (completion time of the last batch) we obtain the following model:

resource res
tasks taskj (j ∈ JOBS)
minimize finish
finish = maximumj ∈ JOBS(taskj.end)
res.capacity = 1
∀ j ∈ JOBS: taskj.duration = DURj
∀ j ∈ JOBS: taskj.requirementres = 1
∀ j,k ∈ JOBS: setup(taskj, taskk) = CLEANjk

The tricky bit in the formulation of the original problem is that we wish to minimize the cycle time, that is, the completion of the last job plus the setup required between the last and the first jobs in the sequence. Since our task-based model does not contain any information about the sequence or rank of the jobs we introduce auxiliary variables firstjob and lastjob for the index values of the jobs in the first and last positions of the production cycle, and a variable cleanlf for the duration of the setup operation between the last and first tasks. The following constraints express the relations between these variables and the task objects:

firstjob, lastjob ∈ JOBS
firstjob ≠ lastjob
∀ j ∈ JOBS: taskj.end = finish ⇔ lastjob = j
∀ j ∈ JOBS: taskj.start = 1 ⇔ firstjob = j
cleanlf = CLEANlastjob,firstjob

Minimizing the cycle time then corresponds to minimizing the sum finish + cleanlf.

Implementation

The following Mosel model implements the task-based model formulated above. The setup times between tasks are set with the procedure setsetuptimes indicating the two task objects and the corresponding duration value.

model "B-5 Paint production (CP)"
 uses "kalis", "mmsystem"

 declarations
  NJ = 5                             ! Number of paint batches (=jobs)
  JOBS=1..NJ

  DUR: array(JOBS) of integer        ! Durations of jobs
  CLEAN: array(JOBS,JOBS) of integer ! Cleaning times between jobs

  task: array(JOBS) of cptask
  res: cpresource

  firstjob,lastjob,cleanlf,finish: cpvar
  L: cpvarlist
  cycleTime: cpvar                   ! Objective variable
 end-declarations

 initializations from 'Data/b5paint.dat'
  DUR CLEAN
 end-initializations

! Setting up the resource (formulation of the disjunction of tasks)
 set_resource_attributes(res, KALIS_UNARY_RESOURCE, 1)

! Setting up the tasks
 forall(j in JOBS) getstart(task(j)) >= 1                     ! Start times
 forall(j in JOBS) set_task_attributes(task(j), DUR(j), res)  ! Dur.s + disj.
 forall(j,k in JOBS) setsetuptime(task(j), task(k), CLEAN(j,k), CLEAN(k,j))
                                     ! Cleaning times between batches

! Cleaning time at end of cycle (between last and first jobs)
 setdomain(firstjob, JOBS); setdomain(lastjob, JOBS)
 firstjob <> lastjob
 forall(j in JOBS) equiv(getend(task(j))=getmakespan, lastjob=j)
 forall(j in JOBS) equiv(getstart(task(j))=1, firstjob=j)
 cleanlf = element(CLEAN, lastjob, firstjob)

 forall(j in JOBS) L += getend(task(j))
 finish = maximum(L)

! Objective: minimize the duration of a production cycle
 cycleTime = finish - 1 + cleanlf

! Solve the problem
 if cp_schedule(cycleTime) = 0 then
  writeln("Problem is infeasible")
  exit(1)
 end-if
 cp_show_stats

! Solution printing
 declarations
  SUCC: array(JOBS) of integer
 end-declarations

 forall(j in JOBS)
  forall(k in JOBS)
   if getsol(getstart(task(k))) = getsol(getend(task(j)))+CLEAN(j,k) then
    SUCC(j):= k
    break
   end-if
 writeln("Minimum cycle time: ", getsol(cycleTime))
 writeln("Sequence of batches:\nBatch Start Duration Cleaning")
 forall(k in JOBS)
  writeln(formattext("  %d%7d%8d%9d", k, getsol(task(k).start), DUR(k),
          if(SUCC(k)>0, CLEAN(k,SUCC(k)), cleanlf.sol)))

end-model

Results

The results are similar to those reported in Section implies: Paint production. It should be noted here that this model formulation is less efficient, in terms of search nodes and running times, than the previous model versions, and in particular the 'cycle' constraint version presented in Section cycle: Paint production. However, the task-based formulation is more generic and easier to extend with additional features than the problem-specific formulations in the previous model versions.

The graphical representation of the results looks as follows (Figure Solution graph).

KalisUG/iveschedsetup.png

Figure 5.4: Solution graph

Alternative resources

Quite frequently in scheduling applications the assignment of tasks to resources is not entirely fixed from the outset. For instance, there may be a group of resources (identical or not) among which to choose for a given processing step. If the resources are identical the task will have the same properties (duration, amount of resource use/provision/consumption/production) independent of the resource used for its processing. Otherwise, if resources are non-identical there is some resource-dependent data component.

We shall show here how to formulate a tiny example of four tasks that may be processed by two non-identical machines. Each task must be assigned to a single machine. The resource usage per task depends on the machine it is assigned to.

Model formulation

For each task j in the set of tasks TASKS we are given a fixed duration DURj and a machine-dependent amount of resource REQjm that is required per time unit for the whole duration of the task. Both machines have the same capacity CAP.

The resulting model looks as follows.

resource resm (m ∈ MACH)
tasks taskj (j ∈ TASKS)
minimize makespan
∀ m ∈ MACH: resm.capacity = CAP
∀ j ∈ TASKS: taskj.start,taskj.end ∈ {0,...,HORIZON}
∀ j ∈ TASKS: taskj.duration = DURt
∀ j ∈ TASKS,m ∈ MACH: taskj.assignmentresm ∈ {0,1}
∀ j ∈ TASKS:
m ∈ MACH
taskj.assignmentresm = 1
∀ j ∈ TASKS,m ∈ MACH: taskj.requirementresm ∈ {0,REQjm}

Implementation

This is a Mosel implementation of the scheduling problem with alternative resources.

 uses "kalis", "mmsystem"

 setparam("KALIS_DEFAULT_LB", 0)

 forward procedure print_solution

 declarations
  TASKS = {"a","b","c","d"}           ! Index set of tasks
  MACH = {"M1", "M2"}                 ! Index set of resources
  USE: array(TASKS,MACH) of integer   ! Machine-dependent res. requirement
  DUR: array(TASKS) of integer        ! Durations of tasks

  task: array(TASKS) of cptask        ! Tasks
  res: array(MACH) of cpresource      ! Resources
 end-declarations

 DUR::(["a","b","c","d"])[7, 9, 8, 5]
 USE::(["a","b","c","d"],["M1","M2"])[
      4, 3,
      2, 3,
      2, 1,
      4, 5]

! Define discrete resources
 forall(m in MACH)
  set_resource_attributes(res(m), KALIS_DISCRETE_RESOURCE, 5)

! Define tasks with machine-dependent resource usages
 forall(j in TASKS) do
  task(j).duration:= DUR(j)
  task(j).name:= j
  requires(task(j), union(m in MACH) {resusage(res(m), USE(j,m))})
 end-do

 cp_set_solution_callback(->print_solution)
 starttime:=timestamp

! Solve the problem
 if cp_schedule(getmakespan)=0 then
  writeln("No solution")
  exit(0)
 end-if

! Solution printing
 forall(j in TASKS)
  writeln(j, ": ", getsol(getstart(task(j))), " - ", getsol(getend(task(j))))
 forall(t in 1..getsol(getmakespan)) do
  write(strfmt(t-1,2), ": ")

  ! We cannot use 'getrequirement' here to access solution information
  ! (it returns a value corresponding to the current state, that is 0)
  forall(j in TASKS | t>getsol(task(j).start) and t<=getsol(getend(task(j))))
   write(formattext("%s:%d  ",j,
         sum(m in MACH) USE(j,m)*getsol(getassignment(task(j),res(m)))) )
  writeln
 end-do

! ****************************************************************

! Print solutions during enumeration at the node where they are found
 procedure print_solution
  writeln(timestamp-starttime, "sec. Solution: ", getsol(getmakespan))

  forall(m in MACH) do
   writeln(m, ":")

   forall(t in 0..getsol(getmakespan)-1) do
    write(strfmt(t,2), ": ")
    forall(j in TASKS | getrequirement(task(j), res(m), t)>0)
     write(j, ":", getrequirement(task(j), res(m), t), "  " )
    writeln("(total ", sum(j in TASKS) getrequirement(task(j), res(m), t), ")")
   end-do

  end-do
 end-procedure

end-model

A new feature introduced by this example are the resusage objects that are employed in the definition of the resource requirement constraints for the tasks. A resusage consists of a resource object and a resource usage (specified by a constant, lower and upper bounds, or a complete profile — see Section Resource profiles). In this example we use the simplest version with a fixed resource usage value. Optional third and forth arguments to requires can be defined to specify the minimum and maximum number of resources to be used by a task. The default version (employed in this example) will choose a single resource from the set of alternative resources.

The implementation also shows how to retrieve solution information about resource assignments at a solution node (that is, during the enumeration) and after the search. At the solution node the function getrequirement returns the desired resource usage information; for a solution summary after terminating the search we need to work with the decision variable obtained through getassignment.

The formulation of produces, consumes, and provides constraints with resusage objects is analogous to what we have shown here for requires.

Note: when implementing alternative resources you always have to use the resource type KALIS_DISCRETE_RESOURCES, setting a capacity limit of 1 for modeling disjunctive resources.

Results

Figure Solutions for scheduling with resource choice shows two solutions generated by the execution of our model. The first solution uses a lesser total amount resource but the second one is optimal with respect to the objective of minimizing the makespan.

KalisUG/altressol1.png KalisUG/altressol2.png

Figure 5.5: Solutions for scheduling with resource choice

Note: leaving the decision about the resource choice open for the solver considerably increases the complexity of a scheduling problem. We therefore recommend to use this feature sparingly. Optimization problems involving resource assignment and sequencing aspects are often more easily solved by a decompostion approach (see the Xpress Whitepaper Hybrid MIP/CP solving with Xpress Optimizer and Xpress Kalis).

Resource profiles

In all preceding examples we have worked with the assumption that a task is defined by a constant, though not necessarily fixed resource requirement (respectively resource provision, consumption, or production). Following this line of thought, a sequence of processesing steps with different resource usages would be represented by a corresponding number of tasks, each with a constant resource usage. However, the number of cptask objects required by such a formulation may be prohibitively large. Kalis therefore offers the possibility to define profiled tasks, that is, tasks with a non-constant resource usage profile. A task resource usage profile states for every time unit of the execution of the task the corresponding amount of resource. For example the four tasks in Figure Task profiles have the profiles Profilea: (12, 12, 6, 2, 2, 2, 2), Profileb: (12, 12, 6, 2, 2, 2, 2, 2, 2), Profilec: (12, 12, 3, 3, 3, 3, 3, 3), and Profiled: (6, 6, 6, 6, 6) respectively.

KalisUG/proftaska.png  KalisUG/proftaskb.png KalisUG/proftaskc.png  KalisUG/proftaskd.png

Figure 5.6: Task profiles

Model formulation

Let TASKS = {'a','b','c','d'} be the set of tasks and res a resource of capacity CAP required by all tasks for their execution. With the objective of minimizing the makespan of the schedule we may formulate the model as follows.

resource res
tasks taskj (j ∈ TASKS)
minimize makespan
res.capacity = CAP
∀ j ∈ TASKS: taskj.start,taskj.end ∈ {0,...,HORIZON}
∀ j ∈ TASKS: taskj.duration = DURj
∀ j ∈ TASKS: taskj.requirementres = Profilej

This model formulation differs from a standard cumulative scheduling model (see Section Cumulative scheduling: discrete resources only in a single point: the resource requirement Profilej of a task j is a list of values and not just a single constant.

Implementation

Let us now take a look at the model formulation with Mosel.

 uses "kalis"

 setparam("KALIS_DEFAULT_LB", 0)

 forward procedure print_solution

 declarations
  TASKS = {"a","b","c","d"}           ! Index set of tasks
  Profile: array(TASKS) of list of integer   ! Task profiles
  DUR: array(TASKS) of integer        ! Durations of tasks

  task: array(TASKS) of cptask        ! Tasks
  res: cpresource                     ! Cumulative resource
 end-declarations

 DUR::(["a","b","c","d"])[7, 9, 8, 5]
 Profile("a"):= [12, 12, 6, 2, 2, 2, 2]
 Profile("b"):= [12, 12, 6, 2, 2, 2, 2, 2, 2]
 Profile("c"):= [12, 12, 3, 3, 3, 3, 3, 3]
 Profile("d"):= [6, 6, 6, 6, 6]

! Define a discrete resource
 set_resource_attributes(res, KALIS_DISCRETE_RESOURCE, 18)
 res.name:="machine"

! Define tasks with profiled resource usage
 forall(t in TASKS) do
  task(t).duration:= DUR(t)
  task(t).name:= t
  requires(task(t), resusage(res, Profile(t)))
 end-do

 cp_set_solution_callback(->print_solution)
 starttime:=timestamp

! Solve the problem
 if cp_schedule(getmakespan)=0 then
  writeln("No solution")
  exit(0)
 end-if

! Solution printing
 writeln("Schedule with makespan ", getsol(getmakespan), ":")
 forall(t in TASKS)
  writeln(t, ": ", getsol(getstart(task(t))), " - ", getsol(getend(task(t))))

! ****************************************************************

! Print solutions during enumeration at the node where they are found.
! 'getrequirement' can only be used here to access solution information,
! not after the enumeration (it returns a value corresponding to the
! current state, that is, 0 after the enumeration).
 procedure print_solution
  writeln(timestamp-starttime, "sec. Solution: ", getsol(getmakespan))

  forall(i in 0..getsol(getmakespan)-1) do
   write(strfmt(i,2), ": ")
   forall(t in TASKS | getrequirement(task(t), res, i)>0)
    write(t, ":", getrequirement(task(t), res, i), "  " )
   writeln("(total ", sum(t in TASKS) getrequirement(task(t), res, i), ")")
  end-do
 end-procedure

end-model

The statement of the resource constraints (requires) uses an additional type of scheduling object for the definition of the resource profiles, namely resusage. We have already encountered this object in Section Alternative resources where it was employed in the definition of sets of alternative resources. In the present case there is only a single resource that must be used by all tasks, we now employ resusage to define resource usage profiles.

Results

The chart in Figure Solution for scheduling with resource profile shows an optimal schedule of the four tasks on a resource with capacity 18. All tasks are completed by time 11.

KalisUG/proftasksol.png

Figure 5.7: Solution for scheduling with resource profile

Resource idle times and preemption of tasks

In the examples of cumulative scheduling we have looked at so far resources are defined with a fixed constant capacity. In reality, it often happens that the resource availability is not constant over time: a resource 'staff', for instance, is likely to show variations corresponding to the presence or absence of personnel. Such changes to the resource level can be defined periodwise with the subroutine setcapacity. In the case of machine resources there may be scheduled downtimes for maintenance or other breaks, such as week-ends, relating to working hours. Any such periods that are known from the outset should be marked as 'unavailable for processing tasks'.

From the perspective of tasks there are two cases of resource unavailability: (1) any task starting before the resource unavailability must also be finished before the begin of this period (generally known as non-preemptive scheduling), and (2) a task started before the unavailability resumes its processing once the resource becomes available again (the case of preemptive scheduling).

With Kalis, the first case applies when resource capacities are set to 0 using setcapacity (independent of the type of the task). The second case is modeled by using profiled tasks and indicating resource unavailability with setidletimes.

For simplicity's sake, we merely consider two tasks, a and b, that are to be scheduled on a resource of capacity 3 that is unavailable in certain time periods. Task a has a constant resource requirement of 1 for 4 units of time, task b has the resource usage profile (1,1,1,2,2,2,1,1). Both tasks are preemptive.

Model formulation

Let TASKS = {'a','b'} be the set of tasks and res a resource of capacity CAP required by the tasks for their execution. The set IDLESET contains the time periods during which the resource is unavailable preempting the processing of tasks. With the objective of minimizing the makespan of the schedule we may formulate the model as follows.

resource res
tasks taskj (j ∈ TASKS)
minimize makespan
res.capacity = CAP
res.idletimes = IDLESET
∀ j ∈ TASKS: taskj.start,taskj.end ∈ {0,...,HORIZON}
∀ j ∈ TASKS: taskj.duration ≥ DURj
∀ j ∈ TASKS: taskj.requirementres = Profilej

Please notice that we do not fix the durations of the tasks in this formulation: we may indicate (lower or upper) bounds to restrict the total duration of a task, but if we want to leave room for preemptions the bounds on the durations of tasks must include some slack.

Implementation

The following model shows how to implement our example problem with Mosel.

model "Preemption"
 uses "kalis", "mmsystem"

 setparam("KALIS_DEFAULT_LB", 0)

 forward procedure print_solution

 declarations
  aprofile,bprofile: list of integer
  a,b: cptask
  res: cpresource
 end-declarations

! Define a discrete resource
 set_resource_attributes(res, KALIS_DISCRETE_RESOURCE, 3)

! Create a 'hole' in the availability of the resource:
! Uncomment one of the following two lines to compare their effect
! setcapacity(res, 1, 2, 0);  setcapacity(res, 7, 8, 0)
 setidletimes(res, (1..2)+(7..8))

! Define two tasks (a: constant resource use, b: resource profile)
 a.duration >= 4
 a.name:= "task_a"
 aprofile:= [1,1,1,1]

 b.duration >= 8
 b.name:= "task_b"
 bprofile:= [1,1,1,2,2,2,1,1]

! Resource usage constraints
 requires(a, resusage(res,aprofile))
 requires(b, resusage(res,bprofile))

 cp_set_solution_callback(->print_solution)

! Solve the problem
 if cp_schedule(getmakespan)<>0 then
  cp_show_sol
 else
  writeln("No solution")
 end-if

! ****************************************************************

 procedure print_solution
  writeln("Solution: ", getsol(getmakespan))
  writeln("Schedule: a: ", getsol(getstart(a)), "-", getsol(getend(a))-1,
          ", b: ", getsol(getstart(b)), "-", getsol(getend(b))-1)
  writeln("Resource usage:")
  forall(t in 0..getsol(getmakespan)-1)
   writeln(formattext("%5d: Cap: %d, a:%d, b:%d", t, getcapacity(res,t),
           getrequirement(a, res, t), getrequirement(b, res, t)) )
 end-procedure

end-model

There are several equivalent ways of stating the constant resource usage of task a. Instead of defining explicitly the complete profile we may simply write

 requires(a, resusage(res,1)) 

or even shorter:

 requires(a, 1, res) 

Results

The model shown above produces the following solution output.

Solution: 12
Schedule: a: 0-5, b: 0-11
Resource usage:
    0: Cap: 3, a:1, b:1
    1: Cap: 3, a:0, b:0
    2: Cap: 3, a:0, b:0
    3: Cap: 3, a:1, b:1
    4: Cap: 3, a:1, b:1
    5: Cap: 3, a:1, b:2
    6: Cap: 3, a:0, b:2
    7: Cap: 3, a:0, b:0
    8: Cap: 3, a:0, b:0
    9: Cap: 3, a:0, b:2
   10: Cap: 3, a:0, b:1
   11: Cap: 3, a:0, b:1

The same model, replacing the line

 setidletimes(res, (1..2)+(7..8)) 

by this version indicating non-preemptive resource unavailability

 setcapacity(res, 1, 2, 0);  setcapacity(res, 7, 8, 0) 

results in the following solution where the tasks are placed in such a way that they do not overlap with any 'hole' in resource availability.

Solution: 17
Schedule: a: 3-6, b: 9-16
Resource usage:
    0: Cap: 3, a:0, b:0
    1: Cap: 0, a:0, b:0
    2: Cap: 0, a:0, b:0
    3: Cap: 3, a:1, b:0
    4: Cap: 3, a:1, b:0
    5: Cap: 3, a:1, b:0
    6: Cap: 3, a:1, b:0
    7: Cap: 0, a:0, b:0
    8: Cap: 0, a:0, b:0
    9: Cap: 3, a:0, b:1
   10: Cap: 3, a:0, b:1
   11: Cap: 3, a:0, b:1
   12: Cap: 3, a:0, b:2
   13: Cap: 3, a:0, b:2
   14: Cap: 3, a:0, b:2
   15: Cap: 3, a:0, b:1
   16: Cap: 3, a:0, b:1
 

It is also possible to define both, preemptive and non-preemptive times of unavailability for the same resource. For example, if times 1-2 correspond to a weekend where products may remain on the machine R and times 7-8 are required for maintenance operations during which the machine must imperatively be empty we would state in our model:

 setidletimes(res, 1, 2, 0)
 setcapacity(res, (7..8)) 

In this case the execution of task a may start at time 0, whereas task b will again be processed in times 9-16.

Enumeration

In the previous scheduling examples we have used the default enumeration for scheduling problems, invoked by the optimization function cp_schedule. The built-in search strategies used by the solver in this case are particularly suited if we wish to minimize the completion time of a schedule. With other objectives the built-in strategies may not be a good choice, especially if the model contains decision variables that are not part of scheduling objects (the built-in strategies always enumerate first the scheduling objects) or if we wish to maximize an objective. kalis makes it therefore possible to use the standard optimization functions cp_minimize and cp_maximize in models that contain scheduling objects, the default search strategies employed by these optimization functions being different from the scheduling-specific ones. In addition, the user may also define his own problem-specific enumeration as shown in the following examples.

User-defined enumeration strategies for scheduling problems may take two different forms: variable-based and task-based. The former case is the subject of Chapter Enumeration, and we only give a small example here (Section Variable-based enumeration). The latter will be explained with some more detail by the means of a job-shop scheduling example.

Variable-based enumeration

When studying the problem solving statistics for the bin packing problem in Section Cumulative scheduling: discrete resources we find that the enumeration using the default scheduling search strategies requires several hundred nodes to prove optimality. With a problem-specific search strategy we may be able to do better! Indeed, we shall see that a greedy-type strategy, assigning the largest files first, to the first disk with sufficient space is clearly more efficient.

Using cp_minimize

The only decisions to make in this problem are the assignments of files to disks, that is, choosing a value for the start time variables of the tasks. The following lines of Mosel code order the tasks in decreasing order of file sizes and define an enumeration strategy for their start times assigning each the smallest possible disk number first. Notice that the sorting subroutine qsort is defined by the module mmsystem that needs to be loaded with a uses statement at the beginning of the model.

 declarations
  Strategy: array(range) of cpbranching
  FSORT: array(FILES) of integer
 end-declarations

 qsort(SYS_DOWN, SIZE, FSORT)       ! Sort files in decreasing order of size
 forall(f in FILES)
  Strategy(f):=assign_var(KALIS_SMALLEST_MIN, KALIS_MIN_TO_MAX,
                          {getstart(file(FSORT(f)))})
 cp_set_branching(Strategy)

 if not cp_minimize(diskuse) then writeln("Problem infeasible"); end-if

The choice of the variable selection criterion (first argument of assign_var) is not really important here since every strategy Strategyf only applies to a single variable and hence no selection takes place. Equivalently, we may have written

 declarations
  Strategy: cpbranching
  FSORT: array(FILES) of integer
  LS: cpvarlist
 end-declarations

 qsort(SYS_DOWN, SIZE, FSORT)       ! Sort files in decreasing order of size
 forall(f in FILES)
  LS+=getstart(file(FSORT(f)))
 Strategy:=assign_var(KALIS_INPUT_ORDER, KALIS_MIN_TO_MAX, LS)
 cp_set_branching(Strategy)

 if not cp_minimize(diskuse) then writeln("Problem infeasible"); end-if 

With this search strategy the first solution found uses 3 disks, that is, we immediately find an optimal solution. The whole search terminates after 17 nodes and takes only a fraction of the time needed by the default scheduling or minimization strategies.

Using cp_schedule

The scheduling search consists of a pretreatment (shaving) phase and two main phases (KALIS_INITIAL_SOLUTION and KALIS_OPTIMAL_SOLUTION) for which the user may specify enumeration strategies by calling cp_set_schedule_search with the corresponding phase selection. The `initial solution' phase aims at providing quickly a good solution whereas the `optimal solution' phase proves optimality. Any search limits such as maximum number of nodes apply separately to each phase, an overall time limit (parameter KALIS_MAX_COMPUTATION_TIME) only applies to the last phase.

The definition of variable-based branching schemes for the scheduling search is done in exactly the same way as what we have seen previously for standard search with cp_minimize or cp_maximize, replacing cp_set_strategy by cp_set_schedule_strategy:

 cp_set_schedule_branching(KALIS_INITIAL_SOLUTION, Strategy)
 if cp_schedule(diskuse)=0 then writeln("Problem infeasible"); end-if

With this search strategy, the optimal solution is found in the 'initial solution' phase after just 8 nodes and the enumeration stops there since the pretreatment phase has proven a lower bound of 3 which is just the value of the optimal solution.

NB: to obtain an output log from the different phases of the scheduling search set the control parameter KALIS_VERBOSE_LEVEL to 2, that is, add the following line to your model before the start of the solution algorithm.

 setparam("KALIS_VERBOSE_LEVEL", 2)

Task-based enumeration

A task-based enumeration strategy consists in the definition of a selection strategy choosing the task to be enumerated, a value selection heuristic for the task durations, and a value selection heuristic for the task start times.

Consider the typical definition of a job-shop scheduling problem: we are given a set of jobs that each need to be processed in a fixed order by a given set of machines. A machine executes one job at a time. The durations of the production tasks (= processing of a job on a machine) and the sequence of machines per job for a 6×6 instance are shown in Table 66 job-shop instance from . The objective is to minimize the makespan (latest completion time) of the schedule.

Table 5.3: 6×6 job-shop instance from [FT63]
Job Machines Durations
1 3 1 2 4 6 5 1 3 6 7 3 6
2 2 3 5 6 1 4 8 5 10 10 10 4
3 3 4 6 1 2 5 5 4 8 9 1 7
4 2 1 3 4 5 6 5 5 5 3 8 9
5 3 2 5 6 1 4 9 3 5 4 3 1
6 2 4 6 1 5 3 3 3 9 10 4 1

Model formulation

Let JOBS denote the set of jobs and MACH (MACH = {1,...,NM}) the set of machines. Every job j is produced as a sequence of tasks taskjm where taskjm needs to be finished before taskj,m+1 can start. A task taskjm is processed by the machine RESjm and has a fixed duration DURjm.

The following model formulates the job-shop scheduling problem.

resources resm (m ∈ MACH = {1,...,NM})
tasks taskjm (j ∈ JOBS, m ∈ MACH)
minimize finish
finish = maximumj ∈ JOBS(taskj,NM.end)
∀ m ∈ MACH: resm.capacity = 1
∀ j ∈ JOBS, m ∈ MACH: taskjm.start, taskjm.end ∈ {0,...,MAXTIME}
∀ j ∈ JOBS, m ∈ {1,...,NM-1}: taskjm.successors = {taskj,m+1}
∀ j ∈ JOBS, m ∈ MACH: taskjm.duration = DURjm
∀ j ∈ JOBS, m ∈ MACH: taskjm.requirementRESjm = 1

Implementation

The following Mosel model implements the job-shop scheduling problem and defines a task-based branching strategy for solving it. We select the task that has the smallest remaining domain for its start variable and enumerate the possible values for this variable starting with the smallest. A task-based branching strategy is defined with the kalis function task_serialize, that takes as arguments the user task selection, value selection strategies for the duration and start variables, and the set of tasks it applies to. Such task-based branching strategies can be combined freely with any variable-based branching strategies.

model "Job shop (CP)"
 uses "kalis", "mmsystem"

 parameters
  DATAFILE = "jobshop.dat"
  NJ = 6                                 ! Number of jobs
  NM = 6                                 ! Number of resources
 end-parameters

 forward function select_task(tlist: cptasklist): integer

 declarations
  JOBS = 1..NJ                           ! Set of jobs
  MACH = 1..NM                           ! Set of resources
  RES: array(JOBS,MACH) of integer       ! Resource use of tasks
  DUR: array(JOBS,MACH) of integer       ! Durations of tasks

  res: array(MACH) of cpresource         ! Resources
  task: array(JOBS,MACH) of cptask       ! Tasks
 end-declarations

 initializations from "Data/"+DATAFILE
  RES DUR
 end-initializations

 HORIZON:= sum(j in JOBS, m in MACH) DUR(j,m)
 forall(j in JOBS) getend(task(j,NM)) <= HORIZON

! Setting up the resources (capacity 1)
 forall(m in MACH) 	
  set_resource_attributes(res(m), KALIS_UNARY_RESOURCE, 1)

! Setting up the tasks (durations, resource used)
 forall(j in JOBS, m in MACH)  	
  set_task_attributes(task(j,m), DUR(j,m), res(RES(j,m)))

! Precedence constraints between the tasks of every job
 forall (j in JOBS, m in 1..NM-1)
!  getstart(task(j,m)) + DUR(j,m) <= getstart(task(j,m+1))
  setsuccessors(task(j,m), {task(j,m+1)})

! Branching strategy
 Strategy:=task_serialize(->select_task, KALIS_MIN_TO_MAX,
            KALIS_MIN_TO_MAX,
            union(j in JOBS, m in MACH | exists(task(j,m))) {task(j,m)})
 cp_set_branching(Strategy)

! Solve the problem
 starttime:= gettime

 if not cp_minimize(getmakespan) then
  writeln("Problem is infeasible")
  exit(1)
 end-if

! Solution printing
 cp_show_stats
 write(gettime-starttime, "sec ")
 writeln("Total completion time: ", getsol(getmakespan))
 forall(j in JOBS) do
  write("Job ", strfmt(j,-2))
  forall(m in MACH | exists(task(j,m)))
   write(formattext("%3d:%3d-%2d", RES(j,m), getsol(getstart(task(j,m))),
         getsol(getend(task(j,m)))))
  writeln
 end-do

!*******************************************************************
! Task selection for branching
 function select_task(tlist: cptasklist): integer
  declarations
   Tset: set of integer
  end-declarations

 ! Get the number of elements of "tlist"
  listsize:= getsize(tlist)

 ! Set of uninstantiated tasks
  forall(i in 1..listsize)
   if not is_fixed(getstart(gettask(tlist,i))) then
    Tset+= {i}   		
   end-if

  returned:= 0
 	
  ! Get a task with smallest start time domain
  smin:= min(j in Tset) getsize(getstart(gettask(tlist,j)))
  forall(j in Tset)
   if getsize(getstart(gettask(tlist,j))) = smin then
    returned:=j; break
   end-if

 end-function

end-model

Results

An optimal solution to this problem has a makespan of 55. In comparison with the default scheduling strategy, our branching strategy reduces the number of nodes that are enumerated from over 300 to just 105 nodes with a comparable model execution time (however, for larger instances the default scheduling strategy is likely to outperform our branching strategy). The default minimization strategy does not find any solution for this problem within several minutes running time.

KalisUG/ivejobshop.png

Figure 5.8: Solution graph

Alternative search strategies

Similarly to what we have seen above, we may define a user task selection strategy for the scheduling search. The only modifications required in our model are to replace cp_set_branching by cp_set_schedule_strategy and cp_minimize by cp_schedule. The definition of the user task choice function select_task remains unchanged.

! Branching strategy
 Strategy:=task_serialize(->select_task, KALIS_MIN_TO_MAX,
            KALIS_MIN_TO_MAX,
            union(j in JOBS, m in MACH | exists(task(j,m))) {task(j,m)})
 cp_set_schedule_strategy(KALIS_INITIAL_SOLUTION, Strategy)

! Solve the problem
 if cp_schedule(getmakespan)=0 then
  writeln("Problem is infeasible")
  exit(1)
 end-if
 

This strategy takes even fewer nodes for completing the enumeration than the standard search with user task selection.

Instead of the user-defined task selection function select_task it is equally possible to use one of the predefined task selection criteria:

KALIS_SMALLEST_EST / KALIS_LARGEST_EST
choose task with the smallest/largest lower bound on its start time ( 'earliest start time'),
KALIS_SMALLEST_LST / KALIS_LARGEST_LST
choose task with the smallest/largest upper bound on its start time ( 'latest start time'),
KALIS_SMALLEST_ECT / KALIS_LARGEST_ECT
choose task with the smallest/largest lower bound on its completion time ( 'earliest completion time'),
KALIS_SMALLEST_LCT / KALIS_LARGEST_LCT
choose task with the smallest/largest upper bound on its completion time ( 'latest completion time').
For the present example, the best choice proves to be KALIS_SMALLEST_LCT (terminating the search after approximately 60 nodes with both cp_schedule and cp_minimize):

 Strategy:=task_serialize(KALIS_SMALLEST_LCT, KALIS_MIN_TO_MAX,
            KALIS_MIN_TO_MAX,
            union(j in JOBS, m in MACH | exists(task(j,m))) {task(j,m)})

Even more specialized task selection strategies can be defined with the help of group_serialize. In this case you may decide freely which are the variables to enumerate for a task and in which order they are to be considered. The scope of group_serialize is not limited to tasks, you may use this subroutine to define search strategies involving other types of (user) objects.

Choice of the propagation algorithm

The performance of search algorithms for scheduling problems relies not alone on the definition of the enumeration strategy; the choice of the propagation algorithm for resource constraints may also have a noticeable impact. The propagation type is set by an optional last argument of the procedure set_resources_attributes, such as

 set_resource_attributes(res, KALIS_DISCRETE_RESOURCE, CAP, ALG)

where ALG is the propagation algorithm choice.

For unary resources we have a choice between the algorithms KALIS_TASK_INTERVALS and KALIS_DISJUNCTIONS. The former achieves stronger pruning at the cost of a larger computational overhead making the choice of KALIS_DISJUNCTIONS more competitive for small to medium sized problems. Taking the example of the jobshop problem from the previous section, when using KALIS_DISJUNCTIONS the default scheduling search is about 4 times faster than with KALIS_TASK_INTERVALS although the number of nodes explored is slightly larger than with the latter.

The propagation algorithm options for cumulative resources are KALIS_TASK_INTERVALS and KALIS_TIMETABLING. The filtering algorithm KALIS_TASK_INTERVALS is stronger and relatively slow making it the preferred choice for hard, medium-sized problems whereas KALIS_TIMETABLING should be given preference for very large problems where the computational overhead of the former may be prohibitive. In terms of an example, the binpacking problem we have worked with in Sections Cumulative scheduling: discrete resources and Variable-based enumeration solves about three times faster with KALIS_TASK_INTERVALS than with KALIS_TIMETABLING (using the default scheduling search).


© 2001-2025 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.