job-shop-lib 0.5.1__py3-none-any.whl → 1.0.0a1__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- job_shop_lib/__init__.py +16 -8
- job_shop_lib/{base_solver.py → _base_solver.py} +1 -1
- job_shop_lib/{job_shop_instance.py → _job_shop_instance.py} +9 -4
- job_shop_lib/_operation.py +95 -0
- job_shop_lib/{schedule.py → _schedule.py} +73 -54
- job_shop_lib/{scheduled_operation.py → _scheduled_operation.py} +13 -37
- 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} +57 -18
- job_shop_lib/dispatching/__init__.py +45 -41
- job_shop_lib/dispatching/{dispatcher.py → _dispatcher.py} +153 -80
- job_shop_lib/dispatching/_dispatcher_observer_config.py +54 -0
- job_shop_lib/dispatching/_factories.py +125 -0
- job_shop_lib/dispatching/{history_tracker.py → _history_observer.py} +4 -6
- job_shop_lib/dispatching/{pruning_functions.py → _ready_operation_filters.py} +6 -35
- job_shop_lib/dispatching/_unscheduled_operations_observer.py +69 -0
- job_shop_lib/dispatching/feature_observers/__init__.py +16 -10
- job_shop_lib/dispatching/feature_observers/{composite_feature_observer.py → _composite_feature_observer.py} +84 -2
- job_shop_lib/dispatching/feature_observers/{duration_observer.py → _duration_observer.py} +6 -17
- job_shop_lib/dispatching/feature_observers/{earliest_start_time_observer.py → _earliest_start_time_observer.py} +114 -35
- job_shop_lib/dispatching/feature_observers/{factory.py → _factory.py} +31 -5
- job_shop_lib/dispatching/feature_observers/{feature_observer.py → _feature_observer.py} +59 -16
- job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +97 -0
- job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +33 -0
- job_shop_lib/dispatching/feature_observers/{position_in_job_observer.py → _position_in_job_observer.py} +1 -8
- job_shop_lib/dispatching/feature_observers/{remaining_operations_observer.py → _remaining_operations_observer.py} +8 -26
- job_shop_lib/dispatching/rules/__init__.py +51 -0
- job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +82 -0
- job_shop_lib/dispatching/{dispatching_rule_solver.py → rules/_dispatching_rule_solver.py} +44 -15
- job_shop_lib/dispatching/{dispatching_rules.py → rules/_dispatching_rules_functions.py} +74 -21
- job_shop_lib/dispatching/rules/_machine_chooser_factory.py +69 -0
- job_shop_lib/dispatching/rules/_utils.py +127 -0
- job_shop_lib/exceptions.py +18 -0
- job_shop_lib/generation/__init__.py +2 -2
- job_shop_lib/generation/{general_instance_generator.py → _general_instance_generator.py} +26 -7
- job_shop_lib/generation/{instance_generator.py → _instance_generator.py} +13 -3
- job_shop_lib/graphs/__init__.py +17 -6
- job_shop_lib/graphs/{job_shop_graph.py → _job_shop_graph.py} +81 -2
- job_shop_lib/graphs/{node.py → _node.py} +18 -12
- job_shop_lib/graphs/graph_updaters/__init__.py +13 -0
- job_shop_lib/graphs/graph_updaters/_graph_updater.py +59 -0
- job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +154 -0
- job_shop_lib/graphs/graph_updaters/_utils.py +25 -0
- job_shop_lib/reinforcement_learning/__init__.py +41 -0
- job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +366 -0
- job_shop_lib/reinforcement_learning/_reward_observers.py +85 -0
- job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +337 -0
- job_shop_lib/reinforcement_learning/_types_and_constants.py +61 -0
- job_shop_lib/reinforcement_learning/_utils.py +96 -0
- job_shop_lib/visualization/__init__.py +20 -4
- job_shop_lib/visualization/{agent_task_graph.py → _agent_task_graph.py} +28 -9
- job_shop_lib/visualization/_gantt_chart_creator.py +219 -0
- job_shop_lib/visualization/_gantt_chart_video_and_gif_creation.py +388 -0
- {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0a1.dist-info}/METADATA +68 -44
- job_shop_lib-1.0.0a1.dist-info/RECORD +66 -0
- job_shop_lib/benchmarking/load_benchmark.py +0 -142
- job_shop_lib/cp_sat/__init__.py +0 -5
- job_shop_lib/dispatching/factories.py +0 -206
- 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/generators/__init__.py +0 -8
- job_shop_lib/generators/basic_generator.py +0 -200
- job_shop_lib/generators/transformations.py +0 -164
- job_shop_lib/operation.py +0 -122
- job_shop_lib/visualization/create_gif.py +0 -209
- job_shop_lib-0.5.1.dist-info/RECORD +0 -52
- /job_shop_lib/dispatching/feature_observers/{is_scheduled_observer.py → _is_scheduled_observer.py} +0 -0
- /job_shop_lib/generation/{transformations.py → _transformations.py} +0 -0
- /job_shop_lib/graphs/{build_agent_task_graph.py → _build_agent_task_graph.py} +0 -0
- /job_shop_lib/graphs/{build_disjunctive_graph.py → _build_disjunctive_graph.py} +0 -0
- /job_shop_lib/graphs/{constants.py → _constants.py} +0 -0
- /job_shop_lib/visualization/{disjunctive_graph.py → _disjunctive_graph.py} +0 -0
- /job_shop_lib/visualization/{gantt_chart.py → _gantt_chart.py} +0 -0
- {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0a1.dist-info}/LICENSE +0 -0
- {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0a1.dist-info}/WHEEL +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
|
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,74 @@ 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
|
+
Example:
|
33
|
+
|
34
|
+
.. code-block:: python
|
35
|
+
|
36
|
+
from job_shop_lib.dispatching import DispatcherObserver, Dispatcher
|
37
|
+
from job_shop_lib import ScheduledOperation
|
38
|
+
|
39
|
+
|
40
|
+
class HistoryObserver(DispatcherObserver):
|
41
|
+
def __init__(self, dispatcher: Dispatcher):
|
42
|
+
super().__init__(dispatcher)
|
43
|
+
self.history: list[ScheduledOperation] = []
|
44
|
+
|
45
|
+
def update(self, scheduled_operation: ScheduledOperation):
|
46
|
+
self.history.append(scheduled_operation)
|
47
|
+
|
48
|
+
def reset(self):
|
49
|
+
self.history = []
|
50
|
+
|
51
|
+
"""
|
52
|
+
|
53
|
+
# Made read-only following Google Style Guide recommendation
|
54
|
+
_is_singleton = True
|
55
|
+
"""If True, ensures only one instance of this observer type is subscribed
|
56
|
+
to the dispatcher."""
|
23
57
|
|
24
58
|
def __init__(
|
25
59
|
self,
|
26
60
|
dispatcher: Dispatcher,
|
27
|
-
|
61
|
+
*,
|
28
62
|
subscribe: bool = True,
|
29
63
|
):
|
30
|
-
"""Initializes the observer with the
|
31
|
-
it.
|
64
|
+
"""Initializes the observer with the :class:`Dispatcher` and subscribes
|
65
|
+
to it.
|
32
66
|
|
33
67
|
Args:
|
34
|
-
|
35
|
-
The
|
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.
|
68
|
+
dispatcher:
|
69
|
+
The `Dispatcher` instance to observe.
|
42
70
|
subscribe:
|
43
|
-
|
44
|
-
|
45
|
-
|
71
|
+
If True, automatically subscribes the observer to the
|
72
|
+
dispatcher.
|
73
|
+
|
74
|
+
Raises:
|
75
|
+
ValidationError: If ``is_singleton`` is True and an observer of the
|
76
|
+
same type already exists in the dispatcher's list of
|
77
|
+
subscribers.
|
46
78
|
"""
|
47
|
-
if
|
79
|
+
if self._is_singleton and any(
|
48
80
|
isinstance(observer, self.__class__)
|
49
81
|
for observer in dispatcher.subscribers
|
50
82
|
):
|
51
|
-
raise
|
83
|
+
raise ValidationError(
|
52
84
|
f"An observer of type {self.__class__.__name__} already "
|
53
85
|
"exists in the dispatcher's list of subscribers. If you want "
|
54
86
|
"to create multiple instances of this observer, set "
|
@@ -59,6 +91,15 @@ class DispatcherObserver(abc.ABC):
|
|
59
91
|
if subscribe:
|
60
92
|
self.dispatcher.subscribe(self)
|
61
93
|
|
94
|
+
@property
|
95
|
+
def is_singleton(self) -> bool:
|
96
|
+
"""Returns whether this observer is a singleton.
|
97
|
+
|
98
|
+
This is a class attribute that determines whether only one
|
99
|
+
instance of this observer type can be subscribed to the dispatcher.
|
100
|
+
"""
|
101
|
+
return self._is_singleton
|
102
|
+
|
62
103
|
@abc.abstractmethod
|
63
104
|
def update(self, scheduled_operation: ScheduledOperation):
|
64
105
|
"""Called when an operation is scheduled on a machine."""
|
@@ -74,6 +115,11 @@ class DispatcherObserver(abc.ABC):
|
|
74
115
|
return self.__class__.__name__
|
75
116
|
|
76
117
|
|
118
|
+
# Disable pylint's false positive
|
119
|
+
# pylint: disable=invalid-name
|
120
|
+
ObserverType = TypeVar("ObserverType", bound=DispatcherObserver)
|
121
|
+
|
122
|
+
|
77
123
|
def _dispatcher_cache(method):
|
78
124
|
"""Decorator to cache results of a method based on its name.
|
79
125
|
|
@@ -115,7 +161,7 @@ class Dispatcher:
|
|
115
161
|
The instance of the job shop problem to be scheduled.
|
116
162
|
schedule:
|
117
163
|
The schedule of operations on machines.
|
118
|
-
|
164
|
+
ready_operations_filter:
|
119
165
|
A function that filters out operations that are not ready to be
|
120
166
|
scheduled.
|
121
167
|
"""
|
@@ -126,7 +172,7 @@ class Dispatcher:
|
|
126
172
|
"_machine_next_available_time",
|
127
173
|
"_job_next_operation_index",
|
128
174
|
"_job_next_available_time",
|
129
|
-
"
|
175
|
+
"ready_operations_filter",
|
130
176
|
"subscribers",
|
131
177
|
"_cache",
|
132
178
|
)
|
@@ -134,7 +180,7 @@ class Dispatcher:
|
|
134
180
|
def __init__(
|
135
181
|
self,
|
136
182
|
instance: JobShopInstance,
|
137
|
-
|
183
|
+
ready_operations_filter: (
|
138
184
|
Callable[[Dispatcher, list[Operation]], list[Operation]] | None
|
139
185
|
) = None,
|
140
186
|
) -> None:
|
@@ -143,16 +189,17 @@ class Dispatcher:
|
|
143
189
|
Args:
|
144
190
|
instance:
|
145
191
|
The instance of the job shop problem to be solved.
|
146
|
-
|
192
|
+
ready_operations_filter:
|
147
193
|
A function that filters out operations that are not ready to
|
148
194
|
be scheduled. The function should take the dispatcher and a
|
149
195
|
list of operations as input and return a list of operations
|
150
|
-
that are ready to be scheduled. If
|
196
|
+
that are ready to be scheduled. If ``None``, no filtering is
|
197
|
+
done.
|
151
198
|
"""
|
152
199
|
|
153
200
|
self.instance = instance
|
154
201
|
self.schedule = Schedule(self.instance)
|
155
|
-
self.
|
202
|
+
self.ready_operations_filter = ready_operations_filter
|
156
203
|
|
157
204
|
self._machine_next_available_time = [0] * self.instance.num_machines
|
158
205
|
self._job_next_operation_index = [0] * self.instance.num_jobs
|
@@ -207,6 +254,7 @@ class Dispatcher:
|
|
207
254
|
available time for the machine and the next available time for the
|
208
255
|
job to which the operation belongs. The operation is then scheduled
|
209
256
|
on the machine and the tracking attributes are updated.
|
257
|
+
|
210
258
|
Args:
|
211
259
|
operation:
|
212
260
|
The operation to be scheduled.
|
@@ -215,11 +263,11 @@ class Dispatcher:
|
|
215
263
|
scheduled.
|
216
264
|
|
217
265
|
Raises:
|
218
|
-
|
266
|
+
ValidationError: If the operation is not ready to be scheduled.
|
219
267
|
"""
|
220
268
|
|
221
269
|
if not self.is_operation_ready(operation):
|
222
|
-
raise
|
270
|
+
raise ValidationError("Operation is not ready to be scheduled.")
|
223
271
|
|
224
272
|
start_time = self.start_time(operation, machine_id)
|
225
273
|
|
@@ -229,10 +277,6 @@ class Dispatcher:
|
|
229
277
|
self.schedule.add(scheduled_operation)
|
230
278
|
self._update_tracking_attributes(scheduled_operation)
|
231
279
|
|
232
|
-
# Notify subscribers
|
233
|
-
for subscriber in self.subscribers:
|
234
|
-
subscriber.update(scheduled_operation)
|
235
|
-
|
236
280
|
def is_operation_ready(self, operation: Operation) -> bool:
|
237
281
|
"""Returns True if the given operation is ready to be scheduled.
|
238
282
|
|
@@ -265,7 +309,7 @@ class Dispatcher:
|
|
265
309
|
The operation to be scheduled.
|
266
310
|
machine_id:
|
267
311
|
The id of the machine on which the operation is to be
|
268
|
-
scheduled. If None
|
312
|
+
scheduled. If ``None``, the start time is computed based on the
|
269
313
|
next available time for the operation on any machine.
|
270
314
|
"""
|
271
315
|
return max(
|
@@ -286,6 +330,40 @@ class Dispatcher:
|
|
286
330
|
self._job_next_available_time[job_id] = end_time
|
287
331
|
self._cache = {}
|
288
332
|
|
333
|
+
# Notify subscribers
|
334
|
+
for subscriber in self.subscribers:
|
335
|
+
subscriber.update(scheduled_operation)
|
336
|
+
|
337
|
+
def create_or_get_observer(
|
338
|
+
self,
|
339
|
+
observer: type[ObserverType],
|
340
|
+
condition: Callable[[DispatcherObserver], bool] = lambda _: True,
|
341
|
+
**kwargs,
|
342
|
+
) -> ObserverType:
|
343
|
+
"""Creates a new observer of the specified type or returns an existing
|
344
|
+
observer of the same type if it already exists in the dispatcher's list
|
345
|
+
of observers.
|
346
|
+
|
347
|
+
Args:
|
348
|
+
observer:
|
349
|
+
The type of observer to be created or retrieved.
|
350
|
+
condition:
|
351
|
+
A function that takes an observer and returns True if it is
|
352
|
+
the observer to be retrieved. By default, it returns True for
|
353
|
+
all observers.
|
354
|
+
**kwargs:
|
355
|
+
Additional keyword arguments to be passed to the observer's
|
356
|
+
constructor.
|
357
|
+
"""
|
358
|
+
for existing_observer in self.subscribers:
|
359
|
+
if isinstance(existing_observer, observer) and condition(
|
360
|
+
existing_observer
|
361
|
+
):
|
362
|
+
return existing_observer
|
363
|
+
|
364
|
+
new_observer = observer(self, **kwargs)
|
365
|
+
return new_observer
|
366
|
+
|
289
367
|
@_dispatcher_cache
|
290
368
|
def current_time(self) -> int:
|
291
369
|
"""Returns the current time of the schedule.
|
@@ -293,7 +371,7 @@ class Dispatcher:
|
|
293
371
|
The current time is the minimum start time of the available
|
294
372
|
operations.
|
295
373
|
"""
|
296
|
-
available_operations = self.
|
374
|
+
available_operations = self.ready_operations()
|
297
375
|
current_time = self.min_start_time(available_operations)
|
298
376
|
return current_time
|
299
377
|
|
@@ -309,39 +387,39 @@ class Dispatcher:
|
|
309
387
|
return int(min_start_time)
|
310
388
|
|
311
389
|
@_dispatcher_cache
|
312
|
-
def
|
390
|
+
def ready_operations(self) -> list[Operation]:
|
313
391
|
"""Returns a list of available operations for processing, optionally
|
314
|
-
filtering out operations using the
|
392
|
+
filtering out operations using the filter function.
|
315
393
|
|
316
394
|
This method first gathers all possible next operations from the jobs
|
317
395
|
being processed. It then optionally filters these operations using the
|
318
|
-
|
396
|
+
filter function.
|
319
397
|
|
320
398
|
Returns:
|
321
|
-
A list of Operation objects that are available for
|
399
|
+
A list of :class:`Operation` objects that are available for
|
400
|
+
scheduling.
|
322
401
|
"""
|
323
|
-
available_operations = self.
|
324
|
-
if self.
|
325
|
-
available_operations = self.
|
402
|
+
available_operations = self.raw_ready_operations()
|
403
|
+
if self.ready_operations_filter is not None:
|
404
|
+
available_operations = self.ready_operations_filter(
|
326
405
|
self, available_operations
|
327
406
|
)
|
328
407
|
return available_operations
|
329
408
|
|
330
409
|
@_dispatcher_cache
|
331
|
-
def
|
410
|
+
def raw_ready_operations(self) -> list[Operation]:
|
332
411
|
"""Returns a list of available operations for processing without
|
333
|
-
applying the
|
412
|
+
applying the filter function.
|
334
413
|
|
335
414
|
Returns:
|
336
|
-
A list of Operation objects that are available for
|
337
|
-
based on precedence and machine constraints only.
|
415
|
+
A list of :class:`Operation` objects that are available for
|
416
|
+
scheduling based on precedence and machine constraints only.
|
338
417
|
"""
|
339
|
-
available_operations = [
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
available_operations.append(operation)
|
418
|
+
available_operations = [
|
419
|
+
self.instance.jobs[job_id][position]
|
420
|
+
for job_id, position in enumerate(self._job_next_operation_index)
|
421
|
+
if position < len(self.instance.jobs[job_id])
|
422
|
+
]
|
345
423
|
return available_operations
|
346
424
|
|
347
425
|
@_dispatcher_cache
|
@@ -364,8 +442,8 @@ class Dispatcher:
|
|
364
442
|
|
365
443
|
@_dispatcher_cache
|
366
444
|
def available_machines(self) -> list[int]:
|
367
|
-
"""Returns the list of
|
368
|
-
available_operations = self.
|
445
|
+
"""Returns the list of ready machines."""
|
446
|
+
available_operations = self.ready_operations()
|
369
447
|
available_machines = set()
|
370
448
|
for operation in available_operations:
|
371
449
|
available_machines.update(operation.machines)
|
@@ -373,8 +451,8 @@ class Dispatcher:
|
|
373
451
|
|
374
452
|
@_dispatcher_cache
|
375
453
|
def available_jobs(self) -> list[int]:
|
376
|
-
"""Returns the list of
|
377
|
-
available_operations = self.
|
454
|
+
"""Returns the list of ready jobs."""
|
455
|
+
available_operations = self.ready_operations()
|
378
456
|
available_jobs = set(
|
379
457
|
operation.job_id for operation in available_operations
|
380
458
|
)
|
@@ -384,7 +462,7 @@ class Dispatcher:
|
|
384
462
|
"""Calculates the earliest start time for a given operation based on
|
385
463
|
machine and job constraints.
|
386
464
|
|
387
|
-
This method is different from the
|
465
|
+
This method is different from the ``start_time`` method in that it
|
388
466
|
takes into account every machine that can process the operation, not
|
389
467
|
just the one that will process it. However, it also assumes that
|
390
468
|
the operation is ready to be scheduled in the job in favor of
|
@@ -489,28 +567,23 @@ class Dispatcher:
|
|
489
567
|
current_time = self.current_time()
|
490
568
|
return scheduled_operation.start_time <= current_time
|
491
569
|
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
570
|
+
def next_operation(self, job_id: int) -> Operation:
|
571
|
+
"""Returns the next operation to be scheduled for the given job.
|
572
|
+
|
573
|
+
Args:
|
574
|
+
job_id:
|
575
|
+
The id of the job for which to return the next operation.
|
576
|
+
|
577
|
+
Raises:
|
578
|
+
ValidationError: If there are no more operations left for the job.
|
579
|
+
"""
|
580
|
+
if (
|
581
|
+
len(self.instance.jobs[job_id])
|
582
|
+
<= self._job_next_operation_index[job_id]
|
583
|
+
):
|
584
|
+
raise ValidationError(
|
585
|
+
f"No more operations left for job {job_id} to schedule."
|
586
|
+
)
|
587
|
+
return self.instance.jobs[job_id][
|
588
|
+
self._job_next_operation_index[job_id]
|
507
589
|
]
|
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,54 @@
|
|
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
|
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(slots=True, 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
|
+
Attributes:
|
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
|
+
DispatcherObserver instances from the factory functions.
|
34
|
+
kwargs:
|
35
|
+
Keyword arguments needed to initialize the class. It must not
|
36
|
+
contain the ``dispatcher`` argument.
|
37
|
+
"""
|
38
|
+
|
39
|
+
# We use the type hint T, instead of ObserverType, to allow for string or
|
40
|
+
# specific Enum values to be passed as the type argument. For example:
|
41
|
+
# FeatureObserverConfig = DispatcherObserverConfig[
|
42
|
+
# type[FeatureObserver] | FeatureObserverType | str
|
43
|
+
# ]
|
44
|
+
# This allows for the creation of a FeatureObserver instance
|
45
|
+
# from the factory function.
|
46
|
+
class_type: T
|
47
|
+
kwargs: dict[str, Any] = field(default_factory=dict)
|
48
|
+
|
49
|
+
def __post_init__(self):
|
50
|
+
if "dispatcher" in self.kwargs:
|
51
|
+
raise ValidationError(
|
52
|
+
"The 'dispatcher' argument should not be included in the "
|
53
|
+
"kwargs attribute."
|
54
|
+
)
|
@@ -0,0 +1,125 @@
|
|
1
|
+
"""Contains factory functions for creating ready operations filters.
|
2
|
+
|
3
|
+
The factory functions create and return the appropriate functions based on the
|
4
|
+
specified names or enums.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from enum import Enum
|
8
|
+
from collections.abc import Iterable
|
9
|
+
|
10
|
+
from job_shop_lib import Operation
|
11
|
+
from job_shop_lib.exceptions import ValidationError
|
12
|
+
from job_shop_lib.dispatching import (
|
13
|
+
Dispatcher,
|
14
|
+
filter_dominated_operations,
|
15
|
+
filter_non_immediate_machines,
|
16
|
+
ReadyOperationsFilter,
|
17
|
+
)
|
18
|
+
|
19
|
+
|
20
|
+
class ReadyOperationsFilterType(str, Enum):
|
21
|
+
"""Enumeration of ready operations filter types.
|
22
|
+
|
23
|
+
A filter function is used by the
|
24
|
+
:class:`~job_shop_lib.dispatching.Dispatcher` class to reduce the
|
25
|
+
amount of available operations to choose from.
|
26
|
+
"""
|
27
|
+
|
28
|
+
DOMINATED_OPERATIONS = "dominated_operations"
|
29
|
+
NON_IMMEDIATE_MACHINES = "non_immediate_machines"
|
30
|
+
|
31
|
+
|
32
|
+
def create_composite_operation_filter(
|
33
|
+
ready_operations_filters: Iterable[
|
34
|
+
ReadyOperationsFilter | str | ReadyOperationsFilterType
|
35
|
+
],
|
36
|
+
) -> ReadyOperationsFilter:
|
37
|
+
"""Creates and returns a composite filter based on the specified list of
|
38
|
+
filters.
|
39
|
+
|
40
|
+
The composite filter function filters operations based on the specified
|
41
|
+
list of filter strategies.
|
42
|
+
|
43
|
+
Args:
|
44
|
+
ready_operations_filters:
|
45
|
+
A list of filter strategies to be used.
|
46
|
+
Supported values are 'dominated_operations' and
|
47
|
+
'non_immediate_machines' or any Callable that takes a
|
48
|
+
:class:`~job_shop_lib.dispatching.Dispatcher` instance and a list
|
49
|
+
of :class:`~job_shop_lib.Operation` instances as input
|
50
|
+
and returns a list of :class:`~job_shop_lib.Operation`instances.
|
51
|
+
|
52
|
+
Returns:
|
53
|
+
A function that takes a :class:`~job_shop_lib.dispatching.Dispatcher`
|
54
|
+
instance and a list of :class:`~job_shop_lib.Operation`
|
55
|
+
instances as input and returns a list of
|
56
|
+
:class:`~job_shop_lib.Operation`instances based on
|
57
|
+
the specified list of filter strategies.
|
58
|
+
|
59
|
+
Raises:
|
60
|
+
ValidationError: If any of the filter strategies in the list are not
|
61
|
+
recognized or are not supported.
|
62
|
+
"""
|
63
|
+
|
64
|
+
filter_functions = [
|
65
|
+
ready_operations_filter_factory(name)
|
66
|
+
for name in ready_operations_filters
|
67
|
+
]
|
68
|
+
|
69
|
+
def composite_pruning_function(
|
70
|
+
dispatcher: Dispatcher, operations: list[Operation]
|
71
|
+
) -> list[Operation]:
|
72
|
+
pruned_operations = operations
|
73
|
+
for pruning_function in filter_functions:
|
74
|
+
pruned_operations = pruning_function(dispatcher, pruned_operations)
|
75
|
+
|
76
|
+
return pruned_operations
|
77
|
+
|
78
|
+
return composite_pruning_function
|
79
|
+
|
80
|
+
|
81
|
+
def ready_operations_filter_factory(
|
82
|
+
filter_name: str | ReadyOperationsFilterType | ReadyOperationsFilter,
|
83
|
+
) -> ReadyOperationsFilter:
|
84
|
+
"""Creates and returns a filter function based on the specified
|
85
|
+
filter strategy name.
|
86
|
+
|
87
|
+
The filter function filters operations based on certain criteria such as
|
88
|
+
dominated operations, immediate machine operations, etc.
|
89
|
+
|
90
|
+
Args:
|
91
|
+
filter_name:
|
92
|
+
The name of the filter function to be used. Supported
|
93
|
+
values are 'dominated_operations' and
|
94
|
+
'immediate_machine_operations'.
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
A function that takes a :class:`~job_shop_lib.dispatching.Dispatcher`
|
98
|
+
instance and a list of :class:`~job_shop_lib.Operation`
|
99
|
+
instances as input and returns a list of
|
100
|
+
:class:`~job_shop_lib.Operation` instances based on
|
101
|
+
the specified filter function.
|
102
|
+
|
103
|
+
Raises:
|
104
|
+
ValidationError: If the ``filter_name`` argument is not recognized or
|
105
|
+
is not supported.
|
106
|
+
"""
|
107
|
+
if callable(filter_name):
|
108
|
+
return filter_name
|
109
|
+
|
110
|
+
filtering_strategies = {
|
111
|
+
ReadyOperationsFilterType.DOMINATED_OPERATIONS: (
|
112
|
+
filter_dominated_operations
|
113
|
+
),
|
114
|
+
ReadyOperationsFilterType.NON_IMMEDIATE_MACHINES: (
|
115
|
+
filter_non_immediate_machines
|
116
|
+
),
|
117
|
+
}
|
118
|
+
|
119
|
+
if filter_name not in filtering_strategies:
|
120
|
+
raise ValidationError(
|
121
|
+
f"Unsupported filter function '{filter_name}'. "
|
122
|
+
f"Supported values are {', '.join(filtering_strategies.keys())}."
|
123
|
+
)
|
124
|
+
|
125
|
+
return filtering_strategies[filter_name] # type: ignore[index]
|
@@ -1,16 +1,14 @@
|
|
1
|
-
"""Home of the `
|
1
|
+
"""Home of the `HistoryObserver` class."""
|
2
2
|
|
3
3
|
from job_shop_lib.dispatching import DispatcherObserver, Dispatcher
|
4
4
|
from job_shop_lib import ScheduledOperation
|
5
5
|
|
6
6
|
|
7
|
-
class
|
7
|
+
class HistoryObserver(DispatcherObserver):
|
8
8
|
"""Observer that stores the history of the dispatcher."""
|
9
9
|
|
10
|
-
def __init__(self, dispatcher: Dispatcher):
|
11
|
-
|
12
|
-
dispatcher."""
|
13
|
-
super().__init__(dispatcher)
|
10
|
+
def __init__(self, dispatcher: Dispatcher, *, subscribe: bool = True):
|
11
|
+
super().__init__(dispatcher, subscribe=subscribe)
|
14
12
|
self.history: list[ScheduledOperation] = []
|
15
13
|
|
16
14
|
def update(self, scheduled_operation: ScheduledOperation):
|
@@ -4,47 +4,18 @@ This functions are used by the `Dispatcher` class to reduce the
|
|
4
4
|
amount of available operations to choose from.
|
5
5
|
"""
|
6
6
|
|
7
|
-
from collections.abc import Callable
|
7
|
+
from collections.abc import Callable
|
8
8
|
|
9
9
|
from job_shop_lib import Operation
|
10
10
|
from job_shop_lib.dispatching import Dispatcher
|
11
11
|
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
],
|
17
|
-
) -> Callable[[Dispatcher, list[Operation]], list[Operation]]:
|
18
|
-
"""Creates and returns a composite pruning strategy function based on the
|
19
|
-
specified list of pruning strategies.
|
20
|
-
The composite pruning strategy function filters out operations based on
|
21
|
-
the specified list of pruning strategies.
|
22
|
-
Args:
|
23
|
-
pruning_strategies:
|
24
|
-
A list of pruning strategies to be used. Supported values are
|
25
|
-
'dominated_operations' and 'non_immediate_machines'.
|
26
|
-
Returns:
|
27
|
-
A function that takes a Dispatcher instance and a list of Operation
|
28
|
-
instances as input and returns a list of Operation instances based on
|
29
|
-
the specified list of pruning strategies.
|
30
|
-
Raises:
|
31
|
-
ValueError: If any of the pruning strategies in the list are not
|
32
|
-
recognized or are not supported.
|
33
|
-
"""
|
34
|
-
|
35
|
-
def composite_pruning_function(
|
36
|
-
dispatcher: Dispatcher, operations: list[Operation]
|
37
|
-
) -> list[Operation]:
|
38
|
-
pruned_operations = operations
|
39
|
-
for pruning_function in pruning_functions:
|
40
|
-
pruned_operations = pruning_function(dispatcher, pruned_operations)
|
41
|
-
|
42
|
-
return pruned_operations
|
43
|
-
|
44
|
-
return composite_pruning_function
|
13
|
+
ReadyOperationsFilter = Callable[
|
14
|
+
[Dispatcher, list[Operation]], list[Operation]
|
15
|
+
]
|
45
16
|
|
46
17
|
|
47
|
-
def
|
18
|
+
def filter_dominated_operations(
|
48
19
|
dispatcher: Dispatcher, operations: list[Operation]
|
49
20
|
) -> list[Operation]:
|
50
21
|
"""Filters out all the operations that are dominated.
|
@@ -69,7 +40,7 @@ def prune_dominated_operations(
|
|
69
40
|
return non_dominated_operations
|
70
41
|
|
71
42
|
|
72
|
-
def
|
43
|
+
def filter_non_immediate_machines(
|
73
44
|
dispatcher: Dispatcher, operations: list[Operation]
|
74
45
|
) -> list[Operation]:
|
75
46
|
"""Filters out all the operations associated with machines which earliest
|