job-shop-lib 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|