job-shop-lib 0.1.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- job_shop_lib/__init__.py +20 -0
- job_shop_lib/base_solver.py +37 -0
- job_shop_lib/benchmarking/__init__.py +78 -0
- job_shop_lib/benchmarking/benchmark_instances.json +1 -0
- job_shop_lib/benchmarking/load_benchmark.py +142 -0
- job_shop_lib/cp_sat/__init__.py +5 -0
- job_shop_lib/cp_sat/ortools_solver.py +201 -0
- job_shop_lib/dispatching/__init__.py +49 -0
- job_shop_lib/dispatching/dispatcher.py +269 -0
- job_shop_lib/dispatching/dispatching_rule_solver.py +111 -0
- job_shop_lib/dispatching/dispatching_rules.py +160 -0
- job_shop_lib/dispatching/factories.py +206 -0
- job_shop_lib/dispatching/pruning_functions.py +116 -0
- job_shop_lib/exceptions.py +26 -0
- job_shop_lib/generators/__init__.py +7 -0
- job_shop_lib/generators/basic_generator.py +197 -0
- job_shop_lib/graphs/__init__.py +52 -0
- job_shop_lib/graphs/build_agent_task_graph.py +209 -0
- job_shop_lib/graphs/build_disjunctive_graph.py +78 -0
- job_shop_lib/graphs/constants.py +21 -0
- job_shop_lib/graphs/job_shop_graph.py +159 -0
- job_shop_lib/graphs/node.py +147 -0
- job_shop_lib/job_shop_instance.py +355 -0
- job_shop_lib/operation.py +120 -0
- job_shop_lib/schedule.py +180 -0
- job_shop_lib/scheduled_operation.py +97 -0
- job_shop_lib/visualization/__init__.py +25 -0
- job_shop_lib/visualization/agent_task_graph.py +257 -0
- job_shop_lib/visualization/create_gif.py +191 -0
- job_shop_lib/visualization/disjunctive_graph.py +206 -0
- job_shop_lib/visualization/gantt_chart.py +147 -0
- job_shop_lib-0.1.0.dist-info/LICENSE +21 -0
- job_shop_lib-0.1.0.dist-info/METADATA +363 -0
- job_shop_lib-0.1.0.dist-info/RECORD +35 -0
- job_shop_lib-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,142 @@
|
|
1
|
+
"""Module for loading benchmark instances.
|
2
|
+
|
3
|
+
All benchmark instances are stored in a single JSON file. This module provides
|
4
|
+
functions to load the instances from the file and return them as
|
5
|
+
JobShopInstance objects.
|
6
|
+
|
7
|
+
The contributions to this benchmark dataset are as follows:
|
8
|
+
|
9
|
+
abz5-9: This subset, comprising five instances, was introduced by Adams et
|
10
|
+
al. (1988).
|
11
|
+
ft06, ft10, ft20: These three instances are attributed to the work of
|
12
|
+
Fisher and Thompson, as detailed in their 1963 work.
|
13
|
+
la01-40: A collection of forty instances, this group was contributed by
|
14
|
+
Lawrence, as referenced in his 1984 report.
|
15
|
+
orb01-10: Ten instances in this category were provided by Applegate and
|
16
|
+
Cook, as seen in their 1991 study.
|
17
|
+
swb01-20: This segment, encompassing twenty instances, was contributed by
|
18
|
+
Storer et al., as per their 1992 article.
|
19
|
+
yn1-4: Yamada and Nakano are credited with the addition of four instances
|
20
|
+
in this group, as found in their 1992 paper.
|
21
|
+
ta01-80: The largest contribution, consisting of eighty instances, was
|
22
|
+
made by Taillard, as documented in his 1993 paper.
|
23
|
+
|
24
|
+
The metadata from these instances has been updated using data from:
|
25
|
+
|
26
|
+
Thomas Weise. jsspInstancesAndResults. Accessed in January 2024.
|
27
|
+
Available at: https://github.com/thomasWeise/jsspInstancesAndResults
|
28
|
+
|
29
|
+
It includes the following information:
|
30
|
+
- "optimum" (int | None): The optimal makespan for the instance.
|
31
|
+
- "lower_bound" (int): The lower bound for the makespan. If
|
32
|
+
optimality is known, it is equal to the optimum.
|
33
|
+
- "upper_bound" (int): The upper bound for the makespan. If
|
34
|
+
optimality is known, it is equal to the optimum.
|
35
|
+
- "reference" (str): The paper or source where the instance was first
|
36
|
+
introduced.
|
37
|
+
|
38
|
+
References:
|
39
|
+
- J. Adams, E. Balas, and D. Zawack, "The shifting bottleneck procedure
|
40
|
+
for job shop scheduling," Management Science, vol. 34, no. 3,
|
41
|
+
pp. 391–401, 1988.
|
42
|
+
|
43
|
+
- J.F. Muth and G.L. Thompson, Industrial scheduling. Englewood Cliffs,
|
44
|
+
NJ: Prentice-Hall, 1963.
|
45
|
+
|
46
|
+
- S. Lawrence, "Resource constrained project scheduling: An experimental
|
47
|
+
investigation of heuristic scheduling techniques (Supplement),"
|
48
|
+
Carnegie-Mellon University, Graduate School of Industrial
|
49
|
+
Administration, Pittsburgh, Pennsylvania, 1984.
|
50
|
+
|
51
|
+
- D. Applegate and W. Cook, "A computational study of job-shop
|
52
|
+
scheduling," ORSA Journal on Computer, vol. 3, no. 2, pp. 149–156,
|
53
|
+
1991.
|
54
|
+
|
55
|
+
- R.H. Storer, S.D. Wu, and R. Vaccari, "New search spaces for
|
56
|
+
sequencing problems with applications to job-shop scheduling,"
|
57
|
+
Management Science, vol. 38, no. 10, pp. 1495–1509, 1992.
|
58
|
+
|
59
|
+
- T. Yamada and R. Nakano, "A genetic algorithm applicable to
|
60
|
+
large-scale job-shop problems," in Proceedings of the Second
|
61
|
+
International Workshop on Parallel Problem Solving from Nature
|
62
|
+
(PPSN'2), Brussels, Belgium, pp. 281–290, 1992.
|
63
|
+
|
64
|
+
- E. Taillard, "Benchmarks for basic scheduling problems," European
|
65
|
+
Journal of Operational Research, vol. 64, no. 2, pp. 278–285, 1993.
|
66
|
+
"""
|
67
|
+
|
68
|
+
from typing import Any
|
69
|
+
|
70
|
+
import functools
|
71
|
+
import json
|
72
|
+
from importlib import resources
|
73
|
+
|
74
|
+
from job_shop_lib import JobShopInstance
|
75
|
+
|
76
|
+
|
77
|
+
@functools.cache
|
78
|
+
def load_all_benchmark_instances() -> dict[str, JobShopInstance]:
|
79
|
+
"""Loads all benchmark instances available.
|
80
|
+
|
81
|
+
Returns:
|
82
|
+
A dictionary containing the names of the benchmark instances as keys
|
83
|
+
and the corresponding JobShopInstance objects as values.
|
84
|
+
|
85
|
+
"""
|
86
|
+
benchmark_instances_dict = load_benchmark_json()
|
87
|
+
return {
|
88
|
+
name: load_benchmark_instance(name)
|
89
|
+
for name in benchmark_instances_dict
|
90
|
+
}
|
91
|
+
|
92
|
+
|
93
|
+
def load_benchmark_instance(name: str) -> JobShopInstance:
|
94
|
+
"""Loads a specific benchmark instance.
|
95
|
+
|
96
|
+
Calls to `load_benchmark_json` to load the benchmark instances from the
|
97
|
+
JSON file. The instance is then loaded from the dictionary using the
|
98
|
+
provided name. Since `load_benchmark_json` is cached, the file is only
|
99
|
+
read once.
|
100
|
+
|
101
|
+
Args:
|
102
|
+
name: The name of the benchmark instance to load. Can be one of the
|
103
|
+
following: "abz5-9", "ft06", "ft10", "ft20", "la01-40", "orb01-10",
|
104
|
+
"swb01-20", "yn1-4", or "ta01-80".
|
105
|
+
"""
|
106
|
+
benchmark_dict = load_benchmark_json()[name]
|
107
|
+
return JobShopInstance.from_matrices(
|
108
|
+
duration_matrix=benchmark_dict["duration_matrix"],
|
109
|
+
machines_matrix=benchmark_dict["machines_matrix"],
|
110
|
+
name=name,
|
111
|
+
metadata=benchmark_dict["metadata"],
|
112
|
+
)
|
113
|
+
|
114
|
+
|
115
|
+
@functools.cache
|
116
|
+
def load_benchmark_json() -> dict[str, dict[str, Any]]:
|
117
|
+
"""Loads the raw JSON file containing the benchmark instances.
|
118
|
+
|
119
|
+
Results are cached to avoid reading the file multiple times.
|
120
|
+
|
121
|
+
Each instance is represented as a dictionary with the following keys
|
122
|
+
and values:
|
123
|
+
- "name" (str): The name of the instance.
|
124
|
+
- "duration_matrix" (list[list[int]]): The matrix containing the
|
125
|
+
durations for each operation.
|
126
|
+
- "machines_matrix" (list[list[int]]): The matrix containing the
|
127
|
+
machines for each operation.
|
128
|
+
- "metadata" (dict[str, Any]): A dictionary containing metadata
|
129
|
+
about the instance. The keys are "optimum" (int | None),
|
130
|
+
"lower_bound" (int), "upper_bound" (int),
|
131
|
+
and "reference" (str).
|
132
|
+
|
133
|
+
Returns:
|
134
|
+
The dictionary containing the benchmark instances.
|
135
|
+
"""
|
136
|
+
benchmark_file = (
|
137
|
+
resources.files("job_shop_lib.benchmarking")
|
138
|
+
/ "benchmark_instances.json"
|
139
|
+
)
|
140
|
+
|
141
|
+
with benchmark_file.open("r", encoding="utf-8") as f:
|
142
|
+
return json.load(f)
|
@@ -0,0 +1,201 @@
|
|
1
|
+
"""Home of the ORToolsSolver class."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import time
|
6
|
+
|
7
|
+
from ortools.sat.python import cp_model
|
8
|
+
from ortools.sat.python.cp_model import IntVar
|
9
|
+
|
10
|
+
from job_shop_lib import (
|
11
|
+
JobShopInstance,
|
12
|
+
Schedule,
|
13
|
+
ScheduledOperation,
|
14
|
+
Operation,
|
15
|
+
)
|
16
|
+
from job_shop_lib import NoSolutionFoundError, BaseSolver
|
17
|
+
|
18
|
+
|
19
|
+
class ORToolsSolver(BaseSolver):
|
20
|
+
"""A solver for the job shop scheduling problem using constraint
|
21
|
+
programming.
|
22
|
+
|
23
|
+
This solver uses the ortools library to solve the job shop scheduling
|
24
|
+
problem using constraint programming.
|
25
|
+
"""
|
26
|
+
|
27
|
+
def __init__(
|
28
|
+
self,
|
29
|
+
max_time_in_seconds: float | None = None,
|
30
|
+
log_search_progress: bool = False,
|
31
|
+
):
|
32
|
+
self.log_search_progress = log_search_progress
|
33
|
+
self.max_time_in_seconds = max_time_in_seconds
|
34
|
+
|
35
|
+
self.makespan: cp_model.IntVar | None = None
|
36
|
+
self.model = cp_model.CpModel()
|
37
|
+
self.solver = cp_model.CpSolver()
|
38
|
+
self._operations_start: dict[Operation, tuple[IntVar, IntVar]] = {}
|
39
|
+
|
40
|
+
def __call__(self, instance: JobShopInstance) -> Schedule:
|
41
|
+
# Re-defined here since we already add metadata to the schedule in
|
42
|
+
# the solve method.
|
43
|
+
return self.solve(instance)
|
44
|
+
|
45
|
+
def solve(self, instance: JobShopInstance) -> Schedule:
|
46
|
+
"""Creates the variables, constraints and objective, and solves the
|
47
|
+
problem.
|
48
|
+
|
49
|
+
If a solution is found, it extracts and returns the start times of
|
50
|
+
each operation and the makespan. If no solution is found, it raises
|
51
|
+
a NoSolutionFound exception.
|
52
|
+
"""
|
53
|
+
self._initialize_model(instance)
|
54
|
+
|
55
|
+
start_time = time.perf_counter()
|
56
|
+
status = self.solver.Solve(self.model)
|
57
|
+
elapsed_time = time.perf_counter() - start_time
|
58
|
+
|
59
|
+
if status not in {cp_model.OPTIMAL, cp_model.FEASIBLE}:
|
60
|
+
raise NoSolutionFoundError(
|
61
|
+
f"No solution could be found for the given problem. "
|
62
|
+
f"Elapsed time: {elapsed_time} seconds."
|
63
|
+
)
|
64
|
+
if self.makespan is None:
|
65
|
+
# Check added to satisfy mypy
|
66
|
+
raise ValueError("The makespan variable was not set.")
|
67
|
+
|
68
|
+
metadata = {
|
69
|
+
"status": "optimal" if status == cp_model.OPTIMAL else "feasible",
|
70
|
+
"elapsed_time": elapsed_time,
|
71
|
+
"makespan": self.solver.Value(self.makespan),
|
72
|
+
"solved_by": "ORToolsSolver",
|
73
|
+
}
|
74
|
+
return self._create_schedule(instance, metadata)
|
75
|
+
|
76
|
+
def _initialize_model(self, instance: JobShopInstance):
|
77
|
+
"""Initializes the model with variables, constraints and objective.
|
78
|
+
|
79
|
+
The model is initialized with two variables for each operation: start
|
80
|
+
and end time. The constraints ensure that operations within a job are
|
81
|
+
performed in sequence and that operations assigned to the same machine
|
82
|
+
do not overlap. The objective is to minimize the makespan.
|
83
|
+
|
84
|
+
Args:
|
85
|
+
instance: The job shop instance to be solved.
|
86
|
+
"""
|
87
|
+
self.model = cp_model.CpModel()
|
88
|
+
self.solver = cp_model.CpSolver()
|
89
|
+
self.solver.parameters.log_search_progress = self.log_search_progress
|
90
|
+
self._operations_start = {}
|
91
|
+
if self.max_time_in_seconds is not None:
|
92
|
+
self.solver.parameters.max_time_in_seconds = (
|
93
|
+
self.max_time_in_seconds
|
94
|
+
)
|
95
|
+
self._create_variables(instance)
|
96
|
+
self._add_constraints(instance)
|
97
|
+
self._set_objective(instance)
|
98
|
+
|
99
|
+
def _create_schedule(
|
100
|
+
self, instance: JobShopInstance, metadata: dict[str, object]
|
101
|
+
) -> Schedule:
|
102
|
+
"""Creates a Schedule object from the solution."""
|
103
|
+
operations_start: dict[Operation, int] = {
|
104
|
+
operation: self.solver.Value(start_var)
|
105
|
+
for operation, (start_var, _) in self._operations_start.items()
|
106
|
+
}
|
107
|
+
|
108
|
+
unsorted_schedule: list[list[ScheduledOperation]] = [
|
109
|
+
[] for _ in range(instance.num_machines)
|
110
|
+
]
|
111
|
+
for operation, start_time in operations_start.items():
|
112
|
+
unsorted_schedule[operation.machine_id].append(
|
113
|
+
ScheduledOperation(operation, start_time, operation.machine_id)
|
114
|
+
)
|
115
|
+
|
116
|
+
sorted_schedule = [
|
117
|
+
sorted(scheduled_operation, key=lambda x: x.start_time)
|
118
|
+
for scheduled_operation in unsorted_schedule
|
119
|
+
]
|
120
|
+
|
121
|
+
return Schedule(
|
122
|
+
instance=instance, schedule=sorted_schedule, **metadata
|
123
|
+
)
|
124
|
+
|
125
|
+
def _create_variables(self, instance: JobShopInstance):
|
126
|
+
"""Creates two variables for each operation: start and end time."""
|
127
|
+
for job in instance.jobs:
|
128
|
+
for operation in job:
|
129
|
+
start_var = self.model.NewIntVar(
|
130
|
+
0, instance.total_duration, f"start_{operation}"
|
131
|
+
)
|
132
|
+
end_var = self.model.NewIntVar(
|
133
|
+
0, instance.total_duration, f"end_{operation}"
|
134
|
+
)
|
135
|
+
self._operations_start[operation] = (start_var, end_var)
|
136
|
+
self.model.Add(end_var == start_var + operation.duration)
|
137
|
+
|
138
|
+
def _add_constraints(self, instance: JobShopInstance):
|
139
|
+
"""Adds job and machine constraints.
|
140
|
+
|
141
|
+
Job Constraints: Ensure that operations within a job are performed in
|
142
|
+
sequence. If operation A must precede operation B in a job, we ensure
|
143
|
+
A's end time is less than or equal to B's start time.
|
144
|
+
|
145
|
+
Machine Constraints: Operations assigned to the same machine cannot
|
146
|
+
overlap. This is ensured by creating interval variables (which
|
147
|
+
represent the duration an operation occupies a machine)
|
148
|
+
and adding a 'no overlap' constraint for these intervals on
|
149
|
+
each machine.
|
150
|
+
"""
|
151
|
+
self._add_job_constraints(instance)
|
152
|
+
self._add_machine_constraints(instance)
|
153
|
+
|
154
|
+
def _set_objective(self, instance: JobShopInstance):
|
155
|
+
"""The objective is to minimize the makespan, which is the total
|
156
|
+
duration of the schedule."""
|
157
|
+
self.makespan = self.model.NewIntVar(
|
158
|
+
0, instance.total_duration, "makespan"
|
159
|
+
)
|
160
|
+
end_times = [end for _, end in self._operations_start.values()]
|
161
|
+
self.model.AddMaxEquality(self.makespan, end_times)
|
162
|
+
self.model.Minimize(self.makespan)
|
163
|
+
|
164
|
+
def _add_job_constraints(self, instance: JobShopInstance):
|
165
|
+
"""Adds job constraints to the model. Operations within a job must be
|
166
|
+
performed in sequence. If operation A must precede operation B in a
|
167
|
+
job, we ensure A's end time is less than or equal to B's start time."""
|
168
|
+
for job in instance.jobs:
|
169
|
+
for position in range(1, len(job)):
|
170
|
+
self.model.Add(
|
171
|
+
self._operations_start[job[position - 1]][1]
|
172
|
+
<= self._operations_start[job[position]][0]
|
173
|
+
)
|
174
|
+
|
175
|
+
def _add_machine_constraints(self, instance: JobShopInstance):
|
176
|
+
"""Adds machine constraints to the model. Operations assigned to the
|
177
|
+
same machine cannot overlap. This is ensured by creating interval
|
178
|
+
variables (which represent the duration an operation occupies a
|
179
|
+
machine) and adding a 'no overlap' constraint for these intervals on
|
180
|
+
each machine."""
|
181
|
+
|
182
|
+
# Create interval variables for each operation on each machine
|
183
|
+
machines_operations: list[list[tuple[tuple[IntVar, IntVar], int]]] = [
|
184
|
+
[] for _ in range(instance.num_machines)
|
185
|
+
]
|
186
|
+
for job in instance.jobs:
|
187
|
+
for operation in job:
|
188
|
+
machines_operations[operation.machine_id].append(
|
189
|
+
(
|
190
|
+
self._operations_start[operation],
|
191
|
+
operation.duration,
|
192
|
+
)
|
193
|
+
)
|
194
|
+
for machine_id, operations in enumerate(machines_operations):
|
195
|
+
intervals = []
|
196
|
+
for (start_var, end_var), duration in operations:
|
197
|
+
interval_var = self.model.NewIntervalVar(
|
198
|
+
start_var, duration, end_var, f"interval_{machine_id}"
|
199
|
+
)
|
200
|
+
intervals.append(interval_var)
|
201
|
+
self.model.AddNoOverlap(intervals)
|
@@ -0,0 +1,49 @@
|
|
1
|
+
"""Package containing all the functionality to solve the Job Shop Scheduling
|
2
|
+
Problem step-by-step."""
|
3
|
+
|
4
|
+
from job_shop_lib.dispatching.dispatcher import Dispatcher
|
5
|
+
from job_shop_lib.dispatching.dispatching_rules import (
|
6
|
+
shortest_processing_time_rule,
|
7
|
+
first_come_first_served_rule,
|
8
|
+
most_work_remaining_rule,
|
9
|
+
most_operations_remaining_rule,
|
10
|
+
random_operation_rule,
|
11
|
+
)
|
12
|
+
from job_shop_lib.dispatching.pruning_functions import (
|
13
|
+
prune_dominated_operations,
|
14
|
+
prune_non_immediate_machines,
|
15
|
+
create_composite_pruning_function,
|
16
|
+
)
|
17
|
+
from job_shop_lib.dispatching.factories import (
|
18
|
+
PruningFunction,
|
19
|
+
DispatchingRule,
|
20
|
+
MachineChooser,
|
21
|
+
dispatching_rule_factory,
|
22
|
+
machine_chooser_factory,
|
23
|
+
pruning_function_factory,
|
24
|
+
composite_pruning_function_factory,
|
25
|
+
)
|
26
|
+
from job_shop_lib.dispatching.dispatching_rule_solver import (
|
27
|
+
DispatchingRuleSolver,
|
28
|
+
)
|
29
|
+
|
30
|
+
|
31
|
+
__all__ = [
|
32
|
+
"dispatching_rule_factory",
|
33
|
+
"machine_chooser_factory",
|
34
|
+
"shortest_processing_time_rule",
|
35
|
+
"first_come_first_served_rule",
|
36
|
+
"most_work_remaining_rule",
|
37
|
+
"most_operations_remaining_rule",
|
38
|
+
"random_operation_rule",
|
39
|
+
"DispatchingRule",
|
40
|
+
"MachineChooser",
|
41
|
+
"Dispatcher",
|
42
|
+
"DispatchingRuleSolver",
|
43
|
+
"prune_dominated_operations",
|
44
|
+
"prune_non_immediate_machines",
|
45
|
+
"create_composite_pruning_function",
|
46
|
+
"PruningFunction",
|
47
|
+
"pruning_function_factory",
|
48
|
+
"composite_pruning_function_factory",
|
49
|
+
]
|
@@ -0,0 +1,269 @@
|
|
1
|
+
"""Home of the `Dispatcher` class."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from collections.abc import Callable
|
6
|
+
from collections import deque
|
7
|
+
|
8
|
+
from job_shop_lib import (
|
9
|
+
JobShopInstance,
|
10
|
+
Schedule,
|
11
|
+
ScheduledOperation,
|
12
|
+
Operation,
|
13
|
+
)
|
14
|
+
|
15
|
+
|
16
|
+
class Dispatcher:
|
17
|
+
"""Handles the logic of scheduling operations on machines.
|
18
|
+
|
19
|
+
This class allow us to just define the order in which operations are
|
20
|
+
sequenced and the machines in which they are processed. It is then
|
21
|
+
responsible for scheduling the operations on the machines and keeping
|
22
|
+
track of the next available time for each machine and job.
|
23
|
+
|
24
|
+
Attributes:
|
25
|
+
instance:
|
26
|
+
The instance of the job shop problem to be scheduled.
|
27
|
+
schedule:
|
28
|
+
The schedule of operations on machines.
|
29
|
+
pruning_function:
|
30
|
+
The pipeline of pruning methods to be used to filter out
|
31
|
+
operations from the list of available operations.
|
32
|
+
"""
|
33
|
+
|
34
|
+
__slots__ = (
|
35
|
+
"instance",
|
36
|
+
"schedule",
|
37
|
+
"_machine_next_available_time",
|
38
|
+
"_job_next_operation_index",
|
39
|
+
"_job_next_available_time",
|
40
|
+
"pruning_function",
|
41
|
+
)
|
42
|
+
|
43
|
+
def __init__(
|
44
|
+
self,
|
45
|
+
instance: JobShopInstance,
|
46
|
+
pruning_function: (
|
47
|
+
Callable[[Dispatcher, list[Operation]], list[Operation]] | None
|
48
|
+
) = None,
|
49
|
+
) -> None:
|
50
|
+
"""Initializes the object with the given instance.
|
51
|
+
|
52
|
+
Args:
|
53
|
+
instance:
|
54
|
+
The instance of the job shop problem to be solved.
|
55
|
+
pruning_strategies:
|
56
|
+
A list of pruning strategies to be used to filter out
|
57
|
+
operations from the list of available operations. Supported
|
58
|
+
values are 'dominated_operations' and 'non_immediate_machines'.
|
59
|
+
Defaults to [PruningStrategy.DOMINATED_OPERATIONS]. To disable
|
60
|
+
pruning, pass an empty list.
|
61
|
+
"""
|
62
|
+
|
63
|
+
self.instance = instance
|
64
|
+
self.schedule = Schedule(self.instance)
|
65
|
+
self._machine_next_available_time = [0] * self.instance.num_machines
|
66
|
+
self._job_next_operation_index = [0] * self.instance.num_jobs
|
67
|
+
self._job_next_available_time = [0] * self.instance.num_jobs
|
68
|
+
self.pruning_function = pruning_function
|
69
|
+
|
70
|
+
@property
|
71
|
+
def machine_next_available_time(self) -> list[int]:
|
72
|
+
"""Returns the next available time for each machine."""
|
73
|
+
return self._machine_next_available_time
|
74
|
+
|
75
|
+
@property
|
76
|
+
def job_next_operation_index(self) -> list[int]:
|
77
|
+
"""Returns the index of the next operation to be scheduled for each
|
78
|
+
job."""
|
79
|
+
return self._job_next_operation_index
|
80
|
+
|
81
|
+
@property
|
82
|
+
def job_next_available_time(self) -> list[int]:
|
83
|
+
"""Returns the next available time for each job."""
|
84
|
+
return self._job_next_available_time
|
85
|
+
|
86
|
+
@classmethod
|
87
|
+
def create_schedule_from_raw_solution(
|
88
|
+
cls, instance: JobShopInstance, raw_solution: list[list[Operation]]
|
89
|
+
) -> Schedule:
|
90
|
+
"""Creates a schedule from a raw solution.
|
91
|
+
|
92
|
+
A raw solution is a list of lists of operations, where each list
|
93
|
+
represents the order of operations for a machine.
|
94
|
+
|
95
|
+
Args:
|
96
|
+
instance:
|
97
|
+
The instance of the job shop problem to be solved.
|
98
|
+
raw_solution:
|
99
|
+
A list of lists of operations, where each list represents the
|
100
|
+
order of operations for a machine.
|
101
|
+
|
102
|
+
Returns:
|
103
|
+
A Schedule object representing the solution.
|
104
|
+
"""
|
105
|
+
dispatcher = cls(instance)
|
106
|
+
dispatcher.reset()
|
107
|
+
raw_solution_deques = [
|
108
|
+
deque(operations) for operations in raw_solution
|
109
|
+
]
|
110
|
+
while not dispatcher.schedule.is_complete():
|
111
|
+
for machine_id, operations in enumerate(raw_solution_deques):
|
112
|
+
if not operations:
|
113
|
+
continue
|
114
|
+
operation = operations[0]
|
115
|
+
if dispatcher.is_operation_ready(operation):
|
116
|
+
dispatcher.dispatch(operation, machine_id)
|
117
|
+
operations.popleft()
|
118
|
+
return dispatcher.schedule
|
119
|
+
|
120
|
+
def reset(self) -> None:
|
121
|
+
"""Resets the dispatcher to its initial state."""
|
122
|
+
self.schedule.reset()
|
123
|
+
self._machine_next_available_time = [0] * self.instance.num_machines
|
124
|
+
self._job_next_operation_index = [0] * self.instance.num_jobs
|
125
|
+
self._job_next_available_time = [0] * self.instance.num_jobs
|
126
|
+
|
127
|
+
def dispatch(self, operation: Operation, machine_id: int) -> None:
|
128
|
+
"""Schedules the given operation on the given machine.
|
129
|
+
|
130
|
+
The start time of the operation is computed based on the next
|
131
|
+
available time for the machine and the next available time for the
|
132
|
+
job to which the operation belongs. The operation is then scheduled
|
133
|
+
on the machine and the tracking attributes are updated.
|
134
|
+
Args:
|
135
|
+
operation:
|
136
|
+
The operation to be scheduled.
|
137
|
+
machine_id:
|
138
|
+
The id of the machine on which the operation is to be
|
139
|
+
scheduled.
|
140
|
+
|
141
|
+
Raises:
|
142
|
+
ValueError: If the operation is not ready to be scheduled.
|
143
|
+
"""
|
144
|
+
|
145
|
+
if not self.is_operation_ready(operation):
|
146
|
+
raise ValueError("Operation is not ready to be scheduled.")
|
147
|
+
|
148
|
+
start_time = self.start_time(operation, machine_id)
|
149
|
+
|
150
|
+
scheduled_operation = ScheduledOperation(
|
151
|
+
operation, start_time, machine_id
|
152
|
+
)
|
153
|
+
self.schedule.add(scheduled_operation)
|
154
|
+
self._update_tracking_attributes(scheduled_operation)
|
155
|
+
|
156
|
+
def is_operation_ready(self, operation: Operation) -> bool:
|
157
|
+
"""Returns True if the given operation is ready to be scheduled.
|
158
|
+
|
159
|
+
An operation is ready to be scheduled if it is the next operation
|
160
|
+
to be scheduled for its job.
|
161
|
+
|
162
|
+
Args:
|
163
|
+
operation:
|
164
|
+
The operation to be checked.
|
165
|
+
"""
|
166
|
+
return (
|
167
|
+
self.job_next_operation_index[operation.job_id]
|
168
|
+
== operation.position_in_job
|
169
|
+
)
|
170
|
+
|
171
|
+
def start_time(self, operation: Operation, machine_id: int) -> int:
|
172
|
+
"""Computes the start time for the given operation on the given
|
173
|
+
machine.
|
174
|
+
|
175
|
+
The start time is the maximum of the next available time for the
|
176
|
+
machine and the next available time for the job to which the
|
177
|
+
operation belongs.
|
178
|
+
|
179
|
+
Args:
|
180
|
+
operation:
|
181
|
+
The operation to be scheduled.
|
182
|
+
machine_id:
|
183
|
+
The id of the machine on which the operation is to be
|
184
|
+
scheduled.
|
185
|
+
"""
|
186
|
+
return max(
|
187
|
+
self.machine_next_available_time[machine_id],
|
188
|
+
self.job_next_available_time[operation.job_id],
|
189
|
+
)
|
190
|
+
|
191
|
+
def _update_tracking_attributes(
|
192
|
+
self, scheduled_operation: ScheduledOperation
|
193
|
+
) -> None:
|
194
|
+
# Variables defined here to make the lines shorter
|
195
|
+
job_id = scheduled_operation.job_id
|
196
|
+
machine_id = scheduled_operation.machine_id
|
197
|
+
end_time = scheduled_operation.end_time
|
198
|
+
|
199
|
+
self.machine_next_available_time[machine_id] = end_time
|
200
|
+
self.job_next_operation_index[job_id] += 1
|
201
|
+
self.job_next_available_time[job_id] = end_time
|
202
|
+
|
203
|
+
def current_time(self) -> int:
|
204
|
+
"""Returns the current time of the schedule.
|
205
|
+
|
206
|
+
The current time is the minimum start time of the available
|
207
|
+
operations.
|
208
|
+
"""
|
209
|
+
available_operations = self.available_operations()
|
210
|
+
return self.min_start_time(available_operations)
|
211
|
+
|
212
|
+
def min_start_time(self, operations: list[Operation]) -> int:
|
213
|
+
"""Returns the minimum start time of the available operations."""
|
214
|
+
if not operations:
|
215
|
+
return self.schedule.makespan()
|
216
|
+
min_start_time = float("inf")
|
217
|
+
for op in operations:
|
218
|
+
for machine_id in op.machines:
|
219
|
+
start_time = self.start_time(op, machine_id)
|
220
|
+
min_start_time = min(min_start_time, start_time)
|
221
|
+
return int(min_start_time)
|
222
|
+
|
223
|
+
def uncompleted_operations(self) -> list[Operation]:
|
224
|
+
"""Returns the list of operations that have not been scheduled.
|
225
|
+
|
226
|
+
An operation is uncompleted if it has not been scheduled yet.
|
227
|
+
|
228
|
+
It is more efficient than checking all operations in the instance.
|
229
|
+
"""
|
230
|
+
uncompleted_operations = []
|
231
|
+
for job_id, next_position in enumerate(self.job_next_operation_index):
|
232
|
+
operations = self.instance.jobs[job_id][next_position:]
|
233
|
+
uncompleted_operations.extend(operations)
|
234
|
+
return uncompleted_operations
|
235
|
+
|
236
|
+
def available_operations(self) -> list[Operation]:
|
237
|
+
"""Returns a list of available operations for processing, optionally
|
238
|
+
filtering out operations known to be bad choices.
|
239
|
+
|
240
|
+
This method first gathers all possible next operations from the jobs
|
241
|
+
being processed. It then optionally filters these operations to exclude
|
242
|
+
ones that are deemed inefficient or suboptimal choices.
|
243
|
+
|
244
|
+
An operation is sub-optimal if there is another operation that could
|
245
|
+
be scheduled in the same machine that would finish before the start
|
246
|
+
time of the sub-optimal operation.
|
247
|
+
|
248
|
+
Returns:
|
249
|
+
A list of Operation objects that are available for scheduling.
|
250
|
+
|
251
|
+
Raises:
|
252
|
+
ValueError: If using the filter_bad_choices option and one of the
|
253
|
+
available operations can be scheduled in more than one machine.
|
254
|
+
"""
|
255
|
+
available_operations = self._available_operations()
|
256
|
+
if self.pruning_function is not None:
|
257
|
+
available_operations = self.pruning_function(
|
258
|
+
self, available_operations
|
259
|
+
)
|
260
|
+
return available_operations
|
261
|
+
|
262
|
+
def _available_operations(self) -> list[Operation]:
|
263
|
+
available_operations = []
|
264
|
+
for job_id, next_position in enumerate(self.job_next_operation_index):
|
265
|
+
if next_position == len(self.instance.jobs[job_id]):
|
266
|
+
continue
|
267
|
+
operation = self.instance.jobs[job_id][next_position]
|
268
|
+
available_operations.append(operation)
|
269
|
+
return available_operations
|