Single-machine Scheduling
Minimizing weighted sum of WIP considering job deadlines:
1 | \(C_j \leq d_j \; \forall j \in J\) | \(\sum_{j \in J}{w_j C_j}\)
A possible relaxation is given by ignoring dealine constraints, in which the optimal sequence of jobs is given by sorting them by \(w_j / p_j\).
Stronger relaxations exploring lagrangian relaxations, besides dominance rules can be applied to make it more efficient, but to illustrate the library functionality, we will keep this basic example.
For a performatic example, try our bnbprob implementation:
from bnbprob.machdeadline import MachDeadlineProb, Job
[1]:
from dataclasses import field
from pydantic.dataclasses import dataclass
from bnbpy import BranchAndBound, Problem, configure_logfile, plot_tree
[2]:
configure_logfile('machine-deadline-example.log')
Defining a Job
[3]:
@dataclass(slots=True, frozen=True)
class Job:
id: int
"""Job unique identifier"""
p: int
"""Job processing time"""
w: int
"""Job weight in objective function"""
d: int
"""Job deadline"""
pri: float = field(init=False, repr=False)
"""Job priority, calculated as w/p"""
def __post_init__(self) -> None:
object.__setattr__(self, 'pri', self.w / self.p)
def __repr__(self):
return f"Job({self.id})"
Problem Class
[4]:
LARGE_INT = 100000000
class MachDeadlineProb(Problem):
_fixed: list[Job]
"""End sequence in reverse order,
i.e. the last scheduled job is the first in the list"""
_unscheduled: list[Job]
"""Unsceduled jobs, sorted by WSPT rule in correct order"""
_fixed_term: int
"""Term of the objective function for the fixed part of the sequence"""
_unscheduled_term: int
"""Term of the objective function for the unscheduled
part of the sequence"""
_violations: bool
"""Whether the current sequence has any deadline delay violation"""
_unscheduled_total_time: int
"""Total processing time of the unscheduled jobs"""
def __init__(self, jobs: list[Job]) -> None:
super().__init__()
self._fixed = []
self._unscheduled = jobs.copy()
self._fixed_term = 0
self._unscheduled_term = 0
self._violations = False
self._unscheduled_total_time = sum(job.p for job in self._unscheduled)
MachDeadlineProb.find_wspt(self._unscheduled)
self._compute_completion_times()
@property
def sequence(self) -> list[Job]:
return self._unscheduled + list(reversed(self._fixed))
def write(self) -> str:
return '->'.join([f'{job}' for job in self.sequence])
def _compute_completion_times(self) -> None:
last_c = 0
self._violations = False
self._unscheduled_term = 0
for job in self._unscheduled:
c = last_c + job.p
last_c = c
self._unscheduled_term += job.w * c
if c > job.d:
self._violations = True
def calc_bound(self) -> int:
return self._unscheduled_term + self._fixed_term
def is_feasible(self) -> bool:
return not self._violations
def branch(self) -> list['MachDeadlineProb']:
# Create one child for each possible job to schedule next
children = []
for job in self._unscheduled:
# Early infeasibility check
if job.d < self._unscheduled_total_time:
continue
child = self.child_copy(deep=False)
child.fix_job(job)
children.append(child)
return children
def fix_job(self, job: Job) -> None:
self._fixed.append(job)
self._unscheduled.remove(job)
self._fixed_term += job.w * self._unscheduled_total_time
self._unscheduled_total_time -= job.p
self._compute_completion_times()
def child_copy(self, deep: bool = True) -> 'MachDeadlineProb':
other = super().child_copy(deep)
# In case of a shallow copy,
# we need to make sure to copy the mutable attributes.
# Notice jobs are immutable, so we can just pass on the references.
other._fixed = self._fixed.copy()
other._unscheduled = self._unscheduled.copy()
return other
@staticmethod
def find_wspt(jobs: list[Job]) -> None:
jobs.sort(key=lambda job: job.pri, reverse=True)
Solve
[5]:
# Parameters
p = [4, 3, 8, 2, 7, 6]
w = [1, 1, 1, 1, 1, 1]
d = [10, 20, 20, 30, 30, 30]
# Instantiate problem
jobs = [Job(j, p[j], w[j], d[j]) for j in range(len(p))]
problem = MachDeadlineProb(jobs)
print(problem.calc_bound())
# Solve
bnb = BranchAndBound(eval_node='in', save_tree=True)
sol = bnb.solve(problem)
print(f'Nodes explored: {bnb.explored}')
print(sol)
print(sol.problem.write())
83
Nodes explored: 3
Status: OPTIMAL | Cost: 86.0 | LB: 86.0
Job(3)->Job(1)->Job(0)->Job(2)->Job(5)->Job(4)
Visualize
Remember, by default the search tree is deleted from memory as nodes are fathomed. To keep it for illustration/debug purposes, initialize BranchAndBound with save_tree=True.
[6]:
plot_tree(bnb.root, dpi=120, figsize=[8, 3])