job-shop-lib 0.3.0__py3-none-any.whl → 0.5.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/dispatching/dispatcher.py +220 -59
- job_shop_lib/dispatching/feature_observers/__init__.py +28 -0
- job_shop_lib/dispatching/feature_observers/composite_feature_observer.py +87 -0
- job_shop_lib/dispatching/feature_observers/duration_observer.py +95 -0
- job_shop_lib/dispatching/feature_observers/earliest_start_time_observer.py +156 -0
- job_shop_lib/dispatching/feature_observers/factory.py +58 -0
- job_shop_lib/dispatching/feature_observers/feature_observer.py +113 -0
- job_shop_lib/dispatching/feature_observers/is_completed_observer.py +98 -0
- job_shop_lib/dispatching/feature_observers/is_ready_observer.py +40 -0
- job_shop_lib/dispatching/feature_observers/is_scheduled_observer.py +34 -0
- job_shop_lib/dispatching/feature_observers/position_in_job_observer.py +39 -0
- job_shop_lib/dispatching/feature_observers/remaining_operations_observer.py +54 -0
- job_shop_lib/graphs/build_disjunctive_graph.py +20 -0
- job_shop_lib/job_shop_instance.py +101 -0
- job_shop_lib/operation.py +4 -4
- job_shop_lib/schedule.py +96 -1
- job_shop_lib/scheduled_operation.py +17 -5
- job_shop_lib/visualization/create_gif.py +47 -38
- job_shop_lib/visualization/gantt_chart.py +1 -1
- {job_shop_lib-0.3.0.dist-info → job_shop_lib-0.5.0.dist-info}/METADATA +9 -5
- {job_shop_lib-0.3.0.dist-info → job_shop_lib-0.5.0.dist-info}/RECORD +23 -12
- {job_shop_lib-0.3.0.dist-info → job_shop_lib-0.5.0.dist-info}/LICENSE +0 -0
- {job_shop_lib-0.3.0.dist-info → job_shop_lib-0.5.0.dist-info}/WHEEL +0 -0
@@ -7,6 +7,7 @@ from typing import Any
|
|
7
7
|
from collections.abc import Callable
|
8
8
|
from collections import deque
|
9
9
|
from functools import wraps
|
10
|
+
from warnings import warn
|
10
11
|
|
11
12
|
from job_shop_lib import (
|
12
13
|
JobShopInstance,
|
@@ -18,13 +19,45 @@ from job_shop_lib import (
|
|
18
19
|
|
19
20
|
# Added here to avoid circular imports
|
20
21
|
class DispatcherObserver(abc.ABC):
|
21
|
-
"""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
|
+
)
|
22
57
|
|
23
|
-
def __init__(self, dispatcher: Dispatcher):
|
24
|
-
"""Initializes the observer with the dispatcher and subscribes to
|
25
|
-
it."""
|
26
58
|
self.dispatcher = dispatcher
|
27
|
-
|
59
|
+
if subscribe:
|
60
|
+
self.dispatcher.subscribe(self)
|
28
61
|
|
29
62
|
@abc.abstractmethod
|
30
63
|
def update(self, scheduled_operation: ScheduledOperation):
|
@@ -38,7 +71,7 @@ class DispatcherObserver(abc.ABC):
|
|
38
71
|
return self.__class__.__name__
|
39
72
|
|
40
73
|
def __repr__(self) -> str:
|
41
|
-
return
|
74
|
+
return self.__class__.__name__
|
42
75
|
|
43
76
|
|
44
77
|
def _dispatcher_cache(method):
|
@@ -85,8 +118,6 @@ class Dispatcher:
|
|
85
118
|
pruning_function:
|
86
119
|
A function that filters out operations that are not ready to be
|
87
120
|
scheduled.
|
88
|
-
subscribers:
|
89
|
-
A list of observers that are subscribed to the dispatcher.
|
90
121
|
"""
|
91
122
|
|
92
123
|
__slots__ = (
|
@@ -151,40 +182,6 @@ class Dispatcher:
|
|
151
182
|
"""Returns the next available time for each job."""
|
152
183
|
return self._job_next_available_time
|
153
184
|
|
154
|
-
@classmethod
|
155
|
-
def create_schedule_from_raw_solution(
|
156
|
-
cls, instance: JobShopInstance, raw_solution: list[list[Operation]]
|
157
|
-
) -> Schedule:
|
158
|
-
"""Creates a schedule from a raw solution.
|
159
|
-
|
160
|
-
A raw solution is a list of lists of operations, where each list
|
161
|
-
represents the order of operations for a machine.
|
162
|
-
|
163
|
-
Args:
|
164
|
-
instance:
|
165
|
-
The instance of the job shop problem to be solved.
|
166
|
-
raw_solution:
|
167
|
-
A list of lists of operations, where each list represents the
|
168
|
-
order of operations for a machine.
|
169
|
-
|
170
|
-
Returns:
|
171
|
-
A Schedule object representing the solution.
|
172
|
-
"""
|
173
|
-
dispatcher = cls(instance)
|
174
|
-
dispatcher.reset()
|
175
|
-
raw_solution_deques = [
|
176
|
-
deque(operations) for operations in raw_solution
|
177
|
-
]
|
178
|
-
while not dispatcher.schedule.is_complete():
|
179
|
-
for machine_id, operations in enumerate(raw_solution_deques):
|
180
|
-
if not operations:
|
181
|
-
continue
|
182
|
-
operation = operations[0]
|
183
|
-
if dispatcher.is_operation_ready(operation):
|
184
|
-
dispatcher.dispatch(operation, machine_id)
|
185
|
-
operations.popleft()
|
186
|
-
return dispatcher.schedule
|
187
|
-
|
188
185
|
def subscribe(self, observer: DispatcherObserver):
|
189
186
|
"""Subscribes an observer to the dispatcher."""
|
190
187
|
self.subscribers.append(observer)
|
@@ -311,20 +308,6 @@ class Dispatcher:
|
|
311
308
|
min_start_time = min(min_start_time, start_time)
|
312
309
|
return int(min_start_time)
|
313
310
|
|
314
|
-
@_dispatcher_cache
|
315
|
-
def uncompleted_operations(self) -> list[Operation]:
|
316
|
-
"""Returns the list of operations that have not been scheduled.
|
317
|
-
|
318
|
-
An operation is uncompleted if it has not been scheduled yet.
|
319
|
-
|
320
|
-
It is more efficient than checking all operations in the instance.
|
321
|
-
"""
|
322
|
-
uncompleted_operations = []
|
323
|
-
for job_id, next_position in enumerate(self._job_next_operation_index):
|
324
|
-
operations = self.instance.jobs[job_id][next_position:]
|
325
|
-
uncompleted_operations.extend(operations)
|
326
|
-
return uncompleted_operations
|
327
|
-
|
328
311
|
@_dispatcher_cache
|
329
312
|
def available_operations(self) -> list[Operation]:
|
330
313
|
"""Returns a list of available operations for processing, optionally
|
@@ -337,15 +320,22 @@ class Dispatcher:
|
|
337
320
|
Returns:
|
338
321
|
A list of Operation objects that are available for scheduling.
|
339
322
|
"""
|
340
|
-
|
341
|
-
available_operations = self._available_operations()
|
323
|
+
available_operations = self.available_operations_without_pruning()
|
342
324
|
if self.pruning_function is not None:
|
343
325
|
available_operations = self.pruning_function(
|
344
326
|
self, available_operations
|
345
327
|
)
|
346
328
|
return available_operations
|
347
329
|
|
348
|
-
|
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
|
+
"""
|
349
339
|
available_operations = []
|
350
340
|
for job_id, next_position in enumerate(self._job_next_operation_index):
|
351
341
|
if next_position == len(self.instance.jobs[job_id]):
|
@@ -353,3 +343,174 @@ class Dispatcher:
|
|
353
343
|
operation = self.instance.jobs[job_id][next_position]
|
354
344
|
available_operations.append(operation)
|
355
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
|