job-shop-lib 1.1.1__py3-none-any.whl → 1.1.3__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 +1 -1
- job_shop_lib/_job_shop_instance.py +5 -5
- job_shop_lib/_schedule.py +2 -2
- job_shop_lib/dispatching/_dispatcher.py +12 -7
- job_shop_lib/dispatching/_optimal_operations_observer.py +6 -3
- job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +11 -46
- job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +2 -2
- job_shop_lib/dispatching/feature_observers/_factory.py +2 -4
- job_shop_lib/dispatching/feature_observers/_feature_observer.py +3 -0
- job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +0 -46
- job_shop_lib/dispatching/rules/_dispatching_rules_functions.py +106 -11
- job_shop_lib/generation/_general_instance_generator.py +6 -4
- job_shop_lib/graphs/_build_resource_task_graphs.py +1 -0
- job_shop_lib/graphs/_job_shop_graph.py +2 -2
- job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +4 -3
- job_shop_lib/reinforcement_learning/_resource_task_graph_observation.py +5 -3
- job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +23 -29
- job_shop_lib/visualization/gantt/_gantt_chart_video_and_gif_creation.py +6 -4
- job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py +18 -11
- job_shop_lib/visualization/graphs/_plot_resource_task_graph.py +1 -1
- {job_shop_lib-1.1.1.dist-info → job_shop_lib-1.1.3.dist-info}/METADATA +90 -19
- {job_shop_lib-1.1.1.dist-info → job_shop_lib-1.1.3.dist-info}/RECORD +24 -25
- job_shop_lib/generation/_transformations.py +0 -167
- {job_shop_lib-1.1.1.dist-info → job_shop_lib-1.1.3.dist-info}/LICENSE +0 -0
- {job_shop_lib-1.1.1.dist-info → job_shop_lib-1.1.3.dist-info}/WHEEL +0 -0
job_shop_lib/__init__.py
CHANGED
@@ -13,7 +13,7 @@ from job_shop_lib import Operation
|
|
13
13
|
|
14
14
|
|
15
15
|
class JobShopInstance:
|
16
|
-
"""Data structure to store a Job Shop Scheduling Problem instance.
|
16
|
+
r"""Data structure to store a Job Shop Scheduling Problem instance.
|
17
17
|
|
18
18
|
Additional attributes such as ``num_machines`` or ``durations_matrix`` can
|
19
19
|
be computed from the instance and are cached for performance since they
|
@@ -75,7 +75,7 @@ class JobShopInstance:
|
|
75
75
|
attributes of the operations are set when the instance is created.
|
76
76
|
See :meth:`set_operation_attributes` for more information. Defaults
|
77
77
|
to True.
|
78
|
-
|
78
|
+
\**metadata:
|
79
79
|
Additional information about the instance.
|
80
80
|
"""
|
81
81
|
|
@@ -131,7 +131,7 @@ class JobShopInstance:
|
|
131
131
|
name: str | None = None,
|
132
132
|
**metadata: Any,
|
133
133
|
) -> JobShopInstance:
|
134
|
-
"""Creates a JobShopInstance from a file following Taillard's format.
|
134
|
+
r"""Creates a JobShopInstance from a file following Taillard's format.
|
135
135
|
|
136
136
|
Args:
|
137
137
|
file_path:
|
@@ -144,7 +144,7 @@ class JobShopInstance:
|
|
144
144
|
name:
|
145
145
|
A string with the name of the instance. If not provided, the
|
146
146
|
name of the instance is set to the name of the file.
|
147
|
-
|
147
|
+
\**metadata:
|
148
148
|
Additional information about the instance.
|
149
149
|
|
150
150
|
Returns:
|
@@ -221,7 +221,7 @@ class JobShopInstance:
|
|
221
221
|
A list of lists of integers. The i-th list contains the
|
222
222
|
durations of the operations of the job with id i.
|
223
223
|
machines_matrix:
|
224
|
-
|
224
|
+
A list of lists of lists of integers if the
|
225
225
|
instance is flexible, or a list of lists of integers if the
|
226
226
|
instance is not flexible. The i-th list contains the machines
|
227
227
|
in which the operations of the job with id i can be processed.
|
job_shop_lib/_schedule.py
CHANGED
@@ -10,7 +10,7 @@ from job_shop_lib.exceptions import ValidationError
|
|
10
10
|
|
11
11
|
|
12
12
|
class Schedule:
|
13
|
-
"""Data structure to store a complete or partial solution for a particular
|
13
|
+
r"""Data structure to store a complete or partial solution for a particular
|
14
14
|
:class:`JobShopInstance`.
|
15
15
|
|
16
16
|
A schedule is a list of lists of :class:`ScheduledOperation` objects. Each
|
@@ -33,7 +33,7 @@ class Schedule:
|
|
33
33
|
A list of lists of :class:`ScheduledOperation` objects. Each
|
34
34
|
list represents the order of operations on a machine. If
|
35
35
|
not provided, the schedule is initialized as an empty schedule.
|
36
|
-
|
36
|
+
\**metadata:
|
37
37
|
Additional information about the schedule.
|
38
38
|
"""
|
39
39
|
|
@@ -30,9 +30,12 @@ def no_setup_time_calculator(
|
|
30
30
|
operation belongs.
|
31
31
|
|
32
32
|
Args:
|
33
|
-
dispatcher:
|
34
|
-
|
35
|
-
|
33
|
+
dispatcher:
|
34
|
+
The dispatcher instance.
|
35
|
+
operation:
|
36
|
+
The operation to be scheduled.
|
37
|
+
machine_id:
|
38
|
+
The id of the machine on which the operation is to be
|
36
39
|
scheduled.
|
37
40
|
|
38
41
|
Returns:
|
@@ -329,8 +332,10 @@ class Dispatcher:
|
|
329
332
|
:attr:`~job_shop_lib.Operation.machine_id` attribute is used.
|
330
333
|
|
331
334
|
Raises:
|
332
|
-
ValidationError:
|
333
|
-
|
335
|
+
ValidationError:
|
336
|
+
If the operation is not ready to be scheduled.
|
337
|
+
UninitializedAttributeError:
|
338
|
+
If the operation has multiple
|
334
339
|
machines in its list and no ``machine_id`` is provided.
|
335
340
|
"""
|
336
341
|
|
@@ -408,7 +413,7 @@ class Dispatcher:
|
|
408
413
|
condition: Callable[[DispatcherObserver], bool] = lambda _: True,
|
409
414
|
**kwargs,
|
410
415
|
) -> ObserverType:
|
411
|
-
"""Creates a new observer of the specified type or returns an existing
|
416
|
+
r"""Creates a new observer of the specified type or returns an existing
|
412
417
|
observer of the same type if it already exists in the dispatcher's list
|
413
418
|
of observers.
|
414
419
|
|
@@ -419,7 +424,7 @@ class Dispatcher:
|
|
419
424
|
A function that takes an observer and returns True if it is
|
420
425
|
the observer to be retrieved. By default, it returns True for
|
421
426
|
all observers.
|
422
|
-
|
427
|
+
\**kwargs:
|
423
428
|
Additional keyword arguments to be passed to the observer's
|
424
429
|
constructor.
|
425
430
|
"""
|
@@ -21,10 +21,13 @@ class OptimalOperationsObserver(DispatcherObserver):
|
|
21
21
|
operations.
|
22
22
|
|
23
23
|
Args:
|
24
|
-
dispatcher:
|
25
|
-
|
24
|
+
dispatcher:
|
25
|
+
The dispatcher instance to observe.
|
26
|
+
reference_schedule:
|
27
|
+
A complete schedule that represents the optimal
|
26
28
|
or reference solution.
|
27
|
-
subscribe:
|
29
|
+
subscribe:
|
30
|
+
If True, automatically subscribes to the dispatcher.
|
28
31
|
|
29
32
|
Raises:
|
30
33
|
ValidationError: If the reference schedule is incomplete or if it
|
@@ -11,7 +11,6 @@ import numpy as np
|
|
11
11
|
from numpy.typing import NDArray
|
12
12
|
import pandas as pd
|
13
13
|
|
14
|
-
from job_shop_lib.exceptions import ValidationError
|
15
14
|
from job_shop_lib.dispatching import Dispatcher
|
16
15
|
from job_shop_lib.dispatching.feature_observers import (
|
17
16
|
FeatureObserver,
|
@@ -86,19 +85,11 @@ class CompositeFeatureObserver(FeatureObserver):
|
|
86
85
|
for observer in dispatcher.subscribers
|
87
86
|
if isinstance(observer, FeatureObserver)
|
88
87
|
]
|
89
|
-
feature_types = self._get_feature_types_list(feature_types)
|
90
|
-
for observer in feature_observers:
|
91
|
-
if not set(observer.features.keys()).issubset(set(feature_types)):
|
92
|
-
raise ValidationError(
|
93
|
-
"The feature types observed by the feature observer "
|
94
|
-
f"{observer.__class__.__name__} are not a subset of the "
|
95
|
-
"feature types specified in the CompositeFeatureObserver."
|
96
|
-
f"Observer feature types: {observer.features.keys()}"
|
97
|
-
f"Composite feature types: {feature_types}"
|
98
|
-
)
|
99
88
|
self.feature_observers = feature_observers
|
100
89
|
self.column_names: dict[FeatureType, list[str]] = defaultdict(list)
|
101
|
-
super().__init__(
|
90
|
+
super().__init__(
|
91
|
+
dispatcher, subscribe=subscribe, feature_types=feature_types
|
92
|
+
)
|
102
93
|
self._set_column_names()
|
103
94
|
|
104
95
|
@classmethod
|
@@ -148,12 +139,17 @@ class CompositeFeatureObserver(FeatureObserver):
|
|
148
139
|
list
|
149
140
|
)
|
150
141
|
for observer in self.feature_observers:
|
151
|
-
for feature_type
|
142
|
+
for feature_type in self.supported_feature_types:
|
143
|
+
feature_matrix = observer.features.get(feature_type)
|
144
|
+
if feature_matrix is None:
|
145
|
+
continue
|
152
146
|
features[feature_type].append(feature_matrix)
|
153
147
|
|
154
148
|
self.features = {
|
155
|
-
feature_type: np.concatenate(
|
156
|
-
|
149
|
+
feature_type: np.concatenate(
|
150
|
+
feature_matrices, axis=1 # type: ignore[misc]
|
151
|
+
)
|
152
|
+
for feature_type, feature_matrices in features.items()
|
157
153
|
}
|
158
154
|
|
159
155
|
def _set_column_names(self):
|
@@ -177,34 +173,3 @@ class CompositeFeatureObserver(FeatureObserver):
|
|
177
173
|
out.append(f"{feature_type.value}:")
|
178
174
|
out.append(dataframe.to_string())
|
179
175
|
return "\n".join(out)
|
180
|
-
|
181
|
-
|
182
|
-
if __name__ == "__main__":
|
183
|
-
# from cProfile import Profile
|
184
|
-
import time
|
185
|
-
from job_shop_lib.benchmarking import load_benchmark_instance
|
186
|
-
from job_shop_lib.dispatching.rules import DispatchingRuleSolver
|
187
|
-
|
188
|
-
ta80 = load_benchmark_instance("ta80")
|
189
|
-
|
190
|
-
dispatcher_ = Dispatcher(ta80)
|
191
|
-
feature_observer_types_ = list(FeatureObserverType)
|
192
|
-
feature_observers_ = [
|
193
|
-
feature_observer_factory(
|
194
|
-
observer_type,
|
195
|
-
dispatcher=dispatcher_,
|
196
|
-
)
|
197
|
-
for observer_type in feature_observer_types_
|
198
|
-
# and not FeatureObserverType.EARLIEST_START_TIME
|
199
|
-
]
|
200
|
-
composite_observer_ = CompositeFeatureObserver(
|
201
|
-
dispatcher_, feature_observers=feature_observers_
|
202
|
-
)
|
203
|
-
solver = DispatchingRuleSolver(dispatching_rule="random")
|
204
|
-
# profiler = Profile()
|
205
|
-
# profiler.runcall(solver.solve, dispatcher_.instance, dispatcher_)
|
206
|
-
# profiler.print_stats("cumtime")
|
207
|
-
start = time.perf_counter()
|
208
|
-
solver.solve(dispatcher_.instance, dispatcher_)
|
209
|
-
end = time.perf_counter()
|
210
|
-
print(f"Time: {end - start}")
|
@@ -93,8 +93,8 @@ class EarliestStartTimeObserver(FeatureObserver):
|
|
93
93
|
# Cache:
|
94
94
|
operations_by_machine = dispatcher.instance.operations_by_machine
|
95
95
|
self._is_regular_instance = all(
|
96
|
-
len(
|
97
|
-
for
|
96
|
+
len(machine_ops) == len(operations_by_machine[0])
|
97
|
+
for machine_ops in operations_by_machine
|
98
98
|
)
|
99
99
|
if self._is_regular_instance:
|
100
100
|
self._job_ids = np.array(
|
@@ -1,5 +1,3 @@
|
|
1
|
-
"""Contains factory functions for creating :class:`FeatureObserver`s."""
|
2
|
-
|
3
1
|
from enum import Enum
|
4
2
|
|
5
3
|
from job_shop_lib.dispatching import DispatcherObserverConfig
|
@@ -57,13 +55,13 @@ def feature_observer_factory(
|
|
57
55
|
),
|
58
56
|
**kwargs,
|
59
57
|
) -> FeatureObserver:
|
60
|
-
"""Creates and returns a :class:`FeatureObserver` based on the specified
|
58
|
+
r"""Creates and returns a :class:`FeatureObserver` based on the specified
|
61
59
|
:class:`FeatureObserver` type.
|
62
60
|
|
63
61
|
Args:
|
64
62
|
feature_creator_type:
|
65
63
|
The type of :class:`FeatureObserver` to create.
|
66
|
-
|
64
|
+
\*\*kwargs:
|
67
65
|
Additional keyword arguments to pass to the
|
68
66
|
:class:`FeatureObserver` constructor.
|
69
67
|
|
@@ -148,49 +148,3 @@ class DispatchingRuleSolver(BaseSolver):
|
|
148
148
|
selected_operation = self.dispatching_rule(dispatcher)
|
149
149
|
machine_id = self.machine_chooser(dispatcher, selected_operation)
|
150
150
|
dispatcher.dispatch(selected_operation, machine_id)
|
151
|
-
|
152
|
-
|
153
|
-
if __name__ == "__main__":
|
154
|
-
import time
|
155
|
-
import cProfile
|
156
|
-
|
157
|
-
# import pstats
|
158
|
-
# from io import StringIO
|
159
|
-
from job_shop_lib.benchmarking import (
|
160
|
-
# load_benchmark_instance,
|
161
|
-
load_all_benchmark_instances,
|
162
|
-
)
|
163
|
-
|
164
|
-
# from job_shop_lib.dispatching.rules._dispatching_rules_functions import (
|
165
|
-
# most_work_remaining_rule_2,
|
166
|
-
# )
|
167
|
-
|
168
|
-
# ta_instances = [
|
169
|
-
# load_benchmark_instance(f"ta{i:02d}") for i in range(1, 81)
|
170
|
-
# ]
|
171
|
-
ta_instances = load_all_benchmark_instances().values()
|
172
|
-
solver = DispatchingRuleSolver(
|
173
|
-
dispatching_rule="most_work_remaining", ready_operations_filter=None
|
174
|
-
)
|
175
|
-
|
176
|
-
start = time.perf_counter()
|
177
|
-
|
178
|
-
# Create a Profile object
|
179
|
-
profiler = cProfile.Profile()
|
180
|
-
|
181
|
-
# Run the code under profiling
|
182
|
-
# profiler.enable()
|
183
|
-
for instance_ in ta_instances:
|
184
|
-
solver.solve(instance_)
|
185
|
-
# profiler.disable()
|
186
|
-
|
187
|
-
end = time.perf_counter()
|
188
|
-
|
189
|
-
# Print elapsed time
|
190
|
-
print(f"Elapsed time: {end - start:.2f} seconds.")
|
191
|
-
|
192
|
-
# Print profiling results
|
193
|
-
# s = StringIO()
|
194
|
-
# ps = pstats.Stats(profiler, stream=s).sort_stats("cumulative")
|
195
|
-
# profiler.print_stats("cumtime") # Print top 20 time-consuming functions
|
196
|
-
# print(s.getvalue())
|
@@ -35,7 +35,23 @@ def first_come_first_served_rule(dispatcher: Dispatcher) -> Operation:
|
|
35
35
|
|
36
36
|
|
37
37
|
def most_work_remaining_rule(dispatcher: Dispatcher) -> Operation:
|
38
|
-
"""Dispatches the operation which job has the most remaining work.
|
38
|
+
"""Dispatches the operation which job has the most remaining work.
|
39
|
+
|
40
|
+
The remaining work of a job is defined as the sum of the durations of
|
41
|
+
all unscheduled operations in that job. The operation with the highest
|
42
|
+
remaining work is selected for dispatching. Note that uncompleted
|
43
|
+
but scheduled operations are not considered in this rule, only
|
44
|
+
unscheduled operations are taken into account.
|
45
|
+
|
46
|
+
Args:
|
47
|
+
dispatcher:
|
48
|
+
The :class:`~job_shop_lib.dispatching.Dispatcher` instance
|
49
|
+
containing the job shop instance and the current state of the
|
50
|
+
schedule.
|
51
|
+
|
52
|
+
Returns:
|
53
|
+
The operation that belongs to the job with the most remaining work.
|
54
|
+
"""
|
39
55
|
job_remaining_work = [0] * dispatcher.instance.num_jobs
|
40
56
|
for operation in dispatcher.unscheduled_operations():
|
41
57
|
job_remaining_work[operation.job_id] += operation.duration
|
@@ -47,7 +63,23 @@ def most_work_remaining_rule(dispatcher: Dispatcher) -> Operation:
|
|
47
63
|
|
48
64
|
|
49
65
|
def most_operations_remaining_rule(dispatcher: Dispatcher) -> Operation:
|
50
|
-
"""Dispatches the operation which job has the most remaining operations.
|
66
|
+
"""Dispatches the operation which job has the most remaining operations.
|
67
|
+
|
68
|
+
The remaining operations of a job are defined as the number of
|
69
|
+
uncompleted operations in that job. The operation with the highest
|
70
|
+
number of remaining operations is selected for dispatching. Note that
|
71
|
+
uncompleted but scheduled operations are considered in this rule.
|
72
|
+
|
73
|
+
Args:
|
74
|
+
dispatcher:
|
75
|
+
The :class:`~job_shop_lib.dispatching.Dispatcher` instance
|
76
|
+
containing the job shop instance and the current state of the
|
77
|
+
schedule.
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
The operation that belongs to the job with the most remaining
|
81
|
+
operations.
|
82
|
+
"""
|
51
83
|
job_remaining_operations = [0] * dispatcher.instance.num_jobs
|
52
84
|
for operation in dispatcher.uncompleted_operations():
|
53
85
|
job_remaining_operations[operation.job_id] += 1
|
@@ -69,7 +101,9 @@ def score_based_rule(
|
|
69
101
|
"""Creates a dispatching rule based on a scoring function.
|
70
102
|
|
71
103
|
Args:
|
72
|
-
score_function:
|
104
|
+
score_function:
|
105
|
+
A function that takes a
|
106
|
+
:class:`~job_shop_lib.dispatching.Dispatcher` instance as input
|
73
107
|
and returns a list of scores for each job.
|
74
108
|
|
75
109
|
Returns:
|
@@ -97,7 +131,9 @@ def score_based_rule_with_tie_breaker(
|
|
97
131
|
still a tie, the third scoring function is used, and so on.
|
98
132
|
|
99
133
|
Args:
|
100
|
-
score_functions
|
134
|
+
score_functions
|
135
|
+
A list of scoring functions that take a
|
136
|
+
:class:`~job_shop_lib.dispatching.Dispatcher`
|
101
137
|
instance as input and return a list of scores for each job.
|
102
138
|
"""
|
103
139
|
|
@@ -123,7 +159,21 @@ def score_based_rule_with_tie_breaker(
|
|
123
159
|
|
124
160
|
|
125
161
|
def shortest_processing_time_score(dispatcher: Dispatcher) -> list[int]:
|
126
|
-
"""Scores each job based on the duration of the next operation.
|
162
|
+
"""Scores each job based on the duration of the next operation.
|
163
|
+
|
164
|
+
The score is the negative duration of the next operation in each job.
|
165
|
+
This means that jobs with shorter next operations will have higher scores.
|
166
|
+
|
167
|
+
Args:
|
168
|
+
dispatcher:
|
169
|
+
The :class:`~job_shop_lib.dispatching.Dispatcher` instance
|
170
|
+
containing the job shop instance and the current state of the
|
171
|
+
schedule.
|
172
|
+
|
173
|
+
Returns:
|
174
|
+
A list of scores for each job, where the score is the negative
|
175
|
+
duration of the next operation in that job.
|
176
|
+
"""
|
127
177
|
num_jobs = dispatcher.instance.num_jobs
|
128
178
|
scores = [0] * num_jobs
|
129
179
|
for operation in dispatcher.available_operations():
|
@@ -132,11 +182,26 @@ def shortest_processing_time_score(dispatcher: Dispatcher) -> list[int]:
|
|
132
182
|
|
133
183
|
|
134
184
|
def first_come_first_served_score(dispatcher: Dispatcher) -> list[int]:
|
135
|
-
"""Scores each job based on the position of the next operation.
|
185
|
+
"""Scores each job based on the position of the next operation.
|
186
|
+
|
187
|
+
The score is the negative position of the next operation in each job.
|
188
|
+
This means that jobs with operations that are earlier in the job will have
|
189
|
+
higher scores.
|
190
|
+
|
191
|
+
Args:
|
192
|
+
dispatcher:
|
193
|
+
The :class:`~job_shop_lib.dispatching.Dispatcher` instance
|
194
|
+
containing the job shop instance and the current state of the
|
195
|
+
schedule.
|
196
|
+
|
197
|
+
Returns:
|
198
|
+
A list of scores for each job, where the score is the negative
|
199
|
+
position of the next operation in that job.
|
200
|
+
"""
|
136
201
|
num_jobs = dispatcher.instance.num_jobs
|
137
202
|
scores = [0] * num_jobs
|
138
203
|
for operation in dispatcher.available_operations():
|
139
|
-
scores[operation.job_id] = operation.operation_id
|
204
|
+
scores[operation.job_id] = -operation.operation_id
|
140
205
|
return scores
|
141
206
|
|
142
207
|
|
@@ -145,8 +210,8 @@ class MostWorkRemainingScorer: # pylint: disable=too-few-public-methods
|
|
145
210
|
|
146
211
|
This class is conceptually a function: it can be called with a
|
147
212
|
:class:`~job_shop_lib.dispatching.Dispatcher` instance as input, and it
|
148
|
-
returns a list of scores for each job. The reason for using a class
|
149
|
-
of a function is to cache the observers that are created for each
|
213
|
+
returns a list of scores for each job. The reason for using a class
|
214
|
+
instead of a function is to cache the observers that are created for each
|
150
215
|
dispatcher instance. This way, the observers do not have to be retrieved
|
151
216
|
every time the function is called.
|
152
217
|
|
@@ -195,10 +260,26 @@ class MostWorkRemainingScorer: # pylint: disable=too-few-public-methods
|
|
195
260
|
observer_based_most_work_remaining_rule = score_based_rule(
|
196
261
|
MostWorkRemainingScorer()
|
197
262
|
)
|
263
|
+
"""Dispatching rule that uses the :class:`MostWorkRemainingScorer` to select
|
264
|
+
the next operation."""
|
198
265
|
|
199
266
|
|
200
267
|
def most_operations_remaining_score(dispatcher: Dispatcher) -> list[int]:
|
201
|
-
"""Scores each job based on the remaining operations in the job.
|
268
|
+
"""Scores each job based on the remaining operations in the job.
|
269
|
+
|
270
|
+
The score is the number of uncompleted operations in each job. This means
|
271
|
+
that jobs with more uncompleted operations will have higher scores.
|
272
|
+
|
273
|
+
Args:
|
274
|
+
dispatcher:
|
275
|
+
The :class:`~job_shop_lib.dispatching.Dispatcher` instance
|
276
|
+
containing the job shop instance and the current state of the
|
277
|
+
schedule.
|
278
|
+
|
279
|
+
Returns:
|
280
|
+
A list of scores for each job, where the score is the number of
|
281
|
+
uncompleted operations in that job.
|
282
|
+
"""
|
202
283
|
num_jobs = dispatcher.instance.num_jobs
|
203
284
|
scores = [0] * num_jobs
|
204
285
|
for operation in dispatcher.uncompleted_operations():
|
@@ -207,7 +288,21 @@ def most_operations_remaining_score(dispatcher: Dispatcher) -> list[int]:
|
|
207
288
|
|
208
289
|
|
209
290
|
def random_score(dispatcher: Dispatcher) -> list[int]:
|
210
|
-
"""Scores each job randomly.
|
291
|
+
"""Scores each job randomly.
|
292
|
+
|
293
|
+
This function generates a random score for each job in the job shop
|
294
|
+
instance. The scores are integers between 0 and 100, inclusive.
|
295
|
+
|
296
|
+
Args:
|
297
|
+
dispatcher:
|
298
|
+
The :class:`~job_shop_lib.dispatching.Dispatcher` instance
|
299
|
+
containing the job shop instance and the current state of the
|
300
|
+
schedule.
|
301
|
+
|
302
|
+
Returns:
|
303
|
+
A list of random scores for each job, where each score is an integer
|
304
|
+
between 0 and 100.
|
305
|
+
"""
|
211
306
|
return [
|
212
307
|
random.randint(0, 100) for _ in range(dispatcher.instance.num_jobs)
|
213
308
|
]
|
@@ -129,16 +129,18 @@ class GeneralInstanceGenerator(InstanceGenerator):
|
|
129
129
|
num_machines: int | None = None,
|
130
130
|
) -> JobShopInstance:
|
131
131
|
if num_jobs is None:
|
132
|
-
num_jobs =
|
133
|
-
self.
|
132
|
+
num_jobs = int(
|
133
|
+
self.rng.integers(
|
134
|
+
self.num_jobs_range[0], self.num_jobs_range[1] + 1
|
135
|
+
)
|
134
136
|
)
|
135
137
|
|
136
138
|
if num_machines is None:
|
137
139
|
min_num_machines, max_num_machines = self.num_machines_range
|
138
140
|
if not self.allow_less_jobs_than_machines:
|
139
141
|
min_num_machines = min(num_jobs, max_num_machines)
|
140
|
-
num_machines =
|
141
|
-
min_num_machines, max_num_machines + 1
|
142
|
+
num_machines = int(
|
143
|
+
self.rng.integers(min_num_machines, max_num_machines + 1)
|
142
144
|
)
|
143
145
|
elif (
|
144
146
|
not self.allow_less_jobs_than_machines and num_jobs < num_machines
|
@@ -9,6 +9,7 @@ connected with machine ones. All machine nodes are connected between them, and
|
|
9
9
|
all operation nodes from the same job are connected by non-directed edges too.
|
10
10
|
|
11
11
|
References:
|
12
|
+
|
12
13
|
- Junyoung Park, Sanjar Bakhtiyar, and Jinkyoo Park. Schedulenet: Learn to
|
13
14
|
solve multi-agent scheduling problems with reinforcement learning. ArXiv,
|
14
15
|
abs/2106.03051, 2021.
|
@@ -166,7 +166,7 @@ class JobShopGraph:
|
|
166
166
|
v_of_edge: Node | int,
|
167
167
|
**attr,
|
168
168
|
) -> None:
|
169
|
-
"""Adds an edge to the graph.
|
169
|
+
r"""Adds an edge to the graph.
|
170
170
|
|
171
171
|
It automatically determines the edge type based on the source and
|
172
172
|
destination nodes unless explicitly provided in the ``attr`` argument
|
@@ -182,7 +182,7 @@ class JobShopGraph:
|
|
182
182
|
The destination node of the edge. If it is a :class:`Node`,
|
183
183
|
its ``node_id`` is used as the destination. Otherwise, it
|
184
184
|
is assumed to be the ``node_id`` of the destination.
|
185
|
-
|
185
|
+
\**attr:
|
186
186
|
Additional attributes to be added to the edge.
|
187
187
|
|
188
188
|
Raises:
|
@@ -277,7 +277,8 @@ class MultiJobShopGraphEnv(gym.Env):
|
|
277
277
|
options: Additional options for reset (currently unused).
|
278
278
|
|
279
279
|
Returns:
|
280
|
-
|
280
|
+
tuple[ObservationDict, dict[str, Any]]:
|
281
|
+
|
281
282
|
- ObservationDict: The initial observation of the environment.
|
282
283
|
- dict: An info dictionary containing additional information about
|
283
284
|
the reset state. This may include details about the generated
|
@@ -305,7 +306,7 @@ class MultiJobShopGraphEnv(gym.Env):
|
|
305
306
|
def step(
|
306
307
|
self, action: tuple[int, int]
|
307
308
|
) -> tuple[ObservationDict, float, bool, bool, dict[str, Any]]:
|
308
|
-
"""Takes a step in the environment.
|
309
|
+
r"""Takes a step in the environment.
|
309
310
|
|
310
311
|
Args:
|
311
312
|
action:
|
@@ -315,7 +316,7 @@ class MultiJobShopGraphEnv(gym.Env):
|
|
315
316
|
operation.
|
316
317
|
|
317
318
|
Returns:
|
318
|
-
|
319
|
+
tuple[ObservationDict, float, bool, bool, dict[str, Any]]:
|
319
320
|
|
320
321
|
- The observation of the environment.
|
321
322
|
- The reward obtained.
|
@@ -247,9 +247,11 @@ class ResourceTaskGraphObservation(ObservationWrapper, Generic[EnvType]):
|
|
247
247
|
def _get_start_from_zero_mappings(
|
248
248
|
original_indices_dict: dict[str, NDArray[np.int32]],
|
249
249
|
) -> dict[str, dict[int, int]]:
|
250
|
-
mappings = {}
|
250
|
+
mappings: dict[str, dict[int, int]] = {}
|
251
251
|
for key, indices in original_indices_dict.items():
|
252
|
-
mappings[key] = {
|
252
|
+
mappings[key] = {
|
253
|
+
idx: i for i, idx in enumerate(indices) # type: ignore[misc]
|
254
|
+
} # idx is an integer (false positive)
|
253
255
|
return mappings
|
254
256
|
|
255
257
|
def _create_node_features_dict(
|
@@ -318,7 +320,7 @@ class ResourceTaskGraphObservation(ObservationWrapper, Generic[EnvType]):
|
|
318
320
|
~removed_nodes_of_this_type
|
319
321
|
]
|
320
322
|
original_ids_dict[node_type] = np.where(
|
321
|
-
~removed_nodes_of_this_type
|
323
|
+
~removed_nodes_of_this_type # type: ignore[assignment]
|
322
324
|
)[0]
|
323
325
|
|
324
326
|
return removed_nodes_dict, original_ids_dict
|
@@ -3,10 +3,12 @@
|
|
3
3
|
from copy import deepcopy
|
4
4
|
from collections.abc import Callable, Sequence
|
5
5
|
from typing import Any
|
6
|
+
import warnings
|
6
7
|
|
7
8
|
import matplotlib.pyplot as plt
|
8
9
|
import gymnasium as gym
|
9
10
|
import numpy as np
|
11
|
+
|
10
12
|
from numpy.typing import NDArray
|
11
13
|
|
12
14
|
from job_shop_lib import JobShopInstance, Operation
|
@@ -207,17 +209,34 @@ class SingleJobShopGraphEnv(gym.Env):
|
|
207
209
|
"""Returns current makespan of partial schedule."""
|
208
210
|
return self.dispatcher.schedule.makespan()
|
209
211
|
|
210
|
-
def machine_utilization(
|
211
|
-
|
212
|
+
def machine_utilization( # noqa: DOC201,DOC203
|
213
|
+
self,
|
214
|
+
) -> NDArray[np.float32]:
|
215
|
+
"""Returns utilization percentage for each machine.
|
216
|
+
|
217
|
+
Returns:
|
218
|
+
Utilization percentage for each machine as a numpy array.
|
219
|
+
|
220
|
+
.. deprecated:: 1.1.2
|
221
|
+
This method is deprecated and will be removed in version 2.0.0.
|
222
|
+
"""
|
223
|
+
warnings.warn(
|
224
|
+
"machine_utilization is deprecated and will be removed in "
|
225
|
+
"version 2.0.0",
|
226
|
+
DeprecationWarning,
|
227
|
+
stacklevel=2,
|
228
|
+
)
|
212
229
|
total_time = max(1, self.current_makespan()) # Avoid division by zero
|
213
|
-
machine_busy_time = np.zeros(
|
230
|
+
machine_busy_time = np.zeros(
|
231
|
+
self.instance.num_machines, dtype=np.float32
|
232
|
+
)
|
214
233
|
|
215
234
|
for m_id, m_schedule in enumerate(self.dispatcher.schedule.schedule):
|
216
235
|
machine_busy_time[m_id] = sum(
|
217
236
|
op.operation.duration for op in m_schedule
|
218
237
|
)
|
219
238
|
|
220
|
-
return machine_busy_time / total_time
|
239
|
+
return machine_busy_time / total_time # type: ignore[return-value]
|
221
240
|
|
222
241
|
def _get_observation_space(self) -> gym.spaces.Dict:
|
223
242
|
"""Returns the observation space dictionary."""
|
@@ -414,28 +433,3 @@ class SingleJobShopGraphEnv(gym.Env):
|
|
414
433
|
raise ValidationError(
|
415
434
|
f"Operation {next_operation} requires a machine_id"
|
416
435
|
)
|
417
|
-
|
418
|
-
|
419
|
-
if __name__ == "__main__":
|
420
|
-
from job_shop_lib.dispatching.feature_observers import (
|
421
|
-
FeatureObserverType,
|
422
|
-
FeatureType,
|
423
|
-
)
|
424
|
-
from job_shop_lib.graphs import build_disjunctive_graph
|
425
|
-
from job_shop_lib.benchmarking import load_benchmark_instance
|
426
|
-
|
427
|
-
instance = load_benchmark_instance("ft06")
|
428
|
-
job_shop_graph_ = build_disjunctive_graph(instance)
|
429
|
-
feature_observer_configs_: list[DispatcherObserverConfig] = [
|
430
|
-
DispatcherObserverConfig(
|
431
|
-
FeatureObserverType.IS_READY,
|
432
|
-
kwargs={"feature_types": [FeatureType.JOBS]},
|
433
|
-
)
|
434
|
-
]
|
435
|
-
|
436
|
-
env = SingleJobShopGraphEnv(
|
437
|
-
job_shop_graph=job_shop_graph_,
|
438
|
-
feature_observer_configs=feature_observer_configs_,
|
439
|
-
render_mode="save_video",
|
440
|
-
render_config={"video_config": {"fps": 4}},
|
441
|
-
)
|
@@ -70,14 +70,16 @@ def get_partial_gantt_chart_plotter(
|
|
70
70
|
show_available_operations: bool = False,
|
71
71
|
**kwargs: Any,
|
72
72
|
) -> PartialGanttChartPlotter:
|
73
|
-
"""Returns a function that plots a Gantt chart for an unfinished schedule.
|
73
|
+
r"""Returns a function that plots a Gantt chart for an unfinished schedule.
|
74
74
|
|
75
75
|
Args:
|
76
|
-
title:
|
77
|
-
|
76
|
+
title:
|
77
|
+
The title of the Gantt chart.
|
78
|
+
cmap:
|
79
|
+
The name of the colormap to use.
|
78
80
|
show_available_operations:
|
79
81
|
Whether to show the available operations in the Gantt chart.
|
80
|
-
|
82
|
+
\*\*kwargs: Additional keyword arguments to pass to the
|
81
83
|
:func:`plot_gantt_chart` function.
|
82
84
|
|
83
85
|
Returns:
|
@@ -366,18 +366,25 @@ def plot_disjunctive_graph(
|
|
366
366
|
|
367
367
|
# Add machine colors to the legend
|
368
368
|
if show_machine_colors_in_legend:
|
369
|
+
label_color_pairs = []
|
370
|
+
sorted_machine_colors = sorted(
|
371
|
+
machine_colors.items(), key=lambda x: x[0]
|
372
|
+
)
|
373
|
+
for machine_id, color in sorted_machine_colors:
|
374
|
+
label = None
|
375
|
+
if machine_id == -1:
|
376
|
+
continue
|
377
|
+
if machine_labels is not None:
|
378
|
+
label = machine_labels[machine_id]
|
379
|
+
elif machine_id >= 0:
|
380
|
+
label = f"Machine {machine_id}"
|
381
|
+
|
382
|
+
if label: # Add patch if a label was determined
|
383
|
+
label_color_pairs.append((label, color))
|
384
|
+
|
369
385
|
machine_patches = [
|
370
|
-
matplotlib.patches.Patch(
|
371
|
-
|
372
|
-
label=(
|
373
|
-
machine_labels[machine_id]
|
374
|
-
if machine_labels is not None
|
375
|
-
else f"Machine {machine_id}"
|
376
|
-
),
|
377
|
-
)
|
378
|
-
for machine_id, color in sorted(
|
379
|
-
machine_colors.items(), key=lambda x: x[0]
|
380
|
-
)
|
386
|
+
matplotlib.patches.Patch(color=color, label=label)
|
387
|
+
for label, color in sorted(label_color_pairs)
|
381
388
|
]
|
382
389
|
handles.extend(machine_patches)
|
383
390
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: job-shop-lib
|
3
|
-
Version: 1.1.
|
3
|
+
Version: 1.1.3
|
4
4
|
Summary: An easy-to-use and modular Python library for the Job Shop Scheduling Problem (JSSP)
|
5
5
|
License: MIT
|
6
6
|
Author: Pabloo22
|
@@ -17,7 +17,8 @@ Requires-Dist: imageio[ffmpeg] (>=2.34.1,<3.0.0)
|
|
17
17
|
Requires-Dist: matplotlib (>=3,<4)
|
18
18
|
Requires-Dist: networkx (>=3,<4)
|
19
19
|
Requires-Dist: numpy (>=1.26.4,<3.0.0)
|
20
|
-
Requires-Dist: ortools (>=9.9,<10.0)
|
20
|
+
Requires-Dist: ortools (>=9.9,<10.0) ; sys_platform != "darwin"
|
21
|
+
Requires-Dist: ortools (>=9.9,<9.13) ; sys_platform == "darwin"
|
21
22
|
Requires-Dist: pyarrow (>=15,<21)
|
22
23
|
Requires-Dist: pygraphviz (>=1.12,<2.0) ; extra == "pygraphviz"
|
23
24
|
Description-Content-Type: text/markdown
|
@@ -30,6 +31,7 @@ Description-Content-Type: text/markdown
|
|
30
31
|
|
31
32
|
[](https://github.com/Pabloo22/job_shop_lib/actions/workflows/tests.yaml)
|
32
33
|
[](https://job-shop-lib.readthedocs.io/en/latest/?badge=latest)
|
34
|
+
[](https://codecov.io/gh/Pabloo22/job_shop_lib)
|
33
35
|

|
34
36
|
[](https://github.com/psf/black)
|
35
37
|
[](https://opensource.org/licenses/MIT)
|
@@ -84,6 +86,24 @@ pip install job-shop-lib
|
|
84
86
|
|
85
87
|
<!-- end key features -->
|
86
88
|
|
89
|
+
## Publication :scroll:
|
90
|
+
|
91
|
+
For an in-depth explanation of the library (v1.0.0), including its design, features, reinforcement learning environments, and some experiments, please refer to my [Bachelor's thesis](https://www.arxiv.org/abs/2506.13781).
|
92
|
+
|
93
|
+
You can also cite the library using the following BibTeX entry:
|
94
|
+
|
95
|
+
```bibtex
|
96
|
+
@misc{arino2025jobshoplib,
|
97
|
+
title={Solving the Job Shop Scheduling Problem with Graph Neural Networks: A Customizable Reinforcement Learning Environment},
|
98
|
+
author={Pablo Ariño Fernández},
|
99
|
+
year={2025},
|
100
|
+
eprint={2506.13781},
|
101
|
+
archivePrefix={arXiv},
|
102
|
+
primaryClass={cs.LG},
|
103
|
+
url={https://arxiv.org/abs/2506.13781},
|
104
|
+
}
|
105
|
+
```
|
106
|
+
|
87
107
|
## Some Examples :rocket:
|
88
108
|
|
89
109
|
### Create a Job Shop Instance
|
@@ -328,31 +348,82 @@ plt.show()
|
|
328
348
|
|
329
349
|
The library generalizes this graph by allowing the addition of job nodes and a global one (see `build_resource_task_graph_with_jobs` and `build_resource_task_graph`).
|
330
350
|
|
331
|
-
|
351
|
+
### Gymnasium Environments
|
332
352
|
|
333
|
-
|
353
|
+
<div align="center">
|
354
|
+
<img src="docs/source/images/rl_diagram.png">
|
355
|
+
</div>
|
356
|
+
<br>
|
334
357
|
|
335
|
-
<!-- start installation development -->
|
336
358
|
|
337
|
-
|
359
|
+
The `SingleJobShopGraphEnv` allows to learn from a single job shop instance, while the `MultiJobShopGraphEnv` generates a new instance at each reset. For an in-depth explanation of the environments see chapter 7 of my [Bachelor's thesis](https://www.arxiv.org/abs/2506.13781).
|
338
360
|
|
339
|
-
```
|
340
|
-
|
341
|
-
|
342
|
-
|
361
|
+
```python
|
362
|
+
from IPython.display import clear_output
|
363
|
+
|
364
|
+
from job_shop_lib.reinforcement_learning import (
|
365
|
+
# MakespanReward,
|
366
|
+
SingleJobShopGraphEnv,
|
367
|
+
ObservationSpaceKey,
|
368
|
+
IdleTimeReward,
|
369
|
+
ObservationDict,
|
370
|
+
)
|
371
|
+
from job_shop_lib.dispatching.feature_observers import (
|
372
|
+
FeatureObserverType,
|
373
|
+
FeatureType,
|
374
|
+
)
|
375
|
+
from job_shop_lib.dispatching import DispatcherObserverConfig
|
376
|
+
|
377
|
+
|
378
|
+
instance = load_benchmark_instance("ft06")
|
379
|
+
job_shop_graph = build_disjunctive_graph(instance)
|
380
|
+
feature_observer_configs = [
|
381
|
+
DispatcherObserverConfig(
|
382
|
+
FeatureObserverType.IS_READY,
|
383
|
+
kwargs={"feature_types": [FeatureType.JOBS]},
|
384
|
+
)
|
385
|
+
]
|
386
|
+
|
387
|
+
env = SingleJobShopGraphEnv(
|
388
|
+
job_shop_graph=job_shop_graph,
|
389
|
+
feature_observer_configs=feature_observer_configs,
|
390
|
+
reward_function_config=DispatcherObserverConfig(IdleTimeReward),
|
391
|
+
render_mode="human", # Try "save_video"
|
392
|
+
render_config={
|
393
|
+
"video_config": {"fps": 4}
|
394
|
+
}
|
395
|
+
)
|
343
396
|
|
344
|
-
2. Install [poetry](https://python-poetry.org/docs/) if you don't have it already:
|
345
397
|
|
346
|
-
|
347
|
-
|
348
|
-
|
398
|
+
def random_action(observation: ObservationDict) -> tuple[int, int]:
|
399
|
+
ready_jobs = []
|
400
|
+
for job_id, is_ready in enumerate(
|
401
|
+
observation[ObservationSpaceKey.JOBS.value].ravel()
|
402
|
+
):
|
403
|
+
if is_ready == 1.0:
|
404
|
+
ready_jobs.append(job_id)
|
349
405
|
|
350
|
-
|
351
|
-
|
352
|
-
|
406
|
+
job_id = random.choice(ready_jobs)
|
407
|
+
machine_id = -1 # We can use -1 if each operation can only be scheduled
|
408
|
+
# on one machine.
|
409
|
+
return (job_id, machine_id)
|
410
|
+
|
411
|
+
|
412
|
+
done = False
|
413
|
+
obs, _ = env.reset()
|
414
|
+
while not done:
|
415
|
+
action = random_action(obs)
|
416
|
+
obs, reward, done, *_ = env.step(action)
|
417
|
+
if env.render_mode == "human":
|
418
|
+
env.render()
|
419
|
+
clear_output(wait=True)
|
420
|
+
|
421
|
+
if env.render_mode == "save_video" or env.render_mode == "save_gif":
|
422
|
+
env.render()
|
353
423
|
```
|
354
424
|
|
355
|
-
|
425
|
+
## Contributing :handshake:
|
426
|
+
Any contribution is welcome, whether it's a small bug or documentation fix or a new feature! See the [CONTRIBUTING.md](CONTRIBUTING.md) file for details on how to contribute to this project.
|
356
427
|
|
357
428
|
## License :scroll:
|
358
429
|
|
@@ -389,5 +460,5 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
389
460
|
- E. Taillard, "Benchmarks for basic scheduling problems," European
|
390
461
|
Journal of Operational Research, vol. 64, no. 2, pp. 278–285, 1993.
|
391
462
|
|
392
|
-
- Park, Junyoung, Sanjar Bakhtiyar, and Jinkyoo Park. "ScheduleNet: Learn to solve multi-agent scheduling problems with reinforcement learning." arXiv preprint arXiv:2106.03051, 2021.
|
463
|
+
- Park, Junyoung, Sanjar Bakhtiyar, and Jinkyoo Park. "ScheduleNet: Learn to solve multi-agent scheduling problems with reinforcement learning." arXiv preprint arXiv:2106.03051, 2021.
|
393
464
|
|
@@ -1,8 +1,8 @@
|
|
1
|
-
job_shop_lib/__init__.py,sha256=
|
1
|
+
job_shop_lib/__init__.py,sha256=y-1uRgW-6xUddIIj3ksBlqxbSMJZkLZCSqBELQhL5mw,639
|
2
2
|
job_shop_lib/_base_solver.py,sha256=p17XmtufNc9Y481cqZUT45pEkUmmW1HWG53dfhIBJH8,1363
|
3
|
-
job_shop_lib/_job_shop_instance.py,sha256=
|
3
|
+
job_shop_lib/_job_shop_instance.py,sha256=FkMBy9Yb8cNEGswI9vlN3Wh4mhtEX-QuDbKvSYUOXcM,18361
|
4
4
|
job_shop_lib/_operation.py,sha256=lwCjgXwWlgESFuV3Yh4SCVofPGCd3hJU4vnK7peREac,4235
|
5
|
-
job_shop_lib/_schedule.py,sha256=
|
5
|
+
job_shop_lib/_schedule.py,sha256=PX3wOv9Cw8NgjBLV3yDJW0mNl7a25nvoEV5Hdv7R_-g,11943
|
6
6
|
job_shop_lib/_scheduled_operation.py,sha256=czrGr87EOTlO2NPolIN5CDigeiCzvQEyra5IZPwSFZc,2801
|
7
7
|
job_shop_lib/benchmarking/__init__.py,sha256=JPnCw5mK7sADAW0HctVKHEDRw22afp9caNh2eUS36Ys,3290
|
8
8
|
job_shop_lib/benchmarking/_load_benchmark.py,sha256=-cgyx0Kn6uAc3KdGFSQb6eUVQjQggmpVKOH9qusNkXI,2930
|
@@ -10,20 +10,20 @@ job_shop_lib/benchmarking/benchmark_instances.json,sha256=F9EvyzFwVxiKAN6rQTsrMh
|
|
10
10
|
job_shop_lib/constraint_programming/__init__.py,sha256=kKQRUxxS_nVFUdXGnf4bQOD9mqrXxZZWElS753A4YiA,454
|
11
11
|
job_shop_lib/constraint_programming/_ortools_solver.py,sha256=oMPeA2VHoYX1ZvmygQ8kYew40ITLAQATmM4OhgVFuXM,10482
|
12
12
|
job_shop_lib/dispatching/__init__.py,sha256=eyiCpCWIsx3LDoSOtPImjYAkI6R8t93kz56hM03WODE,2558
|
13
|
-
job_shop_lib/dispatching/_dispatcher.py,sha256=
|
13
|
+
job_shop_lib/dispatching/_dispatcher.py,sha256=KnV_Kry3Ie81WbKhdpRQtOMsuFDNCuh5Kp2ZnelM-R8,23835
|
14
14
|
job_shop_lib/dispatching/_dispatcher_observer_config.py,sha256=QF2d3rJWwmvutQBAkKxzQ1toJs6eMelT404LGS2z9HQ,2467
|
15
15
|
job_shop_lib/dispatching/_factories.py,sha256=j3MhIwVXiq-B8JMit72ObvXSa2sdgWNhUD86gghL6Gg,4689
|
16
16
|
job_shop_lib/dispatching/_history_observer.py,sha256=Vl8rQaxekUeEB-AyNxyC3c76zQakeh-rdri2iDnZvXw,610
|
17
|
-
job_shop_lib/dispatching/_optimal_operations_observer.py,sha256=
|
17
|
+
job_shop_lib/dispatching/_optimal_operations_observer.py,sha256=2EYxevjpeGMP3do-m0ZmtmjIjmNcxrWOSKzN_bW37gQ,4247
|
18
18
|
job_shop_lib/dispatching/_ready_operation_filters.py,sha256=brhmhoyyoZ98wAEEfneZC-CD-aw9SerZHGMB1DpK8HY,5749
|
19
19
|
job_shop_lib/dispatching/_start_time_calculators.py,sha256=sEtInDnFW9gsKDUEDUGQBaIjDWgCkFYvBca46j8XYfE,6408
|
20
20
|
job_shop_lib/dispatching/_unscheduled_operations_observer.py,sha256=0he-j4OlvqtXAJZD5x1nuBnUKqZUfftVx9NT3CVxPyg,2708
|
21
21
|
job_shop_lib/dispatching/feature_observers/__init__.py,sha256=EuJLvSpJpoXUK8A4UuC2k6Mpa293ZR3oCnnvYivIBtU,2240
|
22
|
-
job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py,sha256=
|
22
|
+
job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py,sha256=tpvqTLIcNmbYROSFT62LiUZ_tI4fHWL_qCULKK43BU4,6429
|
23
23
|
job_shop_lib/dispatching/feature_observers/_duration_observer.py,sha256=fbkUIVScF1iNjdVCYr1ImQm53TfahvVnGXhsRAsgdzY,4129
|
24
|
-
job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py,sha256=
|
25
|
-
job_shop_lib/dispatching/feature_observers/_factory.py,sha256=
|
26
|
-
job_shop_lib/dispatching/feature_observers/_feature_observer.py,sha256=
|
24
|
+
job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py,sha256=AQIjVp7VRDnb5GuYZlLUwk-xiXSqbsxJW-Ji7NjLoAw,11452
|
25
|
+
job_shop_lib/dispatching/feature_observers/_factory.py,sha256=NyXYK5A1hXsYEeEqngwVRNAFkevY95DglheeqyfFv8s,3217
|
26
|
+
job_shop_lib/dispatching/feature_observers/_feature_observer.py,sha256=qbgtMUicQ5FWS-Ql4Izjsj4QrevfOGlWzoJ0JlVSLH0,8668
|
27
27
|
job_shop_lib/dispatching/feature_observers/_is_completed_observer.py,sha256=EYJOyWL8ApUElLucoHnFlt0g2Ior_1yO7Q8V3FU_Qog,3576
|
28
28
|
job_shop_lib/dispatching/feature_observers/_is_ready_observer.py,sha256=wy_pA-1wmnzVjhq92mdsT2JJHYbfsm79mcMgSgYUCOs,1264
|
29
29
|
job_shop_lib/dispatching/feature_observers/_is_scheduled_observer.py,sha256=OcuMUB9_By6ZMtX-1_3z-xaxGbP85a5Zv0ywAv7XxWQ,1491
|
@@ -31,21 +31,20 @@ job_shop_lib/dispatching/feature_observers/_position_in_job_observer.py,sha256=W
|
|
31
31
|
job_shop_lib/dispatching/feature_observers/_remaining_operations_observer.py,sha256=5V87lCrJUabEe8AkTGXPu5yS8OGxeN8L3-xNyHmdmLs,1441
|
32
32
|
job_shop_lib/dispatching/rules/__init__.py,sha256=0Nn9FBVmxVYeDeLsd7g7WkmKFBYJqOIDzArbqsC7FAI,2187
|
33
33
|
job_shop_lib/dispatching/rules/_dispatching_rule_factory.py,sha256=5fNpv90fAoR6rcE6NeJOWiB7ir-FVnoONIhHtKJ9H0E,2904
|
34
|
-
job_shop_lib/dispatching/rules/_dispatching_rule_solver.py,sha256=
|
35
|
-
job_shop_lib/dispatching/rules/_dispatching_rules_functions.py,sha256=
|
34
|
+
job_shop_lib/dispatching/rules/_dispatching_rule_solver.py,sha256=1_canC1lXZATrQCZaHOY3JOLmTuT6U0Z_QWzgTOLwqI,5917
|
35
|
+
job_shop_lib/dispatching/rules/_dispatching_rules_functions.py,sha256=Yk40aKePBHHiMO6aTFeyJd1-khsDPhqit2WCOaByCfw,10998
|
36
36
|
job_shop_lib/dispatching/rules/_machine_chooser_factory.py,sha256=CJ74ujgWXgG8cuULWY6VJkD_b3arTcOjTNLZJTAf8xE,2346
|
37
37
|
job_shop_lib/dispatching/rules/_utils.py,sha256=m5qw4qyfaIvVrkmv51nuhreizr98-cg8AJKt2VTd48w,4603
|
38
38
|
job_shop_lib/exceptions.py,sha256=ARzpoZJCvRIvOesCiqqFSRxkv6w9WwEXx0aBP-l2IKA,1597
|
39
39
|
job_shop_lib/generation/__init__.py,sha256=QaWwuBfBNnOiG0OPiP_CV_flBu9dX7r2o_HwL47tREM,822
|
40
|
-
job_shop_lib/generation/_general_instance_generator.py,sha256=
|
40
|
+
job_shop_lib/generation/_general_instance_generator.py,sha256=b_tnyP4H_buoN7b6lKQRLvDkeZDdys0mpqS3thB5-SQ,6544
|
41
41
|
job_shop_lib/generation/_instance_generator.py,sha256=rT7CAJuv6E0zbmRFE_MFY6iaeZB06BshsBHl2_GyPzU,4567
|
42
|
-
job_shop_lib/generation/_transformations.py,sha256=ZigQTBsS3xgB2FhBu9MpsFs7A-_VY3840V_RtOIhCBk,5296
|
43
42
|
job_shop_lib/generation/_utils.py,sha256=TYBGt4Zjw94l6ukIjXBVAK3lmrrZXdyzyq_r1DMlL-E,3986
|
44
43
|
job_shop_lib/graphs/__init__.py,sha256=wlYIiXTuZRE6Kx3K0RpPUoZikzoegBuN2hcdqMODtGk,2433
|
45
44
|
job_shop_lib/graphs/_build_disjunctive_graph.py,sha256=UbUYdeQaaeEqLchcKJGHEFGl4wElfGLb1o_R-u8wqnA,5120
|
46
|
-
job_shop_lib/graphs/_build_resource_task_graphs.py,sha256=
|
45
|
+
job_shop_lib/graphs/_build_resource_task_graphs.py,sha256=vIy_EkQjgQAd5YyJxKAuGf7CLTjgCfhz-fYrObF4DTU,6962
|
47
46
|
job_shop_lib/graphs/_constants.py,sha256=K-GeVvh_DTWpo1KOX1clmxWS_pkUJbq19yOBmrCVIxI,1086
|
48
|
-
job_shop_lib/graphs/_job_shop_graph.py,sha256
|
47
|
+
job_shop_lib/graphs/_job_shop_graph.py,sha256=--9sbPpCiqC71kzmsPWFvMfqpx_gq4TL2x0HI2d-TEM,11427
|
49
48
|
job_shop_lib/graphs/_node.py,sha256=Ue3_BqVPU4w9S70kDChfsMJ09spnW7Dg83osSzi7fko,5990
|
50
49
|
job_shop_lib/graphs/graph_updaters/__init__.py,sha256=YOwb0RYypO9cEG-Nl3Ooj1yvAoyWDMNE_NAaUTyjzIw,658
|
51
50
|
job_shop_lib/graphs/graph_updaters/_disjunctive_graph_updater.py,sha256=-t0T8W-Jz9TJQR9-ljPkcDsDC4CwJAfs2nUF3zjEtuw,4369
|
@@ -54,21 +53,21 @@ job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py,sha256=9NG3pu7Z5h-
|
|
54
53
|
job_shop_lib/graphs/graph_updaters/_utils.py,sha256=sdw2Vo75P9c6Fy-YBlfgpXb9gPwHUluTB1E-9WINm_g,730
|
55
54
|
job_shop_lib/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
56
55
|
job_shop_lib/reinforcement_learning/__init__.py,sha256=sAVgxylKfBnn2rrz0BFcab1kjvQQ1h-hgldfbkPF--E,1537
|
57
|
-
job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py,sha256=
|
58
|
-
job_shop_lib/reinforcement_learning/_resource_task_graph_observation.py,sha256=
|
56
|
+
job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py,sha256=6nXw67Tfmim3LqlSuQ9Cfg3mMY-VmbMHuXfyOL90jng,15740
|
57
|
+
job_shop_lib/reinforcement_learning/_resource_task_graph_observation.py,sha256=ZqN6kzuXbO0BdA1UKrTEHeiHAKzRNIeuH-dBt90ttEc,12914
|
59
58
|
job_shop_lib/reinforcement_learning/_reward_observers.py,sha256=4Kdyn9Jlp_1sBtVr6raF-ZFtcnKxwyCLykfX53TmuhU,2959
|
60
|
-
job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py,sha256=
|
59
|
+
job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py,sha256=MnQYCVpwX4WHiGhYguHziDUrPIrKmXsjOUDoTmuoCBc,16403
|
61
60
|
job_shop_lib/reinforcement_learning/_types_and_constants.py,sha256=6FpuQkZLV2H8_dXmax49OTgAw7dWQcUEWVWWdMLR7bs,1752
|
62
61
|
job_shop_lib/reinforcement_learning/_utils.py,sha256=aHgNdW7YvUH8QM3l7NGIfrgzfpzGoklyYm1jM2Isi6Q,6043
|
63
62
|
job_shop_lib/visualization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
64
63
|
job_shop_lib/visualization/gantt/__init__.py,sha256=xMvuNph6bfwulHYqqklCj_6SUQgRzvC92Yul75F3Zlg,1250
|
65
64
|
job_shop_lib/visualization/gantt/_gantt_chart_creator.py,sha256=FgE4SmKLYKnS7dfTFgnBklWhwGyIo0DKWVkmxusDmp8,8606
|
66
|
-
job_shop_lib/visualization/gantt/_gantt_chart_video_and_gif_creation.py,sha256
|
65
|
+
job_shop_lib/visualization/gantt/_gantt_chart_video_and_gif_creation.py,sha256=jessyvLFgS60HEEmO7o6imo2f0uEpCLkyo-p_SBpE7Y,14571
|
67
66
|
job_shop_lib/visualization/gantt/_plot_gantt_chart.py,sha256=_4UGUTRuIw0tLzsJD9Gcf10aIy2YkDzTGsNTzFb5r0Y,6809
|
68
67
|
job_shop_lib/visualization/graphs/__init__.py,sha256=HUWzfgQLeklNROtjnxeJX_FIySo_baTXO6klx0zUVpQ,630
|
69
|
-
job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py,sha256=
|
70
|
-
job_shop_lib/visualization/graphs/_plot_resource_task_graph.py,sha256=
|
71
|
-
job_shop_lib-1.1.
|
72
|
-
job_shop_lib-1.1.
|
73
|
-
job_shop_lib-1.1.
|
74
|
-
job_shop_lib-1.1.
|
68
|
+
job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py,sha256=L9_ZGgvCFpGc2rTOdZESdtydFQqShjqedimIOhqZx6Y,16209
|
69
|
+
job_shop_lib/visualization/graphs/_plot_resource_task_graph.py,sha256=nkkdZ-9_OBevw72Frecwzv1y3WyhGZ9r9lz0y9MXvZ8,13192
|
70
|
+
job_shop_lib-1.1.3.dist-info/LICENSE,sha256=9mggivMGd5taAu3xbmBway-VQZMBzurBGHofFopvUsQ,1069
|
71
|
+
job_shop_lib-1.1.3.dist-info/METADATA,sha256=O0G7p67y75GHe-gVPEepppAQGETiswCfkaB7JmbmwnI,19130
|
72
|
+
job_shop_lib-1.1.3.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
73
|
+
job_shop_lib-1.1.3.dist-info/RECORD,,
|
@@ -1,167 +0,0 @@
|
|
1
|
-
"""Classes for generating transformed JobShopInstance objects."""
|
2
|
-
|
3
|
-
import abc
|
4
|
-
import copy
|
5
|
-
import random
|
6
|
-
|
7
|
-
from job_shop_lib import JobShopInstance, Operation
|
8
|
-
|
9
|
-
|
10
|
-
class Transformation(abc.ABC):
|
11
|
-
"""Base class for transformations applied to JobShopInstance objects."""
|
12
|
-
|
13
|
-
def __init__(self, suffix: str = ""):
|
14
|
-
self.suffix = suffix
|
15
|
-
self.counter = 0
|
16
|
-
|
17
|
-
@abc.abstractmethod
|
18
|
-
def apply(self, instance: JobShopInstance) -> JobShopInstance:
|
19
|
-
"""Applies the transformation to a given JobShopInstance.
|
20
|
-
|
21
|
-
Args:
|
22
|
-
instance: The JobShopInstance to transform.
|
23
|
-
|
24
|
-
Returns:
|
25
|
-
A new JobShopInstance with the transformation applied.
|
26
|
-
"""
|
27
|
-
|
28
|
-
def __call__(self, instance: JobShopInstance) -> JobShopInstance:
|
29
|
-
instance = self.apply(instance)
|
30
|
-
suffix = f"{self.suffix}_id={self.counter}"
|
31
|
-
instance.name += suffix
|
32
|
-
self.counter += 1
|
33
|
-
return instance
|
34
|
-
|
35
|
-
|
36
|
-
# pylint: disable=too-few-public-methods
|
37
|
-
class RemoveMachines(Transformation):
|
38
|
-
"""Removes operations associated with randomly selected machines until
|
39
|
-
there are exactly num_machines machines left."""
|
40
|
-
|
41
|
-
def __init__(self, num_machines: int, suffix: str | None = None):
|
42
|
-
if suffix is None:
|
43
|
-
suffix = f"_machines={num_machines}"
|
44
|
-
super().__init__(suffix=suffix)
|
45
|
-
self.num_machines = num_machines
|
46
|
-
|
47
|
-
def apply(self, instance: JobShopInstance) -> JobShopInstance:
|
48
|
-
if instance.num_machines <= self.num_machines:
|
49
|
-
return instance # No need to remove machines
|
50
|
-
|
51
|
-
# Select machine indices to keep
|
52
|
-
machines_to_keep = set(
|
53
|
-
random.sample(range(instance.num_machines), self.num_machines)
|
54
|
-
)
|
55
|
-
|
56
|
-
# Re-index machines
|
57
|
-
machine_reindex_map = {
|
58
|
-
old_id: new_id
|
59
|
-
for new_id, old_id in enumerate(sorted(machines_to_keep))
|
60
|
-
}
|
61
|
-
|
62
|
-
new_jobs = []
|
63
|
-
for job in instance.jobs:
|
64
|
-
# Keep operations whose machine_id is in machines_to_keep and
|
65
|
-
# re-index them
|
66
|
-
new_jobs.append(
|
67
|
-
[
|
68
|
-
Operation(machine_reindex_map[op.machine_id], op.duration)
|
69
|
-
for op in job
|
70
|
-
if op.machine_id in machines_to_keep
|
71
|
-
]
|
72
|
-
)
|
73
|
-
|
74
|
-
return JobShopInstance(new_jobs, instance.name)
|
75
|
-
|
76
|
-
|
77
|
-
# pylint: disable=too-few-public-methods
|
78
|
-
class AddDurationNoise(Transformation):
|
79
|
-
"""Adds uniform integer noise to operation durations."""
|
80
|
-
|
81
|
-
def __init__(
|
82
|
-
self,
|
83
|
-
min_duration: int = 1,
|
84
|
-
max_duration: int = 100,
|
85
|
-
noise_level: int = 10,
|
86
|
-
suffix: str | None = None,
|
87
|
-
):
|
88
|
-
if suffix is None:
|
89
|
-
suffix = f"_noise={noise_level}"
|
90
|
-
super().__init__(suffix=suffix)
|
91
|
-
self.min_duration = min_duration
|
92
|
-
self.max_duration = max_duration
|
93
|
-
self.noise_level = noise_level
|
94
|
-
|
95
|
-
def apply(self, instance: JobShopInstance) -> JobShopInstance:
|
96
|
-
new_jobs = []
|
97
|
-
for job in instance.jobs:
|
98
|
-
new_job = []
|
99
|
-
for op in job:
|
100
|
-
noise = random.randint(-self.noise_level, self.noise_level)
|
101
|
-
new_duration = max(
|
102
|
-
self.min_duration,
|
103
|
-
min(self.max_duration, op.duration + noise),
|
104
|
-
)
|
105
|
-
|
106
|
-
new_job.append(Operation(op.machine_id, new_duration))
|
107
|
-
new_jobs.append(new_job)
|
108
|
-
|
109
|
-
return JobShopInstance(new_jobs, instance.name)
|
110
|
-
|
111
|
-
|
112
|
-
class RemoveJobs(Transformation):
|
113
|
-
"""Removes jobs randomly until the number of jobs is within a specified
|
114
|
-
range.
|
115
|
-
|
116
|
-
Args:
|
117
|
-
min_jobs:
|
118
|
-
The minimum number of jobs to remain in the instance.
|
119
|
-
max_jobs:
|
120
|
-
The maximum number of jobs to remain in the instance.
|
121
|
-
target_jobs:
|
122
|
-
If specified, the number of jobs to remain in the
|
123
|
-
instance. Overrides ``min_jobs`` and ``max_jobs``.
|
124
|
-
"""
|
125
|
-
|
126
|
-
def __init__(
|
127
|
-
self,
|
128
|
-
min_jobs: int,
|
129
|
-
max_jobs: int,
|
130
|
-
target_jobs: int | None = None,
|
131
|
-
suffix: str | None = None,
|
132
|
-
):
|
133
|
-
if suffix is None:
|
134
|
-
suffix = f"_jobs={min_jobs}-{max_jobs}"
|
135
|
-
super().__init__(suffix=suffix)
|
136
|
-
self.min_jobs = min_jobs
|
137
|
-
self.max_jobs = max_jobs
|
138
|
-
self.target_jobs = target_jobs
|
139
|
-
|
140
|
-
def apply(self, instance: JobShopInstance) -> JobShopInstance:
|
141
|
-
if self.target_jobs is None:
|
142
|
-
target_jobs = random.randint(self.min_jobs, self.max_jobs)
|
143
|
-
else:
|
144
|
-
target_jobs = self.target_jobs
|
145
|
-
new_jobs = copy.deepcopy(instance.jobs)
|
146
|
-
|
147
|
-
while len(new_jobs) > target_jobs:
|
148
|
-
new_jobs.pop(random.randint(0, len(new_jobs) - 1))
|
149
|
-
|
150
|
-
return JobShopInstance(new_jobs, instance.name)
|
151
|
-
|
152
|
-
@staticmethod
|
153
|
-
def remove_job(
|
154
|
-
instance: JobShopInstance, job_index: int
|
155
|
-
) -> JobShopInstance:
|
156
|
-
"""Removes a specific job from the instance.
|
157
|
-
|
158
|
-
Args:
|
159
|
-
instance: The JobShopInstance from which to remove the job.
|
160
|
-
job_index: The index of the job to remove.
|
161
|
-
|
162
|
-
Returns:
|
163
|
-
A new JobShopInstance with the specified job removed.
|
164
|
-
"""
|
165
|
-
new_jobs = copy.deepcopy(instance.jobs)
|
166
|
-
new_jobs.pop(job_index)
|
167
|
-
return JobShopInstance(new_jobs, instance.name)
|
File without changes
|
File without changes
|