job-shop-lib 0.4.0__tar.gz → 0.5.1__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/PKG-INFO +9 -5
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/README.md +6 -4
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/dispatching/dispatcher.py +219 -51
- job_shop_lib-0.5.1/job_shop_lib/dispatching/feature_observers/__init__.py +28 -0
- job_shop_lib-0.5.1/job_shop_lib/dispatching/feature_observers/composite_feature_observer.py +87 -0
- job_shop_lib-0.5.1/job_shop_lib/dispatching/feature_observers/duration_observer.py +95 -0
- job_shop_lib-0.5.1/job_shop_lib/dispatching/feature_observers/earliest_start_time_observer.py +156 -0
- job_shop_lib-0.5.1/job_shop_lib/dispatching/feature_observers/factory.py +58 -0
- job_shop_lib-0.5.1/job_shop_lib/dispatching/feature_observers/feature_observer.py +113 -0
- job_shop_lib-0.5.1/job_shop_lib/dispatching/feature_observers/is_completed_observer.py +98 -0
- job_shop_lib-0.5.1/job_shop_lib/dispatching/feature_observers/is_ready_observer.py +40 -0
- job_shop_lib-0.5.1/job_shop_lib/dispatching/feature_observers/is_scheduled_observer.py +34 -0
- job_shop_lib-0.5.1/job_shop_lib/dispatching/feature_observers/position_in_job_observer.py +39 -0
- job_shop_lib-0.5.1/job_shop_lib/dispatching/feature_observers/remaining_operations_observer.py +54 -0
- job_shop_lib-0.5.1/job_shop_lib/generation/__init__.py +11 -0
- job_shop_lib-0.5.1/job_shop_lib/generation/general_instance_generator.py +169 -0
- job_shop_lib-0.5.1/job_shop_lib/generation/instance_generator.py +122 -0
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/generators/__init__.py +2 -1
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/generators/basic_generator.py +3 -0
- job_shop_lib-0.5.1/job_shop_lib/generators/transformations.py +164 -0
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/graphs/build_disjunctive_graph.py +20 -0
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/job_shop_instance.py +101 -0
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/visualization/create_gif.py +47 -38
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/visualization/gantt_chart.py +1 -1
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/pyproject.toml +3 -3
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/LICENSE +0 -0
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/__init__.py +0 -0
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/base_solver.py +0 -0
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/benchmarking/__init__.py +0 -0
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/benchmarking/benchmark_instances.json +0 -0
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/benchmarking/load_benchmark.py +0 -0
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/cp_sat/__init__.py +0 -0
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/cp_sat/ortools_solver.py +0 -0
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/dispatching/__init__.py +0 -0
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/dispatching/dispatching_rule_solver.py +0 -0
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/dispatching/dispatching_rules.py +0 -0
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/dispatching/factories.py +0 -0
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/dispatching/history_tracker.py +0 -0
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/dispatching/pruning_functions.py +0 -0
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/exceptions.py +0 -0
- {job_shop_lib-0.4.0/job_shop_lib/generators → job_shop_lib-0.5.1/job_shop_lib/generation}/transformations.py +0 -0
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/graphs/__init__.py +0 -0
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/graphs/build_agent_task_graph.py +0 -0
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/graphs/constants.py +0 -0
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/graphs/job_shop_graph.py +0 -0
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/graphs/node.py +0 -0
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/operation.py +0 -0
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/schedule.py +0 -0
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/scheduled_operation.py +0 -0
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/visualization/__init__.py +0 -0
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/visualization/agent_task_graph.py +0 -0
- {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/visualization/disjunctive_graph.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: job-shop-lib
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.5.1
|
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
|
@@ -15,7 +15,8 @@ Provides-Extra: pygraphviz
|
|
15
15
|
Requires-Dist: imageio (>=2,<3)
|
16
16
|
Requires-Dist: matplotlib (>=3,<4)
|
17
17
|
Requires-Dist: networkx (>=3,<4)
|
18
|
-
Requires-Dist:
|
18
|
+
Requires-Dist: numpy (>=1.26.4,<2.0.0)
|
19
|
+
Requires-Dist: ortools (>=9.9,<9.10)
|
19
20
|
Requires-Dist: pyarrow (>=15.0.0,<16.0.0)
|
20
21
|
Requires-Dist: pygraphviz (>=1.12,<2.0) ; extra == "pygraphviz"
|
21
22
|
Description-Content-Type: text/markdown
|
@@ -83,7 +84,7 @@ ft06 = load_benchmark_instance("ft06")
|
|
83
84
|
```
|
84
85
|
|
85
86
|
The module `benchmarks` contains functions to load the instances from the file and return them as `JobShopInstance` objects without having to download them
|
86
|
-
manually.
|
87
|
+
manually.
|
87
88
|
|
88
89
|
The contributions to this benchmark dataset are as follows:
|
89
90
|
|
@@ -171,13 +172,15 @@ class DispatchingRule(str, Enum):
|
|
171
172
|
We can visualize the solution with a `DispatchingRuleSolver` as a gif:
|
172
173
|
|
173
174
|
```python
|
174
|
-
from job_shop_lib.visualization import create_gif,
|
175
|
+
from job_shop_lib.visualization import create_gif, plot_gantt_chart_wrapper
|
175
176
|
from job_shop_lib.dispatching import DispatchingRuleSolver, DispatchingRule
|
176
177
|
|
177
178
|
plt.style.use("ggplot")
|
178
179
|
|
179
180
|
mwkr_solver = DispatchingRuleSolver("most_work_remaining")
|
180
|
-
plot_function =
|
181
|
+
plot_function = plot_gantt_chart_wrapper(
|
182
|
+
title="Solution with Most Work Remaining Rule"
|
183
|
+
)
|
181
184
|
create_gif(
|
182
185
|
gif_path="ft06_optimized.gif",
|
183
186
|
instance=ft06,
|
@@ -350,3 +353,4 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
350
353
|
Journal of Operational Research, vol. 64, no. 2, pp. 278–285, 1993.
|
351
354
|
|
352
355
|
- Park, Junyoung, Sanjar Bakhtiyar, and Jinkyoo Park. "ScheduleNet: Learn to solve multi-agent scheduling problems with reinforcement learning." arXiv preprint arXiv:2106.03051, 2021.
|
356
|
+
|
@@ -61,7 +61,7 @@ ft06 = load_benchmark_instance("ft06")
|
|
61
61
|
```
|
62
62
|
|
63
63
|
The module `benchmarks` contains functions to load the instances from the file and return them as `JobShopInstance` objects without having to download them
|
64
|
-
manually.
|
64
|
+
manually.
|
65
65
|
|
66
66
|
The contributions to this benchmark dataset are as follows:
|
67
67
|
|
@@ -149,13 +149,15 @@ class DispatchingRule(str, Enum):
|
|
149
149
|
We can visualize the solution with a `DispatchingRuleSolver` as a gif:
|
150
150
|
|
151
151
|
```python
|
152
|
-
from job_shop_lib.visualization import create_gif,
|
152
|
+
from job_shop_lib.visualization import create_gif, plot_gantt_chart_wrapper
|
153
153
|
from job_shop_lib.dispatching import DispatchingRuleSolver, DispatchingRule
|
154
154
|
|
155
155
|
plt.style.use("ggplot")
|
156
156
|
|
157
157
|
mwkr_solver = DispatchingRuleSolver("most_work_remaining")
|
158
|
-
plot_function =
|
158
|
+
plot_function = plot_gantt_chart_wrapper(
|
159
|
+
title="Solution with Most Work Remaining Rule"
|
160
|
+
)
|
159
161
|
create_gif(
|
160
162
|
gif_path="ft06_optimized.gif",
|
161
163
|
instance=ft06,
|
@@ -327,4 +329,4 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
327
329
|
- E. Taillard, "Benchmarks for basic scheduling problems," European
|
328
330
|
Journal of Operational Research, vol. 64, no. 2, pp. 278–285, 1993.
|
329
331
|
|
330
|
-
- Park, Junyoung, Sanjar Bakhtiyar, and Jinkyoo Park. "ScheduleNet: Learn to solve multi-agent scheduling problems with reinforcement learning." arXiv preprint arXiv:2106.03051, 2021.
|
332
|
+
- Park, Junyoung, Sanjar Bakhtiyar, and Jinkyoo Park. "ScheduleNet: Learn to solve multi-agent scheduling problems with reinforcement learning." arXiv preprint arXiv:2106.03051, 2021.
|
@@ -19,13 +19,45 @@ from job_shop_lib import (
|
|
19
19
|
|
20
20
|
# Added here to avoid circular imports
|
21
21
|
class DispatcherObserver(abc.ABC):
|
22
|
-
"""Interface for classes that observe
|
22
|
+
"""Interface for classes that observe th"""
|
23
|
+
|
24
|
+
def __init__(
|
25
|
+
self,
|
26
|
+
dispatcher: Dispatcher,
|
27
|
+
is_singleton: bool = True,
|
28
|
+
subscribe: bool = True,
|
29
|
+
):
|
30
|
+
"""Initializes the observer with the `Dispatcher` and subscribes to
|
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(
|
48
|
+
isinstance(observer, self.__class__)
|
49
|
+
for observer in dispatcher.subscribers
|
50
|
+
):
|
51
|
+
raise ValueError(
|
52
|
+
f"An observer of type {self.__class__.__name__} already "
|
53
|
+
"exists in the dispatcher's list of subscribers. If you want "
|
54
|
+
"to create multiple instances of this observer, set "
|
55
|
+
"`is_singleton` to False."
|
56
|
+
)
|
23
57
|
|
24
|
-
def __init__(self, dispatcher: Dispatcher):
|
25
|
-
"""Initializes the observer with the dispatcher and subscribes to
|
26
|
-
it."""
|
27
58
|
self.dispatcher = dispatcher
|
28
|
-
|
59
|
+
if subscribe:
|
60
|
+
self.dispatcher.subscribe(self)
|
29
61
|
|
30
62
|
@abc.abstractmethod
|
31
63
|
def update(self, scheduled_operation: ScheduledOperation):
|
@@ -39,7 +71,7 @@ class DispatcherObserver(abc.ABC):
|
|
39
71
|
return self.__class__.__name__
|
40
72
|
|
41
73
|
def __repr__(self) -> str:
|
42
|
-
return
|
74
|
+
return self.__class__.__name__
|
43
75
|
|
44
76
|
|
45
77
|
def _dispatcher_cache(method):
|
@@ -86,8 +118,6 @@ class Dispatcher:
|
|
86
118
|
pruning_function:
|
87
119
|
A function that filters out operations that are not ready to be
|
88
120
|
scheduled.
|
89
|
-
subscribers:
|
90
|
-
A list of observers that are subscribed to the dispatcher.
|
91
121
|
"""
|
92
122
|
|
93
123
|
__slots__ = (
|
@@ -152,32 +182,6 @@ class Dispatcher:
|
|
152
182
|
"""Returns the next available time for each job."""
|
153
183
|
return self._job_next_available_time
|
154
184
|
|
155
|
-
@classmethod
|
156
|
-
def create_schedule_from_raw_solution(
|
157
|
-
cls, instance: JobShopInstance, raw_solution: list[list[Operation]]
|
158
|
-
) -> Schedule:
|
159
|
-
"""Deprecated method, use `Schedule.from_job_sequences` instead."""
|
160
|
-
warn(
|
161
|
-
"Dispatcher.create_schedule_from_raw_solution is deprecated. "
|
162
|
-
"Use Schedule.from_job_sequences instead. It will be removed in "
|
163
|
-
"version 1.0.0.",
|
164
|
-
DeprecationWarning,
|
165
|
-
)
|
166
|
-
dispatcher = cls(instance)
|
167
|
-
dispatcher.reset()
|
168
|
-
raw_solution_deques = [
|
169
|
-
deque(operations) for operations in raw_solution
|
170
|
-
]
|
171
|
-
while not dispatcher.schedule.is_complete():
|
172
|
-
for machine_id, operations in enumerate(raw_solution_deques):
|
173
|
-
if not operations:
|
174
|
-
continue
|
175
|
-
operation = operations[0]
|
176
|
-
if dispatcher.is_operation_ready(operation):
|
177
|
-
dispatcher.dispatch(operation, machine_id)
|
178
|
-
operations.popleft()
|
179
|
-
return dispatcher.schedule
|
180
|
-
|
181
185
|
def subscribe(self, observer: DispatcherObserver):
|
182
186
|
"""Subscribes an observer to the dispatcher."""
|
183
187
|
self.subscribers.append(observer)
|
@@ -304,20 +308,6 @@ class Dispatcher:
|
|
304
308
|
min_start_time = min(min_start_time, start_time)
|
305
309
|
return int(min_start_time)
|
306
310
|
|
307
|
-
@_dispatcher_cache
|
308
|
-
def uncompleted_operations(self) -> list[Operation]:
|
309
|
-
"""Returns the list of operations that have not been scheduled.
|
310
|
-
|
311
|
-
An operation is uncompleted if it has not been scheduled yet.
|
312
|
-
|
313
|
-
It is more efficient than checking all operations in the instance.
|
314
|
-
"""
|
315
|
-
uncompleted_operations = []
|
316
|
-
for job_id, next_position in enumerate(self._job_next_operation_index):
|
317
|
-
operations = self.instance.jobs[job_id][next_position:]
|
318
|
-
uncompleted_operations.extend(operations)
|
319
|
-
return uncompleted_operations
|
320
|
-
|
321
311
|
@_dispatcher_cache
|
322
312
|
def available_operations(self) -> list[Operation]:
|
323
313
|
"""Returns a list of available operations for processing, optionally
|
@@ -330,15 +320,22 @@ class Dispatcher:
|
|
330
320
|
Returns:
|
331
321
|
A list of Operation objects that are available for scheduling.
|
332
322
|
"""
|
333
|
-
|
334
|
-
available_operations = self._available_operations()
|
323
|
+
available_operations = self.available_operations_without_pruning()
|
335
324
|
if self.pruning_function is not None:
|
336
325
|
available_operations = self.pruning_function(
|
337
326
|
self, available_operations
|
338
327
|
)
|
339
328
|
return available_operations
|
340
329
|
|
341
|
-
|
330
|
+
@_dispatcher_cache
|
331
|
+
def available_operations_without_pruning(self) -> list[Operation]:
|
332
|
+
"""Returns a list of available operations for processing without
|
333
|
+
applying the pruning function.
|
334
|
+
|
335
|
+
Returns:
|
336
|
+
A list of Operation objects that are available for scheduling
|
337
|
+
based on precedence and machine constraints only.
|
338
|
+
"""
|
342
339
|
available_operations = []
|
343
340
|
for job_id, next_position in enumerate(self._job_next_operation_index):
|
344
341
|
if next_position == len(self.instance.jobs[job_id]):
|
@@ -346,3 +343,174 @@ class Dispatcher:
|
|
346
343
|
operation = self.instance.jobs[job_id][next_position]
|
347
344
|
available_operations.append(operation)
|
348
345
|
return available_operations
|
346
|
+
|
347
|
+
@_dispatcher_cache
|
348
|
+
def unscheduled_operations(self) -> list[Operation]:
|
349
|
+
"""Returns the list of operations that have not been scheduled."""
|
350
|
+
unscheduled_operations = []
|
351
|
+
for job_id, next_position in enumerate(self._job_next_operation_index):
|
352
|
+
operations = self.instance.jobs[job_id][next_position:]
|
353
|
+
unscheduled_operations.extend(operations)
|
354
|
+
return unscheduled_operations
|
355
|
+
|
356
|
+
@_dispatcher_cache
|
357
|
+
def scheduled_operations(self) -> list[Operation]:
|
358
|
+
"""Returns the list of operations that have been scheduled."""
|
359
|
+
scheduled_operations = []
|
360
|
+
for job_id, next_position in enumerate(self._job_next_operation_index):
|
361
|
+
operations = self.instance.jobs[job_id][:next_position]
|
362
|
+
scheduled_operations.extend(operations)
|
363
|
+
return scheduled_operations
|
364
|
+
|
365
|
+
@_dispatcher_cache
|
366
|
+
def available_machines(self) -> list[int]:
|
367
|
+
"""Returns the list of available machines."""
|
368
|
+
available_operations = self.available_operations()
|
369
|
+
available_machines = set()
|
370
|
+
for operation in available_operations:
|
371
|
+
available_machines.update(operation.machines)
|
372
|
+
return list(available_machines)
|
373
|
+
|
374
|
+
@_dispatcher_cache
|
375
|
+
def available_jobs(self) -> list[int]:
|
376
|
+
"""Returns the list of available jobs."""
|
377
|
+
available_operations = self.available_operations()
|
378
|
+
available_jobs = set(
|
379
|
+
operation.job_id for operation in available_operations
|
380
|
+
)
|
381
|
+
return list(available_jobs)
|
382
|
+
|
383
|
+
def earliest_start_time(self, operation: Operation) -> int:
|
384
|
+
"""Calculates the earliest start time for a given operation based on
|
385
|
+
machine and job constraints.
|
386
|
+
|
387
|
+
This method is different from the `start_time` method in that it
|
388
|
+
takes into account every machine that can process the operation, not
|
389
|
+
just the one that will process it. However, it also assumes that
|
390
|
+
the operation is ready to be scheduled in the job in favor of
|
391
|
+
performance.
|
392
|
+
|
393
|
+
Args:
|
394
|
+
operation:
|
395
|
+
The operation for which to calculate the earliest start time.
|
396
|
+
|
397
|
+
Returns:
|
398
|
+
The earliest start time for the operation.
|
399
|
+
"""
|
400
|
+
machine_earliest_start_time = min(
|
401
|
+
self._machine_next_available_time[machine_id]
|
402
|
+
for machine_id in operation.machines
|
403
|
+
)
|
404
|
+
job_start_time = self._job_next_available_time[operation.job_id]
|
405
|
+
return max(machine_earliest_start_time, job_start_time)
|
406
|
+
|
407
|
+
def remaining_duration(
|
408
|
+
self, scheduled_operation: ScheduledOperation
|
409
|
+
) -> int:
|
410
|
+
"""Calculates the remaining duration of a scheduled operation.
|
411
|
+
|
412
|
+
The method computes the remaining time for an operation to finish,
|
413
|
+
based on the maximum of the operation's start time or the current time.
|
414
|
+
This helps in determining how much time is left from 'now' until the
|
415
|
+
operation is completed.
|
416
|
+
|
417
|
+
Args:
|
418
|
+
scheduled_operation:
|
419
|
+
The operation for which to calculate the remaining time.
|
420
|
+
|
421
|
+
Returns:
|
422
|
+
The remaining duration.
|
423
|
+
"""
|
424
|
+
adjusted_start_time = max(
|
425
|
+
scheduled_operation.start_time, self.current_time()
|
426
|
+
)
|
427
|
+
return scheduled_operation.end_time - adjusted_start_time
|
428
|
+
|
429
|
+
@_dispatcher_cache
|
430
|
+
def completed_operations(self) -> set[Operation]:
|
431
|
+
"""Returns the set of operations that have been completed.
|
432
|
+
|
433
|
+
This method returns the operations that have been scheduled and the
|
434
|
+
current time is greater than or equal to the end time of the operation.
|
435
|
+
"""
|
436
|
+
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
|
+
)
|
443
|
+
completed_operations = scheduled_operations - ongoing_operations
|
444
|
+
return completed_operations
|
445
|
+
|
446
|
+
@_dispatcher_cache
|
447
|
+
def uncompleted_operations(self) -> list[Operation]:
|
448
|
+
"""Returns the list of operations that have not been completed yet.
|
449
|
+
|
450
|
+
This method checks for operations that either haven't been scheduled
|
451
|
+
or have been scheduled but haven't reached their completion time.
|
452
|
+
|
453
|
+
Note:
|
454
|
+
The behavior of this method changed in version 0.5.0. Previously, it
|
455
|
+
only returned unscheduled operations. For the old behavior, use the
|
456
|
+
`unscheduled_operations` method.
|
457
|
+
"""
|
458
|
+
uncompleted_operations = self.unscheduled_operations()
|
459
|
+
uncompleted_operations.extend(
|
460
|
+
scheduled_operation.operation
|
461
|
+
for scheduled_operation in self.ongoing_operations()
|
462
|
+
)
|
463
|
+
return uncompleted_operations
|
464
|
+
|
465
|
+
@_dispatcher_cache
|
466
|
+
def ongoing_operations(self) -> list[ScheduledOperation]:
|
467
|
+
"""Returns the list of operations that are currently being processed.
|
468
|
+
|
469
|
+
This method returns the operations that have been scheduled and are
|
470
|
+
currently being processed by the machines.
|
471
|
+
"""
|
472
|
+
current_time = self.current_time()
|
473
|
+
ongoing_operations = []
|
474
|
+
for machine_schedule in self.schedule.schedule:
|
475
|
+
for scheduled_operation in reversed(machine_schedule):
|
476
|
+
is_completed = scheduled_operation.end_time <= current_time
|
477
|
+
if is_completed:
|
478
|
+
break
|
479
|
+
ongoing_operations.append(scheduled_operation)
|
480
|
+
return ongoing_operations
|
481
|
+
|
482
|
+
def is_scheduled(self, operation: Operation) -> bool:
|
483
|
+
"""Checks if the given operation has been scheduled."""
|
484
|
+
job_next_op_idx = self._job_next_operation_index[operation.job_id]
|
485
|
+
return operation.position_in_job < job_next_op_idx
|
486
|
+
|
487
|
+
def is_ongoing(self, scheduled_operation: ScheduledOperation) -> bool:
|
488
|
+
"""Checks if the given operation is currently being processed."""
|
489
|
+
current_time = self.current_time()
|
490
|
+
return scheduled_operation.start_time <= current_time
|
491
|
+
|
492
|
+
@classmethod
|
493
|
+
def create_schedule_from_raw_solution(
|
494
|
+
cls, instance: JobShopInstance, raw_solution: list[list[Operation]]
|
495
|
+
) -> Schedule:
|
496
|
+
"""Deprecated method, use `Schedule.from_job_sequences` instead."""
|
497
|
+
warn(
|
498
|
+
"Dispatcher.create_schedule_from_raw_solution is deprecated. "
|
499
|
+
"Use Schedule.from_job_sequences instead. It will be removed in "
|
500
|
+
"version 1.0.0.",
|
501
|
+
DeprecationWarning,
|
502
|
+
)
|
503
|
+
dispatcher = cls(instance)
|
504
|
+
dispatcher.reset()
|
505
|
+
raw_solution_deques = [
|
506
|
+
deque(operations) for operations in raw_solution
|
507
|
+
]
|
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,28 @@
|
|
1
|
+
"""Contains FeatureObserver classes for observing features of the
|
2
|
+
dispatcher."""
|
3
|
+
|
4
|
+
from .feature_observer import FeatureObserver, FeatureType
|
5
|
+
from .composite_feature_observer import CompositeFeatureObserver
|
6
|
+
from .earliest_start_time_observer import EarliestStartTimeObserver
|
7
|
+
from .is_ready_observer import IsReadyObserver
|
8
|
+
from .duration_observer import DurationObserver
|
9
|
+
from .is_scheduled_observer import IsScheduledObserver
|
10
|
+
from .position_in_job_observer import PositionInJobObserver
|
11
|
+
from .remaining_operations_observer import RemainingOperationsObserver
|
12
|
+
from .is_completed_observer import IsCompletedObserver
|
13
|
+
from .factory import FeatureObserverType, feature_observer_factory
|
14
|
+
|
15
|
+
__all__ = [
|
16
|
+
"FeatureObserver",
|
17
|
+
"FeatureType",
|
18
|
+
"CompositeFeatureObserver",
|
19
|
+
"EarliestStartTimeObserver",
|
20
|
+
"IsReadyObserver",
|
21
|
+
"DurationObserver",
|
22
|
+
"IsScheduledObserver",
|
23
|
+
"PositionInJobObserver",
|
24
|
+
"RemainingOperationsObserver",
|
25
|
+
"IsCompletedObserver",
|
26
|
+
"FeatureObserverType",
|
27
|
+
"feature_observer_factory",
|
28
|
+
]
|
@@ -0,0 +1,87 @@
|
|
1
|
+
"""Home of the `CompositeFeatureObserver` class."""
|
2
|
+
|
3
|
+
from collections import defaultdict
|
4
|
+
import numpy as np
|
5
|
+
import pandas as pd
|
6
|
+
|
7
|
+
from job_shop_lib.dispatching import Dispatcher
|
8
|
+
from job_shop_lib.dispatching.feature_observers import (
|
9
|
+
FeatureObserver,
|
10
|
+
FeatureType,
|
11
|
+
)
|
12
|
+
|
13
|
+
|
14
|
+
class CompositeFeatureObserver(FeatureObserver):
|
15
|
+
"""Aggregates features from other FeatureObserver instances subscribed to
|
16
|
+
the same `Dispatcher` by concatenating their feature matrices along the
|
17
|
+
first axis (horizontal concatenation).
|
18
|
+
|
19
|
+
Attributes:
|
20
|
+
feature_observers:
|
21
|
+
List of `FeatureObserver` instances to aggregate features from.
|
22
|
+
column_names:
|
23
|
+
Dictionary mapping `FeatureType` to a list of column names for the
|
24
|
+
corresponding feature matrix. Column names are generated based on
|
25
|
+
the class name of the `FeatureObserver` instance that produced the
|
26
|
+
feature.
|
27
|
+
"""
|
28
|
+
|
29
|
+
def __init__(
|
30
|
+
self,
|
31
|
+
dispatcher: Dispatcher,
|
32
|
+
feature_observers: list[FeatureObserver] | None = None,
|
33
|
+
subscribe: bool = True,
|
34
|
+
):
|
35
|
+
if feature_observers is None:
|
36
|
+
feature_observers = [
|
37
|
+
observer
|
38
|
+
for observer in dispatcher.subscribers
|
39
|
+
if isinstance(observer, FeatureObserver)
|
40
|
+
]
|
41
|
+
self.feature_observers = feature_observers
|
42
|
+
self.column_names: dict[FeatureType, list[str]] = defaultdict(list)
|
43
|
+
super().__init__(dispatcher, subscribe=subscribe)
|
44
|
+
self._set_column_names()
|
45
|
+
|
46
|
+
@property
|
47
|
+
def features_as_dataframe(self) -> dict[FeatureType, pd.DataFrame]:
|
48
|
+
"""Returns the features as a dictionary of `pd.DataFrame` instances."""
|
49
|
+
return {
|
50
|
+
feature_type: pd.DataFrame(
|
51
|
+
feature_matrix, columns=self.column_names[feature_type]
|
52
|
+
)
|
53
|
+
for feature_type, feature_matrix in self.features.items()
|
54
|
+
}
|
55
|
+
|
56
|
+
def initialize_features(self):
|
57
|
+
features: dict[FeatureType, list[np.ndarray]] = defaultdict(list)
|
58
|
+
for observer in self.feature_observers:
|
59
|
+
for feature_type, feature_matrix in observer.features.items():
|
60
|
+
features[feature_type].append(feature_matrix)
|
61
|
+
|
62
|
+
self.features = {
|
63
|
+
feature_type: np.concatenate(features, axis=1)
|
64
|
+
for feature_type, features in features.items()
|
65
|
+
}
|
66
|
+
|
67
|
+
def _set_column_names(self):
|
68
|
+
for observer in self.feature_observers:
|
69
|
+
for feature_type, feature_matrix in observer.features.items():
|
70
|
+
feature_name = observer.__class__.__name__.replace(
|
71
|
+
"Observer", ""
|
72
|
+
)
|
73
|
+
if feature_matrix.shape[1] > 1:
|
74
|
+
self.column_names[feature_type] += [
|
75
|
+
f"{feature_name}_{i}"
|
76
|
+
for i in range(feature_matrix.shape[1])
|
77
|
+
]
|
78
|
+
else:
|
79
|
+
self.column_names[feature_type].append(feature_name)
|
80
|
+
|
81
|
+
def __str__(self):
|
82
|
+
out = [f"{self.__class__.__name__}:"]
|
83
|
+
out.append("-" * (len(out[0]) - 1))
|
84
|
+
for feature_type, dataframe in self.features_as_dataframe.items():
|
85
|
+
out.append(f"{feature_type.value}:")
|
86
|
+
out.append(dataframe.to_string())
|
87
|
+
return "\n".join(out)
|
@@ -0,0 +1,95 @@
|
|
1
|
+
"""Home of the `DurationObserver` class."""
|
2
|
+
|
3
|
+
import numpy as np
|
4
|
+
|
5
|
+
from job_shop_lib.dispatching import Dispatcher
|
6
|
+
from job_shop_lib import ScheduledOperation
|
7
|
+
from job_shop_lib.dispatching.feature_observers import (
|
8
|
+
FeatureObserver,
|
9
|
+
FeatureType,
|
10
|
+
)
|
11
|
+
|
12
|
+
|
13
|
+
class DurationObserver(FeatureObserver):
|
14
|
+
"""Measures the remaining duration of operations, machines, and jobs.
|
15
|
+
|
16
|
+
The duration of an Operation is:
|
17
|
+
- if the operation has not been scheduled, it is the duration of the
|
18
|
+
operation.
|
19
|
+
- if the operation has been scheduled, it is the remaining duration of
|
20
|
+
the operation.
|
21
|
+
- if the operation has been completed, it is the last duration of the
|
22
|
+
operation that has been computed. The duration must be set to 0
|
23
|
+
manually if needed. We do not update the duration of completed
|
24
|
+
operations to save computation time.
|
25
|
+
|
26
|
+
The duration of a Machine or Job is the sum of the durations of the
|
27
|
+
unscheduled operations that belong to the machine or job.
|
28
|
+
"""
|
29
|
+
|
30
|
+
def __init__(
|
31
|
+
self,
|
32
|
+
dispatcher: Dispatcher,
|
33
|
+
feature_types: list[FeatureType] | FeatureType | None = None,
|
34
|
+
subscribe: bool = True,
|
35
|
+
):
|
36
|
+
super().__init__(
|
37
|
+
dispatcher, feature_types, feature_size=1, subscribe=subscribe
|
38
|
+
)
|
39
|
+
|
40
|
+
def initialize_features(self):
|
41
|
+
mapping = {
|
42
|
+
FeatureType.OPERATIONS: self._initialize_operation_durations,
|
43
|
+
FeatureType.MACHINES: self._initialize_machine_durations,
|
44
|
+
FeatureType.JOBS: self._initialize_job_durations,
|
45
|
+
}
|
46
|
+
for feature_type in self.features:
|
47
|
+
mapping[feature_type]()
|
48
|
+
|
49
|
+
def update(self, scheduled_operation: ScheduledOperation):
|
50
|
+
mapping = {
|
51
|
+
FeatureType.OPERATIONS: self._update_operation_durations,
|
52
|
+
FeatureType.MACHINES: self._update_machine_durations,
|
53
|
+
FeatureType.JOBS: self._update_job_durations,
|
54
|
+
}
|
55
|
+
for feature_type in self.features:
|
56
|
+
mapping[feature_type](scheduled_operation)
|
57
|
+
|
58
|
+
def _initialize_operation_durations(self):
|
59
|
+
duration_matrix = self.dispatcher.instance.durations_matrix_array
|
60
|
+
operation_durations = np.array(duration_matrix).reshape(-1, 1)
|
61
|
+
# Drop the NaN values
|
62
|
+
operation_durations = operation_durations[
|
63
|
+
~np.isnan(operation_durations)
|
64
|
+
].reshape(-1, 1)
|
65
|
+
self.features[FeatureType.OPERATIONS] = operation_durations
|
66
|
+
|
67
|
+
def _initialize_machine_durations(self):
|
68
|
+
machine_durations = self.dispatcher.instance.machine_loads
|
69
|
+
for machine_id, machine_load in enumerate(machine_durations):
|
70
|
+
self.features[FeatureType.MACHINES][machine_id, 0] = machine_load
|
71
|
+
|
72
|
+
def _initialize_job_durations(self):
|
73
|
+
job_durations = self.dispatcher.instance.job_durations
|
74
|
+
for job_id, job_duration in enumerate(job_durations):
|
75
|
+
self.features[FeatureType.JOBS][job_id, 0] = job_duration
|
76
|
+
|
77
|
+
def _update_operation_durations(
|
78
|
+
self, scheduled_operation: ScheduledOperation
|
79
|
+
):
|
80
|
+
operation_id = scheduled_operation.operation.operation_id
|
81
|
+
self.features[FeatureType.OPERATIONS][operation_id, 0] = (
|
82
|
+
self.dispatcher.remaining_duration(scheduled_operation)
|
83
|
+
)
|
84
|
+
|
85
|
+
def _update_machine_durations(
|
86
|
+
self, scheduled_operation: ScheduledOperation
|
87
|
+
):
|
88
|
+
machine_id = scheduled_operation.machine_id
|
89
|
+
op_duration = scheduled_operation.operation.duration
|
90
|
+
self.features[FeatureType.MACHINES][machine_id, 0] -= op_duration
|
91
|
+
|
92
|
+
def _update_job_durations(self, scheduled_operation: ScheduledOperation):
|
93
|
+
operation_duration = scheduled_operation.operation.duration
|
94
|
+
job_id = scheduled_operation.job_id
|
95
|
+
self.features[FeatureType.JOBS][job_id, 0] -= operation_duration
|