Critical Path Method
Contents
Critical Path Method#
This notebook demonstrates the Critical Path Method.
Background#
The Critical Path Method is a technique for calculating the shortest time span needed to complete a series of tasks. The tasks are represented by nodes, each labelled with the duration. The precedence order of the task is given by a set of arcs.
Here we demonstrate the representation and calculation of the critical path. Decision variables are introduced for
Earliest Start
Earliest Finish
Latest Start
Latest Finish
Slack = Earliest Finish - Earliest Start = Latest Finish - Earliest Finish
Tasks on the Critical Path have zero slack.
!pip install cplex
Collecting cplex
Downloading cplex-22.1.0.0-cp39-cp39-macosx_10_6_x86_64.whl (39.6 MB)
|████████████████████████████████| 39.6 MB 4.6 MB/s eta 0:00:01
?25hInstalling collected packages: cplex
Successfully installed cplex-22.1.0.0
Example: Stadium Construction#
Stadium Construction, Example 7.1.1 from Christelle Gueret, Christian Prins, Marc Sevaux, “Applications of Optimization with Xpress-MP,” Chapter 7, Dash Optimization, 2000.
tasks = {
"T01": {"dur": 2.0, "desc": "Installing the contruction site"},
"T02": {"dur": 16.0, "desc": "Terracing"},
"T03": {"dur": 9.0, "desc": "Constructing the foundations"},
"T04": {"dur": 8.0, "desc": "Access roads and other networks"},
"T05": {"dur": 10.0, "desc": "Erecting the basement"},
"T06": {"dur": 6.0, "desc": "Main floor"},
"T07": {"dur": 2.0, "desc": "Dividing up the changing rooms"},
"T08": {"dur": 2.0, "desc": "Electrifying the terraces"},
"T09": {"dur": 9.0, "desc": "Constructing the roof"},
"T10": {"dur": 5.0, "desc": "Lighting the stadium"},
"T11": {"dur": 3.0, "desc": "Installing the terraces"},
"T12": {"dur": 2.0, "desc": "Sealing the roof"},
"T13": {"dur": 1.0, "desc": "Finishing the changing rooms"},
"T14": {"dur": 7.0, "desc": "Constructing the ticket office"},
"T15": {"dur": 4.0, "desc": "Secondary access roads"},
"T16": {"dur": 3.0, "desc": "Means of signaling"},
"T17": {"dur": 9.0, "desc": "Lawn and sports accessories"},
"T18": {"dur": 1.0, "desc": "Handing over the building"}
}
pred = [
("T01", "T02"),
("T02", "T03"),
("T02", "T04"),
("T02", "T14"),
("T03", "T05"),
("T04", "T07"),
("T04", "T10"),
("T04", "T09"),
("T04", "T06"),
("T04", "T15"),
("T05", "T06"),
("T06", "T09"),
("T06", "T11"),
("T06", "T08"),
("T07", "T13"),
("T08", "T16"),
("T09", "T12"),
("T11", "T16"),
("T12", "T17"),
("T14", "T16"),
("T14", "T15"),
("T17", "T18")
]
import pyomo.environ as pyo
m = pyo.ConcreteModel("Stadium Construction")
m.TASKS = pyo.Set(initialize=tasks.keys())
m.ARCS = pyo.Set(initialize=pred)
m.earliest_start = pyo.Var(m.TASKS, domain=pyo.NonNegativeReals)
m.earliest_finish = pyo.Var(m.TASKS, domain=pyo.NonNegativeReals)
m.latest_start = pyo.Var(m.TASKS, domain=pyo.NonNegativeReals)
m.latest_finish = pyo.Var(m.TASKS, domain=pyo.NonNegativeReals)
m.slack = pyo.Var(m.TASKS, domain=pyo.NonNegativeReals)
m.finish_time = pyo.Var(domain=pyo.NonNegativeReals)
@m.Objective(sense=pyo.minimize)
def minimize_finish_time(m):
return 1400*m.finish_time - sum(m.slack[task] for task in m.TASKS)
@m.Constraint(m.TASKS)
def early(m, task):
return m.earliest_finish[task] <= m.finish_time
@m.Constraint(m.TASKS)
def late(m, task):
return m.latest_finish[task] <= m.finish_time
@m.Constraint(m.TASKS)
def dur_early(m, task):
return m.earliest_finish[task] == m.earliest_start[task] + tasks[task]["dur"]
@m.Constraint(m.TASKS)
def dur_late(m, task):
return m.latest_finish[task] == m.latest_start[task] + tasks[task]["dur"]
@m.Constraint(m.TASKS)
def slack_def(m, task):
return m.slack[task] == m.latest_start[task] - m.earliest_start[task]
@m.Constraint(m.ARCS)
def arc_early(m, a, b):
return m.earliest_finish[a] <= m.earliest_start[b]
@m.Constraint(m.ARCS)
def arc_late(m, a, b):
return m.latest_finish[a] <= m.latest_start[b]
solver = pyo.SolverFactory("cbc")
solver.solve(m).write()
# ==========================================================
# = Solver Results =
# ==========================================================
# ----------------------------------------------------------
# Problem Information
# ----------------------------------------------------------
Problem:
- Name: unknown
Lower bound: 89371.0
Upper bound: 89371.0
Number of objectives: 1
Number of constraints: 135
Number of variables: 92
Number of nonzeros: 19
Sense: minimize
# ----------------------------------------------------------
# Solver Information
# ----------------------------------------------------------
Solver:
- Status: ok
User time: -1.0
System time: 0.0
Wallclock time: 0.01
Termination condition: optimal
Termination message: Model was solved to optimality (subject to tolerances), and an optimal solution is available.
Statistics:
Branch and bound:
Number of bounded subproblems: None
Number of created subproblems: None
Black box:
Number of iterations: 34
Error rc: 0
Time: 0.16571903228759766
# ----------------------------------------------------------
# Solution Information
# ----------------------------------------------------------
Solution:
- number of solutions: 0
number of solutions displayed: 0
import pandas as pd
df = pd.DataFrame(tasks).T
for task in m.TASKS:
df.loc[task, "earliest start"] = m.earliest_start[task]()
df.loc[task, "earliest finish"] = m.earliest_finish[task]()
df.loc[task, "latest start"] = m.latest_start[task]()
df.loc[task, "latest finish"] = m.latest_finish[task]()
df.loc[task, "slack"] = m.slack[task]()
df
dur | desc | earliest start | earliest finish | latest start | latest finish | slack | |
---|---|---|---|---|---|---|---|
T01 | 2.0 | Installing the contruction site | 0.0 | 2.0 | 0.0 | 2.0 | 0.0 |
T02 | 16.0 | Terracing | 2.0 | 18.0 | 2.0 | 18.0 | 0.0 |
T03 | 9.0 | Constructing the foundations | 18.0 | 27.0 | 18.0 | 27.0 | 0.0 |
T04 | 8.0 | Access roads and other networks | 18.0 | 26.0 | 29.0 | 37.0 | 11.0 |
T05 | 10.0 | Erecting the basement | 27.0 | 37.0 | 27.0 | 37.0 | 0.0 |
T06 | 6.0 | Main floor | 37.0 | 43.0 | 37.0 | 43.0 | 0.0 |
T07 | 2.0 | Dividing up the changing rooms | 26.0 | 28.0 | 61.0 | 63.0 | 35.0 |
T08 | 2.0 | Electrifying the terraces | 43.0 | 45.0 | 59.0 | 61.0 | 16.0 |
T09 | 9.0 | Constructing the roof | 43.0 | 52.0 | 43.0 | 52.0 | 0.0 |
T10 | 5.0 | Lighting the stadium | 26.0 | 31.0 | 59.0 | 64.0 | 33.0 |
T11 | 3.0 | Installing the terraces | 43.0 | 46.0 | 58.0 | 61.0 | 15.0 |
T12 | 2.0 | Sealing the roof | 52.0 | 54.0 | 52.0 | 54.0 | 0.0 |
T13 | 1.0 | Finishing the changing rooms | 28.0 | 29.0 | 63.0 | 64.0 | 35.0 |
T14 | 7.0 | Constructing the ticket office | 18.0 | 25.0 | 53.0 | 60.0 | 35.0 |
T15 | 4.0 | Secondary access roads | 26.0 | 30.0 | 60.0 | 64.0 | 34.0 |
T16 | 3.0 | Means of signaling | 46.0 | 49.0 | 61.0 | 64.0 | 15.0 |
T17 | 9.0 | Lawn and sports accessories | 54.0 | 63.0 | 54.0 | 63.0 | 0.0 |
T18 | 1.0 | Handing over the building | 63.0 | 64.0 | 63.0 | 64.0 | 0.0 |
df[df["slack"] <= 0.001]
dur | desc | earliest start | earliest finish | latest start | latest finish | slack | |
---|---|---|---|---|---|---|---|
T01 | 2.0 | Installing the contruction site | 0.0 | 2.0 | 0.0 | 2.0 | 0.0 |
T02 | 16.0 | Terracing | 2.0 | 18.0 | 2.0 | 18.0 | 0.0 |
T03 | 9.0 | Constructing the foundations | 18.0 | 27.0 | 18.0 | 27.0 | 0.0 |
T05 | 10.0 | Erecting the basement | 27.0 | 37.0 | 27.0 | 37.0 | 0.0 |
T06 | 6.0 | Main floor | 37.0 | 43.0 | 37.0 | 43.0 | 0.0 |
T09 | 9.0 | Constructing the roof | 43.0 | 52.0 | 43.0 | 52.0 | 0.0 |
T12 | 2.0 | Sealing the roof | 52.0 | 54.0 | 52.0 | 54.0 | 0.0 |
T17 | 9.0 | Lawn and sports accessories | 54.0 | 63.0 | 54.0 | 63.0 | 0.0 |
T18 | 1.0 | Handing over the building | 63.0 | 64.0 | 63.0 | 64.0 | 0.0 |
df[df["slack"] > 0.001]
dur | desc | earliest start | earliest finish | latest start | latest finish | slack | |
---|---|---|---|---|---|---|---|
T04 | 8.0 | Access roads and other networks | 18.0 | 26.0 | 29.0 | 37.0 | 11.0 |
T07 | 2.0 | Dividing up the changing rooms | 26.0 | 28.0 | 61.0 | 63.0 | 35.0 |
T08 | 2.0 | Electrifying the terraces | 43.0 | 45.0 | 59.0 | 61.0 | 16.0 |
T10 | 5.0 | Lighting the stadium | 26.0 | 31.0 | 59.0 | 64.0 | 33.0 |
T11 | 3.0 | Installing the terraces | 43.0 | 46.0 | 58.0 | 61.0 | 15.0 |
T13 | 1.0 | Finishing the changing rooms | 28.0 | 29.0 | 63.0 | 64.0 | 35.0 |
T14 | 7.0 | Constructing the ticket office | 18.0 | 25.0 | 53.0 | 60.0 | 35.0 |
T15 | 4.0 | Secondary access roads | 26.0 | 30.0 | 60.0 | 64.0 | 34.0 |
T16 | 3.0 | Means of signaling | 46.0 | 49.0 | 61.0 | 64.0 | 15.0 |
import pyomo.environ as pyo
m = pyo.ConcreteModel("Stadium Construction")
m.TASKS = pyo.Set(initialize=tasks.keys())
m.ARCS = pyo.Set(initialize=pred)
m.earliest_start = pyo.Var(m.TASKS, domain=pyo.NonNegativeReals)
m.earliest_finish = pyo.Var(m.TASKS, domain=pyo.NonNegativeReals)
m.latest_start = pyo.Var(m.TASKS, domain=pyo.NonNegativeReals)
m.latest_finish = pyo.Var(m.TASKS, domain=pyo.NonNegativeReals)
m.slack = pyo.Var(m.TASKS, domain=pyo.NonNegativeReals)
m.finish_time = pyo.Var(domain=pyo.NonNegativeReals)
@m.Objective(sense=pyo.minimize)
def minimize_finish_time(m):
return 100*m.finish_time - sum(m.slack[task] for task in m.TASKS)
@m.Constraint(m.TASKS)
def early(m, task):
return m.earliest_finish[task] <= m.finish_time
@m.Constraint(m.TASKS)
def late(m, task):
return m.latest_finish[task] <= m.finish_time
@m.Constraint(m.TASKS)
def dur_early(m, task):
return m.earliest_finish[task] == m.earliest_start[task] + tasks[task]["dur"]
@m.Constraint(m.TASKS)
def dur_late(m, task):
return m.latest_finish[task] == m.latest_start[task] + tasks[task]["dur"]
@m.Constraint(m.TASKS)
def slack_def(m, task):
return m.slack[task] == m.latest_start[task] - m.earliest_start[task]
@m.Constraint(m.ARCS)
def arc_early(m, a, b):
return m.earliest_finish[a] <= m.earliest_start[b]
@m.Constraint(m.ARCS)
def arc_late(m, a, b):
return m.latest_finish[a] <= m.latest_start[b]
solver = pyo.SolverFactory("cbc")
solver.solve(m);
Identifying the Critical Path#
import pandas as pd
df = pd.DataFrame(tasks).T
for task in m.TASKS:
df.loc[task, "earliest start"] = m.earliest_start[task]()
df.loc[task, "earliest finish"] = m.earliest_finish[task]()
df.loc[task, "latest start"] = m.latest_start[task]()
df.loc[task, "latest finish"] = m.latest_finish[task]()
df["slack"] = df["latest start"] - df["earliest start"]
display(df[df["slack"] <= 0.1])
display(df[df["slack"] > 0.1])
dur | desc | earliest start | earliest finish | latest start | latest finish | slack | |
---|---|---|---|---|---|---|---|
T01 | 2.0 | Installing the contruction site | 0.0 | 2.0 | 0.0 | 2.0 | 0.0 |
T02 | 16.0 | Terracing | 2.0 | 18.0 | 2.0 | 18.0 | 0.0 |
T03 | 9.0 | Constructing the foundations | 18.0 | 27.0 | 18.0 | 27.0 | 0.0 |
T05 | 10.0 | Erecting the basement | 27.0 | 37.0 | 27.0 | 37.0 | 0.0 |
T06 | 6.0 | Main floor | 37.0 | 43.0 | 37.0 | 43.0 | 0.0 |
T09 | 9.0 | Constructing the roof | 43.0 | 52.0 | 43.0 | 52.0 | 0.0 |
T12 | 2.0 | Sealing the roof | 52.0 | 54.0 | 52.0 | 54.0 | 0.0 |
T17 | 9.0 | Lawn and sports accessories | 54.0 | 63.0 | 54.0 | 63.0 | 0.0 |
T18 | 1.0 | Handing over the building | 63.0 | 64.0 | 63.0 | 64.0 | 0.0 |
dur | desc | earliest start | earliest finish | latest start | latest finish | slack | |
---|---|---|---|---|---|---|---|
T04 | 8.0 | Access roads and other networks | 18.0 | 26.0 | 29.0 | 37.0 | 11.0 |
T07 | 2.0 | Dividing up the changing rooms | 26.0 | 28.0 | 61.0 | 63.0 | 35.0 |
T08 | 2.0 | Electrifying the terraces | 43.0 | 45.0 | 59.0 | 61.0 | 16.0 |
T10 | 5.0 | Lighting the stadium | 26.0 | 31.0 | 59.0 | 64.0 | 33.0 |
T11 | 3.0 | Installing the terraces | 43.0 | 46.0 | 58.0 | 61.0 | 15.0 |
T13 | 1.0 | Finishing the changing rooms | 28.0 | 29.0 | 63.0 | 64.0 | 35.0 |
T14 | 7.0 | Constructing the ticket office | 18.0 | 25.0 | 53.0 | 60.0 | 35.0 |
T15 | 4.0 | Secondary access roads | 26.0 | 30.0 | 60.0 | 64.0 | 34.0 |
T16 | 3.0 | Means of signaling | 46.0 | 49.0 | 61.0 | 64.0 | 15.0 |