job-shop-lib 0.5.1__py3-none-any.whl → 1.0.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 +19 -8
- job_shop_lib/{base_solver.py → _base_solver.py} +1 -1
- job_shop_lib/{job_shop_instance.py → _job_shop_instance.py} +155 -81
- job_shop_lib/_operation.py +118 -0
- job_shop_lib/{schedule.py → _schedule.py} +102 -84
- job_shop_lib/{scheduled_operation.py → _scheduled_operation.py} +25 -49
- job_shop_lib/benchmarking/__init__.py +66 -43
- job_shop_lib/benchmarking/_load_benchmark.py +88 -0
- job_shop_lib/constraint_programming/__init__.py +13 -0
- job_shop_lib/{cp_sat/ortools_solver.py → constraint_programming/_ortools_solver.py} +77 -22
- job_shop_lib/dispatching/__init__.py +51 -42
- job_shop_lib/dispatching/{dispatcher.py → _dispatcher.py} +223 -130
- job_shop_lib/dispatching/_dispatcher_observer_config.py +67 -0
- job_shop_lib/dispatching/_factories.py +135 -0
- job_shop_lib/dispatching/{history_tracker.py → _history_observer.py} +6 -7
- job_shop_lib/dispatching/_optimal_operations_observer.py +113 -0
- job_shop_lib/dispatching/_ready_operation_filters.py +168 -0
- job_shop_lib/dispatching/_unscheduled_operations_observer.py +70 -0
- job_shop_lib/dispatching/feature_observers/__init__.py +51 -13
- job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +212 -0
- job_shop_lib/dispatching/feature_observers/{duration_observer.py → _duration_observer.py} +20 -18
- job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +289 -0
- job_shop_lib/dispatching/feature_observers/_factory.py +95 -0
- job_shop_lib/dispatching/feature_observers/_feature_observer.py +228 -0
- job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +97 -0
- job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +35 -0
- job_shop_lib/dispatching/feature_observers/{is_scheduled_observer.py → _is_scheduled_observer.py} +9 -5
- job_shop_lib/dispatching/feature_observers/{position_in_job_observer.py → _position_in_job_observer.py} +8 -10
- job_shop_lib/dispatching/feature_observers/{remaining_operations_observer.py → _remaining_operations_observer.py} +8 -26
- job_shop_lib/dispatching/rules/__init__.py +87 -0
- job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +84 -0
- job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +201 -0
- job_shop_lib/dispatching/{dispatching_rules.py → rules/_dispatching_rules_functions.py} +70 -16
- job_shop_lib/dispatching/rules/_machine_chooser_factory.py +71 -0
- job_shop_lib/dispatching/rules/_utils.py +128 -0
- job_shop_lib/exceptions.py +18 -0
- job_shop_lib/generation/__init__.py +10 -2
- job_shop_lib/generation/_general_instance_generator.py +165 -0
- job_shop_lib/generation/{instance_generator.py → _instance_generator.py} +37 -26
- job_shop_lib/{generators/transformations.py → generation/_transformations.py} +16 -12
- job_shop_lib/generation/_utils.py +124 -0
- job_shop_lib/graphs/__init__.py +30 -12
- job_shop_lib/graphs/{build_disjunctive_graph.py → _build_disjunctive_graph.py} +41 -3
- job_shop_lib/graphs/{build_agent_task_graph.py → _build_resource_task_graphs.py} +28 -26
- job_shop_lib/graphs/_constants.py +38 -0
- job_shop_lib/graphs/_job_shop_graph.py +320 -0
- job_shop_lib/graphs/_node.py +182 -0
- job_shop_lib/graphs/graph_updaters/__init__.py +26 -0
- job_shop_lib/graphs/graph_updaters/_disjunctive_graph_updater.py +108 -0
- job_shop_lib/graphs/graph_updaters/_graph_updater.py +57 -0
- job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +155 -0
- job_shop_lib/graphs/graph_updaters/_utils.py +25 -0
- job_shop_lib/py.typed +0 -0
- job_shop_lib/reinforcement_learning/__init__.py +68 -0
- job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +398 -0
- job_shop_lib/reinforcement_learning/_resource_task_graph_observation.py +329 -0
- job_shop_lib/reinforcement_learning/_reward_observers.py +87 -0
- job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +443 -0
- job_shop_lib/reinforcement_learning/_types_and_constants.py +62 -0
- job_shop_lib/reinforcement_learning/_utils.py +199 -0
- job_shop_lib/visualization/__init__.py +0 -25
- job_shop_lib/visualization/gantt/__init__.py +48 -0
- job_shop_lib/visualization/gantt/_gantt_chart_creator.py +257 -0
- job_shop_lib/visualization/gantt/_gantt_chart_video_and_gif_creation.py +422 -0
- job_shop_lib/visualization/{gantt_chart.py → gantt/_plot_gantt_chart.py} +84 -21
- job_shop_lib/visualization/graphs/__init__.py +29 -0
- job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py +418 -0
- job_shop_lib/visualization/graphs/_plot_resource_task_graph.py +389 -0
- {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0.dist-info}/METADATA +87 -55
- job_shop_lib-1.0.0.dist-info/RECORD +73 -0
- {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0.dist-info}/WHEEL +1 -1
- job_shop_lib/benchmarking/load_benchmark.py +0 -142
- job_shop_lib/cp_sat/__init__.py +0 -5
- job_shop_lib/dispatching/dispatching_rule_solver.py +0 -119
- job_shop_lib/dispatching/factories.py +0 -206
- job_shop_lib/dispatching/feature_observers/composite_feature_observer.py +0 -87
- job_shop_lib/dispatching/feature_observers/earliest_start_time_observer.py +0 -156
- job_shop_lib/dispatching/feature_observers/factory.py +0 -58
- job_shop_lib/dispatching/feature_observers/feature_observer.py +0 -113
- job_shop_lib/dispatching/feature_observers/is_completed_observer.py +0 -98
- job_shop_lib/dispatching/feature_observers/is_ready_observer.py +0 -40
- job_shop_lib/dispatching/pruning_functions.py +0 -116
- job_shop_lib/generation/general_instance_generator.py +0 -169
- job_shop_lib/generation/transformations.py +0 -164
- job_shop_lib/generators/__init__.py +0 -8
- job_shop_lib/generators/basic_generator.py +0 -200
- job_shop_lib/graphs/constants.py +0 -21
- job_shop_lib/graphs/job_shop_graph.py +0 -202
- job_shop_lib/graphs/node.py +0 -166
- job_shop_lib/operation.py +0 -122
- job_shop_lib/visualization/agent_task_graph.py +0 -257
- job_shop_lib/visualization/create_gif.py +0 -209
- job_shop_lib/visualization/disjunctive_graph.py +0 -210
- job_shop_lib-0.5.1.dist-info/RECORD +0 -52
- {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0.dist-info}/LICENSE +0 -0
@@ -3,11 +3,9 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import abc
|
6
|
-
from typing import Any
|
6
|
+
from typing import Any, TypeVar, List, Optional, Type, Set
|
7
7
|
from collections.abc import Callable
|
8
|
-
from collections import deque
|
9
8
|
from functools import wraps
|
10
|
-
from warnings import warn
|
11
9
|
|
12
10
|
from job_shop_lib import (
|
13
11
|
JobShopInstance,
|
@@ -15,40 +13,71 @@ from job_shop_lib import (
|
|
15
13
|
ScheduledOperation,
|
16
14
|
Operation,
|
17
15
|
)
|
16
|
+
from job_shop_lib.exceptions import ValidationError
|
18
17
|
|
19
18
|
|
20
19
|
# Added here to avoid circular imports
|
21
20
|
class DispatcherObserver(abc.ABC):
|
22
|
-
"""
|
21
|
+
"""Abstract class that allows objects to observe and respond to changes
|
22
|
+
within the :class:`Dispatcher`.
|
23
|
+
|
24
|
+
It follows the Observer design pattern, where observers subscribe to the
|
25
|
+
dispatcher and receive updates when certain events occur, such as when
|
26
|
+
an operation is scheduled or when the dispatcher is reset.
|
27
|
+
|
28
|
+
Attributes:
|
29
|
+
dispatcher:
|
30
|
+
The :class:`Dispatcher` instance to observe.
|
31
|
+
|
32
|
+
Args:
|
33
|
+
dispatcher:
|
34
|
+
The :class:`Dispatcher` instance to observe.
|
35
|
+
subscribe:
|
36
|
+
If ``True``, automatically subscribes the observer to the
|
37
|
+
dispatcher when it is initialized. Defaults to ``True``.
|
38
|
+
|
39
|
+
Raises:
|
40
|
+
ValidationError: If ``is_singleton`` is ``True`` and an observer of the
|
41
|
+
same type already exists in the dispatcher's list of
|
42
|
+
subscribers.
|
43
|
+
|
44
|
+
Example:
|
45
|
+
|
46
|
+
.. code-block:: python
|
47
|
+
|
48
|
+
from job_shop_lib.dispatching import DispatcherObserver, Dispatcher
|
49
|
+
from job_shop_lib import ScheduledOperation
|
50
|
+
|
51
|
+
|
52
|
+
class HistoryObserver(DispatcherObserver):
|
53
|
+
def __init__(self, dispatcher: Dispatcher):
|
54
|
+
super().__init__(dispatcher)
|
55
|
+
self.history: List[ScheduledOperation] = []
|
56
|
+
|
57
|
+
def update(self, scheduled_operation: ScheduledOperation):
|
58
|
+
self.history.append(scheduled_operation)
|
59
|
+
|
60
|
+
def reset(self):
|
61
|
+
self.history = []
|
62
|
+
|
63
|
+
"""
|
64
|
+
|
65
|
+
# Made read-only following Google Style Guide recommendation
|
66
|
+
_is_singleton = True
|
67
|
+
"""If True, ensures only one instance of this observer type is subscribed
|
68
|
+
to the dispatcher."""
|
23
69
|
|
24
70
|
def __init__(
|
25
71
|
self,
|
26
72
|
dispatcher: Dispatcher,
|
27
|
-
|
73
|
+
*,
|
28
74
|
subscribe: bool = True,
|
29
75
|
):
|
30
|
-
|
31
|
-
it.
|
32
|
-
|
33
|
-
Args:
|
34
|
-
subject:
|
35
|
-
The subject to observe.
|
36
|
-
is_singleton:
|
37
|
-
Whether the observer should be a singleton. If True, the
|
38
|
-
observer will be the only instance of its class in the
|
39
|
-
subject's list of subscribers. If False, the observer will
|
40
|
-
be added to the subject's list of subscribers every time
|
41
|
-
it is initialized.
|
42
|
-
subscribe:
|
43
|
-
Whether to subscribe the observer to the subject. If False,
|
44
|
-
the observer will not be subscribed to the subject and will
|
45
|
-
not receive automatic updates.
|
46
|
-
"""
|
47
|
-
if is_singleton and any(
|
76
|
+
if self._is_singleton and any(
|
48
77
|
isinstance(observer, self.__class__)
|
49
78
|
for observer in dispatcher.subscribers
|
50
79
|
):
|
51
|
-
raise
|
80
|
+
raise ValidationError(
|
52
81
|
f"An observer of type {self.__class__.__name__} already "
|
53
82
|
"exists in the dispatcher's list of subscribers. If you want "
|
54
83
|
"to create multiple instances of this observer, set "
|
@@ -59,6 +88,15 @@ class DispatcherObserver(abc.ABC):
|
|
59
88
|
if subscribe:
|
60
89
|
self.dispatcher.subscribe(self)
|
61
90
|
|
91
|
+
@property
|
92
|
+
def is_singleton(self) -> bool:
|
93
|
+
"""Returns whether this observer is a singleton.
|
94
|
+
|
95
|
+
This is a class attribute that determines whether only one
|
96
|
+
instance of this observer type can be subscribed to the dispatcher.
|
97
|
+
"""
|
98
|
+
return self._is_singleton
|
99
|
+
|
62
100
|
@abc.abstractmethod
|
63
101
|
def update(self, scheduled_operation: ScheduledOperation):
|
64
102
|
"""Called when an operation is scheduled on a machine."""
|
@@ -74,6 +112,11 @@ class DispatcherObserver(abc.ABC):
|
|
74
112
|
return self.__class__.__name__
|
75
113
|
|
76
114
|
|
115
|
+
# Disable pylint's false positive
|
116
|
+
# pylint: disable=invalid-name
|
117
|
+
ObserverType = TypeVar("ObserverType", bound=DispatcherObserver)
|
118
|
+
|
119
|
+
|
77
120
|
def _dispatcher_cache(method):
|
78
121
|
"""Decorator to cache results of a method based on its name.
|
79
122
|
|
@@ -110,54 +153,77 @@ class Dispatcher:
|
|
110
153
|
responsible for scheduling the operations on the machines and keeping
|
111
154
|
track of the next available time for each machine and job.
|
112
155
|
|
113
|
-
|
156
|
+
The core method of the class are:
|
157
|
+
|
158
|
+
.. autosummary::
|
159
|
+
|
160
|
+
dispatch
|
161
|
+
reset
|
162
|
+
|
163
|
+
It also provides methods to query the state of the schedule and the
|
164
|
+
operations:
|
165
|
+
|
166
|
+
.. autosummary::
|
167
|
+
|
168
|
+
current_time
|
169
|
+
available_operations
|
170
|
+
available_machines
|
171
|
+
available_jobs
|
172
|
+
unscheduled_operations
|
173
|
+
scheduled_operations
|
174
|
+
ongoing_operations
|
175
|
+
completed_operations
|
176
|
+
uncompleted_operations
|
177
|
+
is_scheduled
|
178
|
+
is_ongoing
|
179
|
+
next_operation
|
180
|
+
earliest_start_time
|
181
|
+
remaining_duration
|
182
|
+
|
183
|
+
The above methods which do not take any arguments are cached to improve
|
184
|
+
performance. After each scheduling operation, the cache is cleared.
|
185
|
+
|
186
|
+
Args:
|
114
187
|
instance:
|
115
|
-
The instance of the job shop problem to be
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
scheduled.
|
188
|
+
The instance of the job shop problem to be solved.
|
189
|
+
ready_operations_filter:
|
190
|
+
A function that filters out operations that are not ready to
|
191
|
+
be scheduled. The function should take the dispatcher and a
|
192
|
+
list of operations as input and return a list of operations
|
193
|
+
that are ready to be scheduled. If ``None``, no filtering is
|
194
|
+
done.
|
121
195
|
"""
|
122
196
|
|
123
|
-
__slots__ =
|
124
|
-
"instance",
|
125
|
-
"schedule",
|
126
|
-
"_machine_next_available_time",
|
127
|
-
"_job_next_operation_index",
|
128
|
-
"_job_next_available_time",
|
129
|
-
"
|
130
|
-
|
131
|
-
|
132
|
-
|
197
|
+
__slots__ = {
|
198
|
+
"instance": "The instance of the job shop problem to be scheduled.",
|
199
|
+
"schedule": "The schedule of operations on machines.",
|
200
|
+
"_machine_next_available_time": "",
|
201
|
+
"_job_next_operation_index": "",
|
202
|
+
"_job_next_available_time": "",
|
203
|
+
"ready_operations_filter": (
|
204
|
+
"A function that filters out operations that are not ready to be "
|
205
|
+
"scheduled."
|
206
|
+
),
|
207
|
+
"subscribers": "A list of observers subscribed to the dispatcher.",
|
208
|
+
"_cache": "A dictionary to cache the results of the cached methods.",
|
209
|
+
}
|
133
210
|
|
134
211
|
def __init__(
|
135
212
|
self,
|
136
213
|
instance: JobShopInstance,
|
137
|
-
|
138
|
-
Callable[[Dispatcher,
|
214
|
+
ready_operations_filter: (
|
215
|
+
Optional[Callable[[Dispatcher, List[Operation]], List[Operation]]]
|
139
216
|
) = None,
|
140
217
|
) -> None:
|
141
|
-
"""Initializes the object with the given instance.
|
142
|
-
|
143
|
-
Args:
|
144
|
-
instance:
|
145
|
-
The instance of the job shop problem to be solved.
|
146
|
-
pruning_function:
|
147
|
-
A function that filters out operations that are not ready to
|
148
|
-
be scheduled. The function should take the dispatcher and a
|
149
|
-
list of operations as input and return a list of operations
|
150
|
-
that are ready to be scheduled. If `None`, no pruning is done.
|
151
|
-
"""
|
152
218
|
|
153
219
|
self.instance = instance
|
154
220
|
self.schedule = Schedule(self.instance)
|
155
|
-
self.
|
221
|
+
self.ready_operations_filter = ready_operations_filter
|
222
|
+
self.subscribers: List[DispatcherObserver] = []
|
156
223
|
|
157
224
|
self._machine_next_available_time = [0] * self.instance.num_machines
|
158
225
|
self._job_next_operation_index = [0] * self.instance.num_jobs
|
159
226
|
self._job_next_available_time = [0] * self.instance.num_jobs
|
160
|
-
self.subscribers: list[DispatcherObserver] = []
|
161
227
|
self._cache: dict[str, Any] = {}
|
162
228
|
|
163
229
|
def __str__(self) -> str:
|
@@ -167,18 +233,18 @@ class Dispatcher:
|
|
167
233
|
return str(self)
|
168
234
|
|
169
235
|
@property
|
170
|
-
def machine_next_available_time(self) ->
|
236
|
+
def machine_next_available_time(self) -> List[int]:
|
171
237
|
"""Returns the next available time for each machine."""
|
172
238
|
return self._machine_next_available_time
|
173
239
|
|
174
240
|
@property
|
175
|
-
def job_next_operation_index(self) ->
|
241
|
+
def job_next_operation_index(self) -> List[int]:
|
176
242
|
"""Returns the index of the next operation to be scheduled for each
|
177
243
|
job."""
|
178
244
|
return self._job_next_operation_index
|
179
245
|
|
180
246
|
@property
|
181
|
-
def job_next_available_time(self) ->
|
247
|
+
def job_next_available_time(self) -> List[int]:
|
182
248
|
"""Returns the next available time for each job."""
|
183
249
|
return self._job_next_available_time
|
184
250
|
|
@@ -200,26 +266,35 @@ class Dispatcher:
|
|
200
266
|
for subscriber in self.subscribers:
|
201
267
|
subscriber.reset()
|
202
268
|
|
203
|
-
def dispatch(
|
269
|
+
def dispatch(
|
270
|
+
self, operation: Operation, machine_id: Optional[int] = None
|
271
|
+
) -> None:
|
204
272
|
"""Schedules the given operation on the given machine.
|
205
273
|
|
206
274
|
The start time of the operation is computed based on the next
|
207
275
|
available time for the machine and the next available time for the
|
208
276
|
job to which the operation belongs. The operation is then scheduled
|
209
277
|
on the machine and the tracking attributes are updated.
|
278
|
+
|
210
279
|
Args:
|
211
280
|
operation:
|
212
281
|
The operation to be scheduled.
|
213
282
|
machine_id:
|
214
283
|
The id of the machine on which the operation is to be
|
215
|
-
scheduled.
|
284
|
+
scheduled. If ``None``, the :class:`~job_shop_lib.Operation`'s
|
285
|
+
:attr:`~job_shop_lib.Operation.machine_id` attribute is used.
|
216
286
|
|
217
287
|
Raises:
|
218
|
-
|
288
|
+
ValidationError: If the operation is not ready to be scheduled.
|
289
|
+
UninitializedAttributeError: If the operation has multiple
|
290
|
+
machines in its list and no ``machine_id`` is provided.
|
219
291
|
"""
|
220
292
|
|
221
293
|
if not self.is_operation_ready(operation):
|
222
|
-
raise
|
294
|
+
raise ValidationError("Operation is not ready to be scheduled.")
|
295
|
+
|
296
|
+
if machine_id is None:
|
297
|
+
machine_id = operation.machine_id
|
223
298
|
|
224
299
|
start_time = self.start_time(operation, machine_id)
|
225
300
|
|
@@ -229,10 +304,6 @@ class Dispatcher:
|
|
229
304
|
self.schedule.add(scheduled_operation)
|
230
305
|
self._update_tracking_attributes(scheduled_operation)
|
231
306
|
|
232
|
-
# Notify subscribers
|
233
|
-
for subscriber in self.subscribers:
|
234
|
-
subscriber.update(scheduled_operation)
|
235
|
-
|
236
307
|
def is_operation_ready(self, operation: Operation) -> bool:
|
237
308
|
"""Returns True if the given operation is ready to be scheduled.
|
238
309
|
|
@@ -265,8 +336,7 @@ class Dispatcher:
|
|
265
336
|
The operation to be scheduled.
|
266
337
|
machine_id:
|
267
338
|
The id of the machine on which the operation is to be
|
268
|
-
scheduled.
|
269
|
-
next available time for the operation on any machine.
|
339
|
+
scheduled.
|
270
340
|
"""
|
271
341
|
return max(
|
272
342
|
self._machine_next_available_time[machine_id],
|
@@ -286,6 +356,40 @@ class Dispatcher:
|
|
286
356
|
self._job_next_available_time[job_id] = end_time
|
287
357
|
self._cache = {}
|
288
358
|
|
359
|
+
# Notify subscribers
|
360
|
+
for subscriber in self.subscribers:
|
361
|
+
subscriber.update(scheduled_operation)
|
362
|
+
|
363
|
+
def create_or_get_observer(
|
364
|
+
self,
|
365
|
+
observer: Type[ObserverType],
|
366
|
+
condition: Callable[[DispatcherObserver], bool] = lambda _: True,
|
367
|
+
**kwargs,
|
368
|
+
) -> ObserverType:
|
369
|
+
"""Creates a new observer of the specified type or returns an existing
|
370
|
+
observer of the same type if it already exists in the dispatcher's list
|
371
|
+
of observers.
|
372
|
+
|
373
|
+
Args:
|
374
|
+
observer:
|
375
|
+
The type of observer to be created or retrieved.
|
376
|
+
condition:
|
377
|
+
A function that takes an observer and returns True if it is
|
378
|
+
the observer to be retrieved. By default, it returns True for
|
379
|
+
all observers.
|
380
|
+
**kwargs:
|
381
|
+
Additional keyword arguments to be passed to the observer's
|
382
|
+
constructor.
|
383
|
+
"""
|
384
|
+
for existing_observer in self.subscribers:
|
385
|
+
if isinstance(existing_observer, observer) and condition(
|
386
|
+
existing_observer
|
387
|
+
):
|
388
|
+
return existing_observer
|
389
|
+
|
390
|
+
new_observer = observer(self, **kwargs)
|
391
|
+
return new_observer
|
392
|
+
|
289
393
|
@_dispatcher_cache
|
290
394
|
def current_time(self) -> int:
|
291
395
|
"""Returns the current time of the schedule.
|
@@ -297,7 +401,7 @@ class Dispatcher:
|
|
297
401
|
current_time = self.min_start_time(available_operations)
|
298
402
|
return current_time
|
299
403
|
|
300
|
-
def min_start_time(self, operations:
|
404
|
+
def min_start_time(self, operations: List[Operation]) -> int:
|
301
405
|
"""Returns the minimum start time of the available operations."""
|
302
406
|
if not operations:
|
303
407
|
return self.schedule.makespan()
|
@@ -309,43 +413,43 @@ class Dispatcher:
|
|
309
413
|
return int(min_start_time)
|
310
414
|
|
311
415
|
@_dispatcher_cache
|
312
|
-
def available_operations(self) ->
|
416
|
+
def available_operations(self) -> List[Operation]:
|
313
417
|
"""Returns a list of available operations for processing, optionally
|
314
|
-
filtering out operations using the
|
418
|
+
filtering out operations using the filter function.
|
315
419
|
|
316
420
|
This method first gathers all possible next operations from the jobs
|
317
421
|
being processed. It then optionally filters these operations using the
|
318
|
-
|
422
|
+
filter function.
|
319
423
|
|
320
424
|
Returns:
|
321
|
-
A list of Operation objects that are available for
|
425
|
+
A list of :class:`Operation` objects that are available for
|
426
|
+
scheduling.
|
322
427
|
"""
|
323
|
-
available_operations = self.
|
324
|
-
if self.
|
325
|
-
available_operations = self.
|
428
|
+
available_operations = self.raw_ready_operations()
|
429
|
+
if self.ready_operations_filter is not None:
|
430
|
+
available_operations = self.ready_operations_filter(
|
326
431
|
self, available_operations
|
327
432
|
)
|
328
433
|
return available_operations
|
329
434
|
|
330
435
|
@_dispatcher_cache
|
331
|
-
def
|
436
|
+
def raw_ready_operations(self) -> List[Operation]:
|
332
437
|
"""Returns a list of available operations for processing without
|
333
|
-
applying the
|
438
|
+
applying the filter function.
|
334
439
|
|
335
440
|
Returns:
|
336
|
-
A list of Operation objects that are available for
|
337
|
-
based on precedence and machine constraints only.
|
441
|
+
A list of :class:`Operation` objects that are available for
|
442
|
+
scheduling based on precedence and machine constraints only.
|
338
443
|
"""
|
339
|
-
available_operations = [
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
available_operations.append(operation)
|
444
|
+
available_operations = [
|
445
|
+
self.instance.jobs[job_id][position]
|
446
|
+
for job_id, position in enumerate(self._job_next_operation_index)
|
447
|
+
if position < len(self.instance.jobs[job_id])
|
448
|
+
]
|
345
449
|
return available_operations
|
346
450
|
|
347
451
|
@_dispatcher_cache
|
348
|
-
def unscheduled_operations(self) ->
|
452
|
+
def unscheduled_operations(self) -> List[Operation]:
|
349
453
|
"""Returns the list of operations that have not been scheduled."""
|
350
454
|
unscheduled_operations = []
|
351
455
|
for job_id, next_position in enumerate(self._job_next_operation_index):
|
@@ -354,17 +458,16 @@ class Dispatcher:
|
|
354
458
|
return unscheduled_operations
|
355
459
|
|
356
460
|
@_dispatcher_cache
|
357
|
-
def scheduled_operations(self) ->
|
461
|
+
def scheduled_operations(self) -> List[ScheduledOperation]:
|
358
462
|
"""Returns the list of operations that have been scheduled."""
|
359
463
|
scheduled_operations = []
|
360
|
-
for
|
361
|
-
|
362
|
-
scheduled_operations.extend(operations)
|
464
|
+
for machine_schedule in self.schedule.schedule:
|
465
|
+
scheduled_operations.extend(machine_schedule)
|
363
466
|
return scheduled_operations
|
364
467
|
|
365
468
|
@_dispatcher_cache
|
366
|
-
def available_machines(self) ->
|
367
|
-
"""Returns the list of
|
469
|
+
def available_machines(self) -> List[int]:
|
470
|
+
"""Returns the list of ready machines."""
|
368
471
|
available_operations = self.available_operations()
|
369
472
|
available_machines = set()
|
370
473
|
for operation in available_operations:
|
@@ -372,8 +475,8 @@ class Dispatcher:
|
|
372
475
|
return list(available_machines)
|
373
476
|
|
374
477
|
@_dispatcher_cache
|
375
|
-
def available_jobs(self) ->
|
376
|
-
"""Returns the list of
|
478
|
+
def available_jobs(self) -> List[int]:
|
479
|
+
"""Returns the list of ready jobs."""
|
377
480
|
available_operations = self.available_operations()
|
378
481
|
available_jobs = set(
|
379
482
|
operation.job_id for operation in available_operations
|
@@ -384,7 +487,7 @@ class Dispatcher:
|
|
384
487
|
"""Calculates the earliest start time for a given operation based on
|
385
488
|
machine and job constraints.
|
386
489
|
|
387
|
-
This method is different from the
|
490
|
+
This method is different from the ``start_time`` method in that it
|
388
491
|
takes into account every machine that can process the operation, not
|
389
492
|
just the one that will process it. However, it also assumes that
|
390
493
|
the operation is ready to be scheduled in the job in favor of
|
@@ -427,24 +530,19 @@ class Dispatcher:
|
|
427
530
|
return scheduled_operation.end_time - adjusted_start_time
|
428
531
|
|
429
532
|
@_dispatcher_cache
|
430
|
-
def completed_operations(self) ->
|
533
|
+
def completed_operations(self) -> Set[ScheduledOperation]:
|
431
534
|
"""Returns the set of operations that have been completed.
|
432
535
|
|
433
536
|
This method returns the operations that have been scheduled and the
|
434
537
|
current time is greater than or equal to the end time of the operation.
|
435
538
|
"""
|
436
539
|
scheduled_operations = set(self.scheduled_operations())
|
437
|
-
ongoing_operations = set(
|
438
|
-
map(
|
439
|
-
lambda scheduled_op: scheduled_op.operation,
|
440
|
-
self.ongoing_operations(),
|
441
|
-
)
|
442
|
-
)
|
540
|
+
ongoing_operations = set(self.ongoing_operations())
|
443
541
|
completed_operations = scheduled_operations - ongoing_operations
|
444
542
|
return completed_operations
|
445
543
|
|
446
544
|
@_dispatcher_cache
|
447
|
-
def uncompleted_operations(self) ->
|
545
|
+
def uncompleted_operations(self) -> List[Operation]:
|
448
546
|
"""Returns the list of operations that have not been completed yet.
|
449
547
|
|
450
548
|
This method checks for operations that either haven't been scheduled
|
@@ -463,7 +561,7 @@ class Dispatcher:
|
|
463
561
|
return uncompleted_operations
|
464
562
|
|
465
563
|
@_dispatcher_cache
|
466
|
-
def ongoing_operations(self) ->
|
564
|
+
def ongoing_operations(self) -> List[ScheduledOperation]:
|
467
565
|
"""Returns the list of operations that are currently being processed.
|
468
566
|
|
469
567
|
This method returns the operations that have been scheduled and are
|
@@ -489,28 +587,23 @@ class Dispatcher:
|
|
489
587
|
current_time = self.current_time()
|
490
588
|
return scheduled_operation.start_time <= current_time
|
491
589
|
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
590
|
+
def next_operation(self, job_id: int) -> Operation:
|
591
|
+
"""Returns the next operation to be scheduled for the given job.
|
592
|
+
|
593
|
+
Args:
|
594
|
+
job_id:
|
595
|
+
The id of the job for which to return the next operation.
|
596
|
+
|
597
|
+
Raises:
|
598
|
+
ValidationError: If there are no more operations left for the job.
|
599
|
+
"""
|
600
|
+
if (
|
601
|
+
len(self.instance.jobs[job_id])
|
602
|
+
<= self._job_next_operation_index[job_id]
|
603
|
+
):
|
604
|
+
raise ValidationError(
|
605
|
+
f"No more operations left for job {job_id} to schedule."
|
606
|
+
)
|
607
|
+
return self.instance.jobs[job_id][
|
608
|
+
self._job_next_operation_index[job_id]
|
507
609
|
]
|
508
|
-
while not dispatcher.schedule.is_complete():
|
509
|
-
for machine_id, operations in enumerate(raw_solution_deques):
|
510
|
-
if not operations:
|
511
|
-
continue
|
512
|
-
operation = operations[0]
|
513
|
-
if dispatcher.is_operation_ready(operation):
|
514
|
-
dispatcher.dispatch(operation, machine_id)
|
515
|
-
operations.popleft()
|
516
|
-
return dispatcher.schedule
|
@@ -0,0 +1,67 @@
|
|
1
|
+
"""Contains factory functions for creating dispatching rules, machine choosers,
|
2
|
+
and pruning functions for the job shop scheduling problem.
|
3
|
+
|
4
|
+
The factory functions create and return the appropriate functions based on the
|
5
|
+
specified names or enums.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from typing import TypeVar, Generic, Any, Dict
|
9
|
+
|
10
|
+
from dataclasses import dataclass, field
|
11
|
+
|
12
|
+
from job_shop_lib.exceptions import ValidationError
|
13
|
+
|
14
|
+
|
15
|
+
# Disable pylint's false positive
|
16
|
+
# pylint: disable=invalid-name
|
17
|
+
T = TypeVar("T")
|
18
|
+
|
19
|
+
|
20
|
+
@dataclass(frozen=True)
|
21
|
+
class DispatcherObserverConfig(Generic[T]):
|
22
|
+
"""Configuration for initializing any type of class.
|
23
|
+
|
24
|
+
Useful for specifying the type of the
|
25
|
+
:class:`~job_shop_lib.dispatching.DispatcherObserver` and additional
|
26
|
+
keyword arguments to pass to the dispatcher observer constructor while
|
27
|
+
not containing the ``dispatcher`` argument.
|
28
|
+
|
29
|
+
Args:
|
30
|
+
class_type:
|
31
|
+
Type of the class to be initialized. It can be the class type, an
|
32
|
+
enum value, or a string. This is useful for the creation of
|
33
|
+
:class:`~job_shop_lib.dispatching.DispatcherObserver` instances
|
34
|
+
from the factory functions.
|
35
|
+
kwargs:
|
36
|
+
Keyword arguments needed to initialize the class. It must not
|
37
|
+
contain the ``dispatcher`` argument.
|
38
|
+
|
39
|
+
.. seealso::
|
40
|
+
|
41
|
+
- :class:`~job_shop_lib.dispatching.DispatcherObserver`
|
42
|
+
- :func:`job_shop_lib.dispatching.feature_observers.\\
|
43
|
+
feature_observer_factory`
|
44
|
+
"""
|
45
|
+
|
46
|
+
# We use the type hint T, instead of ObserverType, to allow for string or
|
47
|
+
# specific Enum values to be passed as the type argument. For example:
|
48
|
+
# FeatureObserverConfig = DispatcherObserverConfig[
|
49
|
+
# Type[FeatureObserver] | FeatureObserverType | str
|
50
|
+
# ]
|
51
|
+
# This allows for the creation of a FeatureObserver instance
|
52
|
+
# from the factory function.
|
53
|
+
class_type: T
|
54
|
+
"""Type of the class to be initialized. It can be the class type, an
|
55
|
+
enum value, or a string. This is useful for the creation of
|
56
|
+
:class:`DispatcherObserver` instances from the factory functions."""
|
57
|
+
|
58
|
+
kwargs: Dict[str, Any] = field(default_factory=dict)
|
59
|
+
"""Keyword arguments needed to initialize the class. It must not
|
60
|
+
contain the ``dispatcher`` argument."""
|
61
|
+
|
62
|
+
def __post_init__(self):
|
63
|
+
if "dispatcher" in self.kwargs:
|
64
|
+
raise ValidationError(
|
65
|
+
"The 'dispatcher' argument should not be included in the "
|
66
|
+
"kwargs attribute."
|
67
|
+
)
|