# Machine scheduling problem taken from Jeffrey Kantor's cookbook:
# https://nbviewer.jupyter.org/github/jckantor/ND-Pyomo-Cookbook/blob/master/notebooks/04.01-Machine-Bottleneck.ipynb
#
# Link to cookbook:
# https://jckantor.github.io/ND-Pyomo-Cookbook/
#
# set-up python version to use
reticulate::use_python("/opt/anaconda3/bin/python")
library(reticulate)
# import pyomo
pyo = import("pyomo.environ", convert = FALSE)
pygdp = import("pyomo.gdp", convert = FALSE)
bi = import_builtins()
# had signal handling issues when using solver. Turned
# off signal handling based on this discussion thread
# https://github.com/PyUtilib/pyutilib/issues/31
pyulib = import("pyutilib.subprocess.GlobalData")
pyulib$DEFINE_SIGNAL_HANDLERS_DEFAULT = FALSE
# printing model to a file, reading it back into R to print in R markdown
mdl_print = function(m) {
printstr = "
with open('tmp_model.txt', 'w') as f:
r.mdl.pprint(ostream=f)
f.close()
"
printstr = gsub("mdl", m, printstr)
py_run_string(printstr)
printstr_R = paste(readLines("tmp_model.txt"), collapse = "\n")
return(printstr_R)
}
# get operators
source("operators.R")
# load other libraries
library(dplyr)
##
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
library(ggplot2)
# list of jobs to be scheduled on the single machine
# with their release date, due date and duration of task
jobs_df = tibble(job = c("A", "B", "C", "D", "E", "F", "G"),
release = c(2, 5, 4, 0, 0, 8, 9),
duration = c(5, 6, 8, 4, 2, 3, 2),
due = c(10, 21, 15, 10, 5, 15, 22))
# data converted into list to be used in pyomo model
release_L = list()
duration_L = list()
due_L = list()
for (i in 1: nrow(jobs_df)) {
release_L[[jobs_df$job[i]]] = jobs_df$release[i]
duration_L[[jobs_df$job[i]]] = jobs_df$duration[i]
due_L[[jobs_df$job[i]]] = jobs_df$due[i]
}
# max time
tmax = sum(as.numeric(release_L)) + sum(as.numeric(duration_L))
# create model object
M = pyo$ConcreteModel()
# Set of jobs
M$jobs = pyo$Set(initialize = jobs_df$job)
# Set of job pairs (j, k) where j < k
filt_rule = function(m, j, k) {
return(j < k)
}
M$pairs = pyo$Set(initialize = M$jobs * M$jobs, filter = filt_rule)
# Parameters: release, duration, due time
M$release = pyo$Param(M$jobs, initialize = release_L)
M$duration = pyo$Param(M$jobs, initialize = duration_L)
M$due = pyo$Param(M$jobs, initialize = due_L)
# decision variables for each job:start time
# other variables: time by which job is past due or early
M$start = pyo$Var(M$jobs, bounds = tuple(0, tmax))
M$pastdue = pyo$Var(M$jobs, bounds = tuple(0, tmax))
M$early = pyo$Var(M$jobs, bounds = tuple(0, tmax))
# objective: minimize total past due time across jobs
obj_rule = function(m) {
expr = 0
for (j in bi$list(m$jobs)) {
expr = expr + m$pastdue[j]
}
return(expr)
}
M$obj = pyo$Objective(rule = obj_rule, sense = pyo$minimize)
# constraint that enables calculation of past due time
pastdue_calc_rule = function(m, j) {
expr = m$start[j] + m$duration[j] + m$early[j] == m$due[j] + m$pastdue[j]
return(expr)
}
M$pastdue_calc_cons = pyo$Constraint(M$jobs, rule = pastdue_calc_rule)
# constraint that start time of job >= release time of job
release_rule = function(m, j) {
expr = m$start[j] >= m$release[j]
return(expr)
}
M$release_cons = pyo$Constraint(M$jobs, rule = release_rule)
#
# Disjunction that either (j precedes K) or (k precedes j)
#
ovlp_rule = function(m, j, k) {
expr1 = m$start[j] + m$duration[j] <= m$start[k]
expr2 = m$start[k] + m$duration[k] <= m$start[j]
return(c(expr1, expr2))
}
M$ovlp_cons = pygdp$Disjunction(M$pairs, rule = ovlp_rule)
# This is a mixed integer linear program (MILP) and
# solved here using glpk
pyo$TransformationFactory("gdp.hull")$apply_to(M)
## None
res = pyo$SolverFactory('glpk')$solve(M)
# the results are extracted into a tibble
resL = list()
for (i in bi$list(M$jobs)) {
resL[[i]] = c(job = i, start = py_to_r(M$start[i]()), pastdue = py_to_r(M$pastdue[i]()))
}
res_df = bind_rows(resL)
res_df = res_df %>% mutate(start = as.numeric(start), pastdue = as.numeric(pastdue))
res_df = inner_join(res_df, jobs_df, by = "job")
res_df = res_df %>% mutate(finish = start + duration)
# plot the results as a gantt chart
# light blue bars for a job start at release time and end at due time
# solid line start at start time and end at start + duration time
#
p = ggplot(data = res_df)
p = p + geom_segment(aes(x = release, y = job, xend = due, yend = job), size = 4, alpha = 0.2, color = "blue")
p = p + geom_segment(aes(x = start, y = job, xend = finish, yend = job), size = 1.5)
p = p + labs(x = "time", y = "")
p = p + theme_bw()
p

# In the optimal solution, C is past due by 15 time units and A is past due by 1 time unit
Session Info
sessionInfo()
## R version 4.0.1 (2020-06-06)
## Platform: x86_64-apple-darwin17.0 (64-bit)
## Running under: macOS High Sierra 10.13.6
##
## Matrix products: default
## BLAS: /Library/Frameworks/R.framework/Versions/4.0/Resources/lib/libRblas.dylib
## LAPACK: /Library/Frameworks/R.framework/Versions/4.0/Resources/lib/libRlapack.dylib
##
## locale:
## [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] ggplot2_3.3.2 dplyr_1.0.0 reticulate_1.16
##
## loaded via a namespace (and not attached):
## [1] Rcpp_1.0.4.6 knitr_1.28 magrittr_1.5 munsell_0.5.0
## [5] tidyselect_1.1.0 colorspace_1.4-1 lattice_0.20-41 R6_2.4.1
## [9] rlang_0.4.6 stringr_1.4.0 highr_0.8 tools_4.0.1
## [13] grid_4.0.1 gtable_0.3.0 xfun_0.14 withr_2.2.0
## [17] htmltools_0.5.0 ellipsis_0.3.1 yaml_2.2.1 digest_0.6.25
## [21] tibble_3.0.1 lifecycle_0.2.0 crayon_1.3.4 Matrix_1.2-18
## [25] farver_2.0.3 purrr_0.3.4 vctrs_0.3.1 glue_1.4.1
## [29] evaluate_0.14 rmarkdown_2.3 labeling_0.3 stringi_1.4.6
## [33] compiler_4.0.1 pillar_1.4.4 scales_1.1.1 generics_0.0.2
## [37] jsonlite_1.6.1 pkgconfig_2.0.3
# pyomo version used is 5.7