The previous notebooks showed problems that are ILO fairly directly. Single-machine scheduling is different: two of its requirements, that jobs do not overlap and that lateness is only penalized when a job is actually late, are not linear at first sight. We model the problem in the same steps as the other applications in this lecture, and introduce the tricks that make it linear. After the model, we look at these tricks in general: binary variables combined with a big M turn either/or choices, fixed costs and conditional constraints into linear constraints.
Learning outcomes
On completion of this notebook, you will be able to:
formulate a single-machine scheduling problem as an ILO model;
formulate either/or (disjunctive) constraints as linear constraints with a binary variable and a big M;
model fixed costs and conditional constraints with binary indicator variables.
Single-Machine Scheduling¶
Practical Motivation¶
A production facility receives a list of production orders (jobs), which it processes one at a time on a single machine. Some orders only become available later (the material has not arrived yet), and every order has a promised delivery date. Finishing late costs money, for example a contractual penalty per day, and some customers are more important than others. In which order, and when, should the jobs be processed so that the total penalty is as low as possible? Related problems are project planning and planning in healthcare, such as operating-room scheduling.
Modeling¶
Problem Definition and Example¶
There are jobs. Job has a duration , a release date before which it cannot start, a due date , and a tardiness cost per time unit it finishes after its due date. The machine processes one job at a time, and a job that has started runs until it is finished. The problem is to find a schedule (a start time per job) that minimizes the total weighted tardiness, where the tardiness of a job is how long it finishes after its due date (0 if it is on time).
As an example, take jobs:
| job | duration | release date | due date | tardiness cost |
|---|---|---|---|---|
| 1 | 6 | 0 | 8 | 1 |
| 2 | 4 | 2 | 7 | 1 |
| 3 | 5 | 1 | 12 | 1 |
For example, processing the jobs in the order 1, 3, 2 without idle time gives job 1 the time interval 0-6, job 3 the interval 6-11 and job 2 the interval 11-15. Jobs 1 and 3 are on time, and job 2 finishes time units late, so this schedule has a total weighted tardiness of 8.
Decision Variables¶
A schedule is fully described by when each job starts, so the natural decision variables are
The finish time of job is then . We will see below that two more sets of variables are needed to write the objective and the constraints linearly: a binary for the order of each pair of jobs, and a for the tardiness of each job.
Objective¶
The tardiness of job is , so we want to minimize
The maximum is not linear. But since we minimize it, the trick from the project-planning model works: introduce a variable for the tardiness of job that must be at least both terms of the maximum,
and minimize instead. These constraints only say that is at least the tardiness, but because , the optimizer pushes every down until one of them is met exactly, so that at the optimum.
Constraints¶
Release dates. A job cannot start before it is released:
No overlap. If job is processed before job , then job can only start once job has finished: . If comes first, we need instead. Exactly one of the two must hold, but which one is not known in advance: the order is itself something to decide. So we add a binary decision variable for the order of each pair of jobs,
with , since one of the two jobs comes first. We then switch the constraint on or off with a big M, a constant larger than any difference in time that can occur in a sensible schedule:
For a pair of jobs and this gives two constraints, one for and one for , and the order decides which one does the work:
| order | ||||
|---|---|---|---|---|
| before | 1 | 0 | (no restriction) | |
| before | 0 | 1 | (no restriction) |
is large enough: all jobs can be finished by then, so no sensible schedule has a job finishing more than after another job starts.
Complete ILO Model¶
The model grows fast: for jobs it has variables and constraints (not counting and the binary constraints), mostly because of the .
Solving the Example in pulp¶
The jobs are named "J1", "J2" and "J3", like the warehouses and stores in the
transportation problem, so that duration["J2"] reads
as “the duration of job 2” and cannot be mistaken for a list position. Two things in
the code are new. The release dates are not added as separate constraints but as lower
bounds of the start-time variables (lowBound=release[i]), which means the same to the
solver. And the big M is computed from the data instead of typed in, so it stays large
enough when the data changes.
import pulp
jobs = ["J1", "J2", "J3"]
duration = {"J1": 6, "J2": 4, "J3": 5}
release = {"J1": 0, "J2": 2, "J3": 1}
due = {"J1": 8, "J2": 7, "J3": 12}
tardiness_cost = {"J1": 1, "J2": 1, "J3": 1}
big_m = max(release.values()) + sum(duration.values())
single_machine = pulp.LpProblem(name="single_machine", sense=pulp.LpMinimize)
start = {i: pulp.LpVariable(name=f"x_{i}", lowBound=release[i]) for i in jobs}
before = {
(i, j): pulp.LpVariable(name=f"y_{i}_{j}", cat="Binary")
for i in jobs
for j in jobs
if i != j
}
tardy = {i: pulp.LpVariable(name=f"z_{i}", lowBound=0) for i in jobs}
single_machine += pulp.lpSum(tardiness_cost[i] * tardy[i] for i in jobs)
for i in jobs:
finish = start[i] + duration[i]
single_machine += tardy[i] >= finish - due[i], f"tardiness_{i}"
for i in jobs:
for j in jobs:
if i == j:
continue
finish = start[i] + duration[i]
switch = big_m * (1 - before[i, j])
single_machine += finish <= start[j] + switch, f"no_overlap_{i}_{j}"
if i < j:
pair_order = before[i, j] + before[j, i]
single_machine += pair_order == 1, f"order_{i}_{j}"
single_machine.solve(pulp.PULP_CBC_CMD(msg=False))
order = sorted(jobs, key=lambda i: start[i].value())
print("status:", pulp.LpStatus[single_machine.status])
print("start times:", {i: start[i].value() for i in jobs})
print("processing order:", order)
print("total weighted tardiness:", single_machine.objective.value())status: Optimal
start times: {'J1': 0.0, 'J2': 6.0, 'J3': 10.0}
processing order: ['J1', 'J2', 'J3']
total weighted tardiness: 6.0
The optimal order is J1, J2, J3, with a total weighted tardiness of 6: jobs 2 and 3 are each 3 time units late. That is better than the order 1, 3, 2 from the example above, which had tardiness 8.
Extensions¶
Other Objectives¶
The same variables support other common objectives just by changing the line we minimize:
flowtime : total time jobs spend in the system;
makespan : when the machine finishes everything; minimize it with the project-planning trick, subject to for all ;
a weighted sum of makespan and tardiness (or: minimize tardiness first, then minimize makespan with the tardiness fixed at its optimum).
Modeling Tricks Used in This Model¶
The scheduling model used a binary variable and a big M to switch a constraint on or off. The same idea shows up in many other models. This section describes it in general.
Big M and Indicator Variables¶
Suppose a cost is incurred only when an activity is used at all, regardless of how much. Take the product-mix problem with bookcases and desks, and suppose that making any desks at all requires a special tool with a fixed cost , however many desks are made. Introduce a binary meaning “desks are made”, subtract from the profit , and tie to with a big M, a constant at least as large as could ever be:
If then is forced to 1. If then could be 0 or 1, but since and we are maximizing profit, the optimizer sets it to 0. So effectively . Here is large enough: the oak-panel constraint already gives . A binary variable used this way, to indicate whether something happens, is called an indicator variable.
Conditional Constraints¶
The same idea handles a constraint that should hold only under a condition. If binary represents the condition and the constraint is , then
is the original constraint when , and no constraint at all (right-hand side huge) when . The no-overlap constraints of the scheduling model are of this form, with the condition “job comes before job ”.
Either/Or (Disjunctive) Constraints¶
In the scheduling model, for every pair of jobs one of two constraints had to hold, and we did not know in advance which one. In general, for two constraints and of which at least one must hold, introduce a binary and a big M:
When the first constraint is enforced and the second is switched off; when the reverse. In the scheduling model we used two binaries and with instead of a single , which is the same thing written symmetrically.
Choose as small as possible while still switching the constraint off: a very large makes the LO relaxations that branch and bound solves weak, and can cause numerical trouble in the solver.
References¶
Koole, G. (2019). An Introduction to Business Analytics. §6.7 “Modeling Tricks.” The book’s AMPL example is replaced with pulp.