Machine Scheduling - Hands-On Mathematical Optimization With AMPL in Python
Machine Scheduling - Hands-On Mathematical Optimization With AMPL in Python
Contents
Problem description
Empirical Scheduling
Optimal Scheduling
SOLVER = "highs"
ampl = ampl_notebook(
modules=["highs"], # modules to install
license_uuid="default", # license to use
) # instantiate AMPL object and register magics
Problem description
“Which job should be done next?” is a question one face in modern life, whether for a busy
student working on course assignments, a courier delivering packages, a server waiting on
tables in a busy restaurant, a computer processing threads, or a machine on a complex
assembly line. There are empirical answers to this question, among them “first in, first out”,
“last in, first out”, or “shortest job first”.
What we consider in this notebook is the modeling finding solutions to this class of problem
using optimization techniques. This notebook demonstrates the formulation of a model for
scheduling a single machine scheduling using disjunctive programming in AMPL. The problem
is to schedule a set of jobs on a single machine given the release time, duration, and due time
for each job. Date for the example problem is from Christelle Gueret, Christian Prins, Marc
Sevaux, “Applications of Optimization with Xpress-MP,” Chapter 5, Dash Optimization, 2000.
Skip to main content
Job data
The problem is to schedule a sequence of jobs for a single machine. The problem data
presented as a Pandas dataframe where each row corresponds to a job. The dataframe in
indexed the names of the jobs. The data for each job consists of the time when it is released
for machine processing, the duration of the job, and when the it is due. The objective is, if
possible, to schedule the jobs on the machine to satisfy the due dates. If that is not possible,
then the objective is to schedule the jobs to minimizing some measure of schedule “badness”.
import pandas as pd
jobs = pd.DataFrame(
{
"A": {"release": 2, "duration": 5, "due": 10},
"B": {"release": 5, "duration": 6, "due": 21},
"C": {"release": 4, "duration": 8, "due": 15},
"D": {"release": 0, "duration": 4, "due": 10},
"E": {"release": 0, "duration": 2, "due": 5},
"F": {"release": 8, "duration": 3, "due": 15},
"G": {"release": 9, "duration": 2, "due": 22},
}
).T
jobs
A 2 5 10
B 5 6 21
C 4 8 15
D 0 4 10
E 0 2 5
F 8 3 15
G 9 2 22
Schedule data
A schedule is also represented here by a Pandas dataframe indexed by job names. The
columns indicate the start, finish, and the
Skip toamount by each job is past due. The following cell
main content
creates a schedule when the jobs are executed in the order given by the jobs dataframe.
Args:
jobs (DataFrame): Job information with columns "release", "duration", a
seq (DataFrame): A list of job indices representing the sequence in whi
Return:
A DataFrame containing schedule information, including "start", "finish
"""
schedule = pd.DataFrame(index=jobs.index)
t = 0
for job in seq:
t = max(t, jobs.loc[job]["release"])
schedule.loc[job, "start"] = t
t += jobs.loc[job, "duration"]
schedule.loc[job, "finish"] = t
schedule.loc[job, "past"] = max(0, t - jobs.loc[job, "due"])
return schedule
Gantt chart
A traditional means of visualizing scheduling data in the form of a Gantt chart. The next cell
presents a function gantt that plots a Gantt
Skip chart
to main given jobs and schedule information.
content
ort matplotlib.pyplot as plt
m matplotlib.lines import Line2D
Args:
jobs (DataFrame): Contains job release times and due times.
schedule (DataFrame): Contains job start times, finish times, and past due
title (str, optional): Title of the Gantt chart. Defaults to "Job Schedule".
"""
w = 0.25 # bar width
fig, ax = plt.subplots(1, 1, figsize=(10, 0.7 * len(jobs.index)))
# If past due
if f > d:
ax.plot([d] * 2, [-k + w, -k + 2 * w], lw=0.5, color="k")
ax.plot([f] * 2, [-k + w, -k + 2 * w], lw=0.5, color="k")
ax.plot([d, f], [-k + 1.5 * w] * 2, solid_capstyle="butt", lw=1, color="
ax.text(
f + 0.5,
-k + 1.5 * w,
f"{schedule.loc[job, 'past']}
Skip to past due",
main content
va="center",
)
total_past_due = schedule["past"].sum()
ax.set_ylim(-len(jobs.index), 1)
ax.set_title(f"{title}: Total past due = {total_past_due}")
ax.set_xlabel("Time")
ax.set_ylabel("Jobs")
ax.set_yticks([]) # range(len(jobs.index)), jobs.index)
ax.spines["right"].set_visible(False)
ax.spines["left"].set_visible(False)
ax.spines["top"].set_visible(False)
custom_lines = [
Line2D([0], [0], c="k", lw=10, alpha=0.2),
Line2D([0], [0], c="g", lw=10, alpha=0.6),
Line2D([0], [0], c="r", lw=10, alpha=0.6),
]
ax.legend(
custom_lines,
["Job release/due window", "Completed on time", "Completed past due"],
)
Empirical Scheduling
To provide a comparison to scheduling with optimization models, the following cells implement
two well known and accepted empirical
Skip rules forcontent
to main scheduling jobs on a single machine:
First in, first out (FIFO)
Earliest due data (EDD)
Symbol Description
The essential decision variable is the time at which the job starts processing.
Print to PDF
Symbol Description
min ∑ pastj
j
Constraints describe the required logical relationships among the decision variables. For
example, a job cannot start until it is released for processing
startj ≥ releasej
Once started, machine processing continues until the job is finished. The finish time for each
job is compared to the due time, and any past due interval is stored the pastj decision variable.
The final set of constraints require that no pair of jobs be operating on the same machine at the
same time. For this purpose, we consider each unique pair (i, j) where the constraint i < j to
imposed to avoid considering the same pair twice. Then for any unique pair i and j, either i
finishes before j starts, or j finishes before i starts. This is expressed as the family of
disjunctions
%%writefile job_machine_scheduling.mod
set JOBS;
set PAIRS within{JOBS, JOBS};
param release{JOBS};
param duration{JOBS};
param due{JOBS};
Skip to
s.t. job_release {job in JOBS}: main content
start[job] >= release[job];
s.t. job_duration {job in JOBS}: finish[job] == start[job] + duration[job];
s.t. past_due_constraint {job in JOBS}: past[job] >= finish[job] - due[job];
s.t. machine_deconflict {(job_a, job_b) in PAIRS}:
(finish[job_a] <= start[job_b] or finish[job_b] <= start[job_a])
and not
(finish[job_a] <= start[job_b] and finish[job_b] <= start[job_a])
;
Overwriting job_machine_scheduling.mod
def build_model(jobs):
m = AMPL()
m.read("job_machine_scheduling.mod")
m.set_data(jobs, "JOBS")
return m
model = build_model(jobs)
schedule = solve_model(model)
display(schedule)