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
@@ -0,0 +1,212 @@
|
|
1
|
+
"""Home of the `CompositeFeatureObserver` class."""
|
2
|
+
|
3
|
+
from collections import defaultdict
|
4
|
+
from collections.abc import Sequence
|
5
|
+
from typing import List, Dict, Union, Optional, Type
|
6
|
+
# The Self type can be imported directly from Python’s typing module in
|
7
|
+
# version 3.11 and beyond. We use the typing_extensions module to support
|
8
|
+
# python >=3.8
|
9
|
+
from typing_extensions import Self
|
10
|
+
import numpy as np
|
11
|
+
from numpy.typing import NDArray
|
12
|
+
import pandas as pd
|
13
|
+
|
14
|
+
from job_shop_lib.exceptions import ValidationError
|
15
|
+
from job_shop_lib.dispatching import Dispatcher
|
16
|
+
from job_shop_lib.dispatching.feature_observers import (
|
17
|
+
FeatureObserver,
|
18
|
+
FeatureType,
|
19
|
+
FeatureObserverConfig,
|
20
|
+
feature_observer_factory,
|
21
|
+
FeatureObserverType,
|
22
|
+
)
|
23
|
+
|
24
|
+
|
25
|
+
class CompositeFeatureObserver(FeatureObserver):
|
26
|
+
"""Aggregates features from other FeatureObserver instances subscribed to
|
27
|
+
the same :class:`~job_shop_lib.dispatching.Dispatcher` by concatenating
|
28
|
+
their feature matrices along the first axis (horizontal concatenation).
|
29
|
+
|
30
|
+
It provides also a custom ``__str__`` method to display the features
|
31
|
+
in a more readable way.
|
32
|
+
|
33
|
+
Args:
|
34
|
+
dispatcher:
|
35
|
+
The :class:`~job_shop_lib.dispatching.Dispatcher` to observe.
|
36
|
+
subscribe:
|
37
|
+
If ``True``, the observer is subscribed to the dispatcher upon
|
38
|
+
initialization. Otherwise, the observer must be subscribed later
|
39
|
+
or manually updated.
|
40
|
+
feature_types:
|
41
|
+
A list of :class:`FeatureType` or a single :class:`FeatureType`
|
42
|
+
that specifies the types of features to observe. They must be a
|
43
|
+
subset of the class attribute :attr:`supported_feature_types`.
|
44
|
+
If ``None``, all supported feature types are tracked.
|
45
|
+
feature_observers:
|
46
|
+
A list of `FeatureObserver` instances to aggregate features from.
|
47
|
+
If ``None``, all feature observers subscribed to the dispatcher are
|
48
|
+
used.
|
49
|
+
|
50
|
+
.. seealso::
|
51
|
+
|
52
|
+
An example using this class can be found in the
|
53
|
+
:doc:`../examples/08-Feature-Observers` example.
|
54
|
+
|
55
|
+
Additionally, the class
|
56
|
+
:class:`~job_shop_lib.reinforcement_learning.SingleJobShopGraphEnv`
|
57
|
+
uses this feature observer to aggregate features from multiple
|
58
|
+
ones.
|
59
|
+
|
60
|
+
"""
|
61
|
+
|
62
|
+
__slots__ = {
|
63
|
+
"feature_observers": (
|
64
|
+
"List of :class:`FeatureObserver` instances to aggregate features "
|
65
|
+
"from."
|
66
|
+
),
|
67
|
+
"column_names": (
|
68
|
+
"Dictionary mapping :class:`FeatureType` to a list of column "
|
69
|
+
"names for the corresponding feature matrix. They are generated "
|
70
|
+
"based on the class name of the :class:`FeatureObserver` instance "
|
71
|
+
"that produced the feature."
|
72
|
+
),
|
73
|
+
}
|
74
|
+
|
75
|
+
def __init__(
|
76
|
+
self,
|
77
|
+
dispatcher: Dispatcher,
|
78
|
+
*,
|
79
|
+
subscribe: bool = True,
|
80
|
+
feature_types: Optional[Union[List[FeatureType], FeatureType]] = None,
|
81
|
+
feature_observers: Optional[List[FeatureObserver]] = None,
|
82
|
+
):
|
83
|
+
if feature_observers is None:
|
84
|
+
feature_observers = [
|
85
|
+
observer
|
86
|
+
for observer in dispatcher.subscribers
|
87
|
+
if isinstance(observer, FeatureObserver)
|
88
|
+
]
|
89
|
+
feature_types = self._get_feature_types_list(feature_types)
|
90
|
+
for observer in feature_observers:
|
91
|
+
if not set(observer.features.keys()).issubset(set(feature_types)):
|
92
|
+
raise ValidationError(
|
93
|
+
"The feature types observed by the feature observer "
|
94
|
+
f"{observer.__class__.__name__} are not a subset of the "
|
95
|
+
"feature types specified in the CompositeFeatureObserver."
|
96
|
+
f"Observer feature types: {observer.features.keys()}"
|
97
|
+
f"Composite feature types: {feature_types}"
|
98
|
+
)
|
99
|
+
self.feature_observers = feature_observers
|
100
|
+
self.column_names: Dict[FeatureType, List[str]] = defaultdict(list)
|
101
|
+
super().__init__(dispatcher, subscribe=subscribe)
|
102
|
+
self._set_column_names()
|
103
|
+
|
104
|
+
@classmethod
|
105
|
+
def from_feature_observer_configs(
|
106
|
+
cls,
|
107
|
+
dispatcher: Dispatcher,
|
108
|
+
feature_observer_configs: Sequence[
|
109
|
+
Union[
|
110
|
+
str,
|
111
|
+
FeatureObserverType,
|
112
|
+
Type[FeatureObserver],
|
113
|
+
FeatureObserverConfig,
|
114
|
+
],
|
115
|
+
],
|
116
|
+
subscribe: bool = True,
|
117
|
+
) -> Self:
|
118
|
+
"""Creates the composite feature observer.
|
119
|
+
|
120
|
+
Args:
|
121
|
+
dispatcher:
|
122
|
+
The dispatcher used to create the feature observers.
|
123
|
+
feature_observer_configs:
|
124
|
+
The list of feature observer configuration objects.
|
125
|
+
subscribe:
|
126
|
+
Whether to subscribe the CompositeFeatureObserver to the
|
127
|
+
dispatcher.
|
128
|
+
"""
|
129
|
+
observers = [
|
130
|
+
feature_observer_factory(observer_config, dispatcher=dispatcher)
|
131
|
+
for observer_config in feature_observer_configs
|
132
|
+
]
|
133
|
+
composite_observer = cls(
|
134
|
+
dispatcher, feature_observers=observers, subscribe=subscribe
|
135
|
+
)
|
136
|
+
return composite_observer
|
137
|
+
|
138
|
+
@property
|
139
|
+
def features_as_dataframe(self) -> Dict[FeatureType, pd.DataFrame]:
|
140
|
+
"""Returns the features as a dictionary of `pd.DataFrame` instances."""
|
141
|
+
return {
|
142
|
+
feature_type: pd.DataFrame(
|
143
|
+
feature_matrix, columns=self.column_names[feature_type]
|
144
|
+
)
|
145
|
+
for feature_type, feature_matrix in self.features.items()
|
146
|
+
}
|
147
|
+
|
148
|
+
def initialize_features(self):
|
149
|
+
features: Dict[FeatureType, List[NDArray[np.float32]]] = defaultdict(
|
150
|
+
list
|
151
|
+
)
|
152
|
+
for observer in self.feature_observers:
|
153
|
+
for feature_type, feature_matrix in observer.features.items():
|
154
|
+
features[feature_type].append(feature_matrix)
|
155
|
+
|
156
|
+
self.features = {
|
157
|
+
feature_type: np.concatenate(features, axis=1)
|
158
|
+
for feature_type, features in features.items()
|
159
|
+
}
|
160
|
+
|
161
|
+
def _set_column_names(self):
|
162
|
+
for observer in self.feature_observers:
|
163
|
+
for feature_type, feature_matrix in observer.features.items():
|
164
|
+
feature_name = observer.__class__.__name__.replace(
|
165
|
+
"Observer", ""
|
166
|
+
)
|
167
|
+
if feature_matrix.shape[1] > 1:
|
168
|
+
self.column_names[feature_type] += [
|
169
|
+
f"{feature_name}_{i}"
|
170
|
+
for i in range(feature_matrix.shape[1])
|
171
|
+
]
|
172
|
+
else:
|
173
|
+
self.column_names[feature_type].append(feature_name)
|
174
|
+
|
175
|
+
def __str__(self):
|
176
|
+
out = [f"{self.__class__.__name__}:"]
|
177
|
+
out.append("-" * (len(out[0]) - 1))
|
178
|
+
for feature_type, dataframe in self.features_as_dataframe.items():
|
179
|
+
out.append(f"{feature_type.value}:")
|
180
|
+
out.append(dataframe.to_string())
|
181
|
+
return "\n".join(out)
|
182
|
+
|
183
|
+
|
184
|
+
if __name__ == "__main__":
|
185
|
+
# from cProfile import Profile
|
186
|
+
import time
|
187
|
+
from job_shop_lib.benchmarking import load_benchmark_instance
|
188
|
+
from job_shop_lib.dispatching.rules import DispatchingRuleSolver
|
189
|
+
|
190
|
+
ta80 = load_benchmark_instance("ta80")
|
191
|
+
|
192
|
+
dispatcher_ = Dispatcher(ta80)
|
193
|
+
feature_observer_types_ = list(FeatureObserverType)
|
194
|
+
feature_observers_ = [
|
195
|
+
feature_observer_factory(
|
196
|
+
observer_type,
|
197
|
+
dispatcher=dispatcher_,
|
198
|
+
)
|
199
|
+
for observer_type in feature_observer_types_
|
200
|
+
# and not FeatureObserverType.EARLIEST_START_TIME
|
201
|
+
]
|
202
|
+
composite_observer_ = CompositeFeatureObserver(
|
203
|
+
dispatcher_, feature_observers=feature_observers_
|
204
|
+
)
|
205
|
+
solver = DispatchingRuleSolver(dispatching_rule="random")
|
206
|
+
# profiler = Profile()
|
207
|
+
# profiler.runcall(solver.solve, dispatcher_.instance, dispatcher_)
|
208
|
+
# profiler.print_stats("cumtime")
|
209
|
+
start = time.perf_counter()
|
210
|
+
solver.solve(dispatcher_.instance, dispatcher_)
|
211
|
+
end = time.perf_counter()
|
212
|
+
print(f"Time: {end - start}")
|
@@ -2,7 +2,6 @@
|
|
2
2
|
|
3
3
|
import numpy as np
|
4
4
|
|
5
|
-
from job_shop_lib.dispatching import Dispatcher
|
6
5
|
from job_shop_lib import ScheduledOperation
|
7
6
|
from job_shop_lib.dispatching.feature_observers import (
|
8
7
|
FeatureObserver,
|
@@ -13,29 +12,32 @@ from job_shop_lib.dispatching.feature_observers import (
|
|
13
12
|
class DurationObserver(FeatureObserver):
|
14
13
|
"""Measures the remaining duration of operations, machines, and jobs.
|
15
14
|
|
16
|
-
The duration of an Operation is:
|
15
|
+
The duration of an :class:`~job_shop_lib.Operation` is:
|
17
16
|
- if the operation has not been scheduled, it is the duration of the
|
18
|
-
|
17
|
+
operation.
|
19
18
|
- if the operation has been scheduled, it is the remaining duration of
|
20
|
-
|
19
|
+
the operation.
|
21
20
|
- if the operation has been completed, it is the last duration of the
|
22
|
-
|
23
|
-
|
24
|
-
|
21
|
+
operation that has been computed. The duration must be set to 0
|
22
|
+
manually if needed. We do not update the duration of completed
|
23
|
+
operations to save computation time.
|
25
24
|
|
26
|
-
The duration of a
|
25
|
+
The duration of a machine or job is the sum of the durations of the
|
27
26
|
unscheduled operations that belong to the machine or job.
|
28
|
-
"""
|
29
27
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
28
|
+
Args:
|
29
|
+
dispatcher:
|
30
|
+
The :class:`~job_shop_lib.dispatching.Dispatcher` to observe.
|
31
|
+
subscribe:
|
32
|
+
If ``True``, the observer is subscribed to the dispatcher upon
|
33
|
+
initialization. Otherwise, the observer must be subscribed later
|
34
|
+
or manually updated.
|
35
|
+
feature_types:
|
36
|
+
A list of :class:`FeatureType` or a single :class:`FeatureType`
|
37
|
+
that specifies the types of features to observe. They must be a
|
38
|
+
subset of the class attribute :attr:`supported_feature_types`.
|
39
|
+
If ``None``, all supported feature types are tracked.
|
40
|
+
"""
|
39
41
|
|
40
42
|
def initialize_features(self):
|
41
43
|
mapping = {
|
@@ -0,0 +1,289 @@
|
|
1
|
+
"""Home of the `EarliestStartTimeObserver` class."""
|
2
|
+
|
3
|
+
from typing import List, Optional, Union
|
4
|
+
|
5
|
+
import numpy as np
|
6
|
+
from numpy.typing import NDArray
|
7
|
+
|
8
|
+
from job_shop_lib.dispatching import Dispatcher
|
9
|
+
from job_shop_lib.dispatching.feature_observers import (
|
10
|
+
FeatureObserver,
|
11
|
+
FeatureType,
|
12
|
+
)
|
13
|
+
from job_shop_lib import ScheduledOperation
|
14
|
+
|
15
|
+
|
16
|
+
class EarliestStartTimeObserver(FeatureObserver):
|
17
|
+
"""Observer that adds a feature indicating the earliest start time of
|
18
|
+
each operation, machine, and job in the graph.
|
19
|
+
|
20
|
+
The earliest start time of an operation refers to the earliest time at
|
21
|
+
which the operation could potentially start without violating any
|
22
|
+
constraints. This time is normalized by the current time (i.e., the
|
23
|
+
difference between the earliest start time and the current time).
|
24
|
+
|
25
|
+
The earliest start time of a machine is the earliest start time of the
|
26
|
+
next operation that can be scheduled on that machine.
|
27
|
+
|
28
|
+
Finally, the earliest start time of a job is the earliest start time of the
|
29
|
+
next operation in the job.
|
30
|
+
|
31
|
+
Args:
|
32
|
+
dispatcher:
|
33
|
+
The :class:`~job_shop_lib.dispatching.Dispatcher` to observe.
|
34
|
+
subscribe:
|
35
|
+
If ``True``, the observer is subscribed to the dispatcher upon
|
36
|
+
initialization. Otherwise, the observer must be subscribed later
|
37
|
+
or manually updated.
|
38
|
+
feature_types:
|
39
|
+
A list of :class:`FeatureType` or a single :class:`FeatureType`
|
40
|
+
that specifies the types of features to observe. They must be a
|
41
|
+
subset of the class attribute :attr:`supported_feature_types`.
|
42
|
+
If ``None``, all supported feature types are tracked.
|
43
|
+
"""
|
44
|
+
|
45
|
+
__slots__ = {
|
46
|
+
"earliest_start_times": (
|
47
|
+
"A 2D numpy array with the earliest start "
|
48
|
+
"times of each operation. The array has "
|
49
|
+
"shape (``num_jobs``, ``max_operations_per_job``). "
|
50
|
+
"The value at index (i, j) is the earliest start "
|
51
|
+
"time of the j-th operation in the i-th job. "
|
52
|
+
"If a job has fewer than the maximum number of "
|
53
|
+
"operations in a job, the remaining values are "
|
54
|
+
"set to ``np.nan``. Similarly to "
|
55
|
+
":class:`~job_shop_lib.JobShopInstance`'s "
|
56
|
+
":meth:`~job_shop_lib.JobShopInstance.durations_matrix_array` "
|
57
|
+
"method."
|
58
|
+
),
|
59
|
+
"_job_ids": (
|
60
|
+
"An array that stores the job IDs for each operation in the "
|
61
|
+
"dispatcher's instance. The array has shape "
|
62
|
+
"(``num_machines``, ``max_operations_per_machine``)."
|
63
|
+
),
|
64
|
+
"_positions": (
|
65
|
+
"An array that stores the positions of each operation in their "
|
66
|
+
"respective jobs. The array has shape "
|
67
|
+
"(``num_machines``, ``max_operations_per_machine``)."
|
68
|
+
),
|
69
|
+
"_is_regular_instance": (
|
70
|
+
"Whether the dispatcher's instance is a regular "
|
71
|
+
"instance, where each job has the same number of operations."
|
72
|
+
),
|
73
|
+
}
|
74
|
+
|
75
|
+
def __init__(
|
76
|
+
self,
|
77
|
+
dispatcher: Dispatcher,
|
78
|
+
*,
|
79
|
+
subscribe: bool = True,
|
80
|
+
feature_types: Optional[Union[List[FeatureType], FeatureType]] = None,
|
81
|
+
):
|
82
|
+
|
83
|
+
# Earliest start times initialization
|
84
|
+
# -------------------------------
|
85
|
+
squared_duration_matrix = dispatcher.instance.durations_matrix_array
|
86
|
+
self.earliest_start_times: NDArray[np.float32] = np.hstack(
|
87
|
+
(
|
88
|
+
np.zeros((squared_duration_matrix.shape[0], 1), dtype=float),
|
89
|
+
np.cumsum(squared_duration_matrix[:, :-1], axis=1),
|
90
|
+
)
|
91
|
+
)
|
92
|
+
self.earliest_start_times[np.isnan(squared_duration_matrix)] = np.nan
|
93
|
+
# -------------------------------
|
94
|
+
|
95
|
+
# Cache:
|
96
|
+
operations_by_machine = dispatcher.instance.operations_by_machine
|
97
|
+
self._is_regular_instance = all(
|
98
|
+
len(job) == len(dispatcher.instance.jobs[0])
|
99
|
+
for job in dispatcher.instance.jobs
|
100
|
+
)
|
101
|
+
if self._is_regular_instance:
|
102
|
+
self._job_ids = np.array(
|
103
|
+
[
|
104
|
+
[op.job_id for op in machine_ops]
|
105
|
+
for machine_ops in operations_by_machine
|
106
|
+
]
|
107
|
+
)
|
108
|
+
self._positions = np.array(
|
109
|
+
[
|
110
|
+
[op.position_in_job for op in machine_ops]
|
111
|
+
for machine_ops in operations_by_machine
|
112
|
+
]
|
113
|
+
)
|
114
|
+
else:
|
115
|
+
self._job_ids = np.array([])
|
116
|
+
self._positions = np.array([])
|
117
|
+
|
118
|
+
super().__init__(
|
119
|
+
dispatcher, feature_types=feature_types, subscribe=subscribe
|
120
|
+
)
|
121
|
+
|
122
|
+
def update(self, scheduled_operation: ScheduledOperation):
|
123
|
+
"""Recomputes the earliest start times and calls the
|
124
|
+
``initialize_features`` method.
|
125
|
+
|
126
|
+
The earliest start times is computed as the cumulative sum of the
|
127
|
+
previous unscheduled operations in the job plus the maximum of the
|
128
|
+
completion time of the last scheduled operation and the next available
|
129
|
+
time of the machine(s) the operation is assigned.
|
130
|
+
|
131
|
+
After that, we substract the current time.
|
132
|
+
|
133
|
+
Args:
|
134
|
+
scheduled_operation: The operation that has been scheduled.
|
135
|
+
"""
|
136
|
+
# We compute the gap that the current scheduled operation could be
|
137
|
+
# adding to each job.
|
138
|
+
job_id = scheduled_operation.job_id
|
139
|
+
next_operation_idx = self.dispatcher.job_next_operation_index[job_id]
|
140
|
+
if next_operation_idx < len(self.dispatcher.instance.jobs[job_id]):
|
141
|
+
old_start_time = self.earliest_start_times[
|
142
|
+
job_id, next_operation_idx
|
143
|
+
]
|
144
|
+
next_operation = self.dispatcher.instance.jobs[job_id][
|
145
|
+
next_operation_idx
|
146
|
+
]
|
147
|
+
new_start_time = max(
|
148
|
+
scheduled_operation.end_time,
|
149
|
+
old_start_time,
|
150
|
+
self.dispatcher.earliest_start_time(next_operation),
|
151
|
+
)
|
152
|
+
gap = new_start_time - old_start_time
|
153
|
+
self.earliest_start_times[job_id, next_operation_idx:] += gap
|
154
|
+
|
155
|
+
# Now, we compute the gap that could be introduced by the new
|
156
|
+
# next_available_time of the machine.
|
157
|
+
machine_ops = self.dispatcher.instance.operations_by_machine[
|
158
|
+
scheduled_operation.machine_id
|
159
|
+
]
|
160
|
+
unscheduled_mask = np.array(
|
161
|
+
[not self.dispatcher.is_scheduled(op) for op in machine_ops]
|
162
|
+
)
|
163
|
+
if np.any(unscheduled_mask):
|
164
|
+
if self._job_ids.size == 0:
|
165
|
+
job_ids = np.array([op.job_id for op in machine_ops])[
|
166
|
+
unscheduled_mask
|
167
|
+
]
|
168
|
+
else:
|
169
|
+
job_ids = self._job_ids[scheduled_operation.machine_id][
|
170
|
+
unscheduled_mask
|
171
|
+
]
|
172
|
+
|
173
|
+
if self._positions.size == 0:
|
174
|
+
positions = np.array(
|
175
|
+
[op.position_in_job for op in machine_ops]
|
176
|
+
)[unscheduled_mask]
|
177
|
+
else:
|
178
|
+
positions = self._positions[scheduled_operation.machine_id][
|
179
|
+
unscheduled_mask
|
180
|
+
]
|
181
|
+
old_start_times = self.earliest_start_times[job_ids, positions]
|
182
|
+
new_start_times = np.maximum(
|
183
|
+
scheduled_operation.end_time, old_start_times
|
184
|
+
)
|
185
|
+
gaps = new_start_times - old_start_times
|
186
|
+
|
187
|
+
for job_id, position, gap in zip(job_ids, positions, gaps):
|
188
|
+
self.earliest_start_times[job_id, position:] += gap
|
189
|
+
|
190
|
+
self.initialize_features()
|
191
|
+
|
192
|
+
def initialize_features(self):
|
193
|
+
"""Initializes the features based on the current state of the
|
194
|
+
dispatcher."""
|
195
|
+
for feature_type in self.features:
|
196
|
+
if feature_type == FeatureType.OPERATIONS:
|
197
|
+
self._update_operation_features()
|
198
|
+
elif (
|
199
|
+
feature_type == FeatureType.MACHINES
|
200
|
+
and self._is_regular_instance
|
201
|
+
):
|
202
|
+
self._update_machine_features_vectorized()
|
203
|
+
elif feature_type == FeatureType.MACHINES:
|
204
|
+
self._update_machine_features()
|
205
|
+
elif feature_type == FeatureType.JOBS:
|
206
|
+
self._update_job_features()
|
207
|
+
|
208
|
+
def _update_operation_features(self):
|
209
|
+
"""Ravels the 2D array into a 1D array"""
|
210
|
+
current_time = self.dispatcher.current_time()
|
211
|
+
next_index = 0
|
212
|
+
for job_id, operations in enumerate(self.dispatcher.instance.jobs):
|
213
|
+
self.features[FeatureType.OPERATIONS][
|
214
|
+
next_index : next_index + len(operations), 0
|
215
|
+
] = (
|
216
|
+
self.earliest_start_times[job_id, : len(operations)]
|
217
|
+
- current_time
|
218
|
+
)
|
219
|
+
next_index += len(operations)
|
220
|
+
|
221
|
+
def _update_machine_features(self):
|
222
|
+
"""Picks the minimum start time of all operations that can be scheduled
|
223
|
+
on that machine"""
|
224
|
+
current_time = self.dispatcher.current_time()
|
225
|
+
operations_by_machine = self.dispatcher.instance.operations_by_machine
|
226
|
+
for machine_id, operations in enumerate(operations_by_machine):
|
227
|
+
min_earliest_start_time = min(
|
228
|
+
(
|
229
|
+
self.earliest_start_times[
|
230
|
+
operation.job_id, operation.position_in_job
|
231
|
+
]
|
232
|
+
for operation in operations
|
233
|
+
if not self.dispatcher.is_scheduled(operation)
|
234
|
+
),
|
235
|
+
default=0,
|
236
|
+
)
|
237
|
+
self.features[FeatureType.MACHINES][machine_id, 0] = (
|
238
|
+
min_earliest_start_time - current_time
|
239
|
+
)
|
240
|
+
|
241
|
+
def _update_machine_features_vectorized(self):
|
242
|
+
"""Picks the minimum start time of all operations that can be scheduled
|
243
|
+
on that machine"""
|
244
|
+
current_time = self.dispatcher.current_time()
|
245
|
+
operations_by_machine = self.dispatcher.instance.operations_by_machine
|
246
|
+
|
247
|
+
# Create a mask for unscheduled operations
|
248
|
+
is_unscheduled = np.array(
|
249
|
+
[
|
250
|
+
[not self.dispatcher.is_scheduled(op) for op in machine_ops]
|
251
|
+
for machine_ops in operations_by_machine
|
252
|
+
]
|
253
|
+
)
|
254
|
+
|
255
|
+
# Get earliest start times for all operations
|
256
|
+
earliest_start_times = self.earliest_start_times[
|
257
|
+
self._job_ids, self._positions
|
258
|
+
]
|
259
|
+
|
260
|
+
# Apply mask for unscheduled operations
|
261
|
+
masked_start_times = np.where(
|
262
|
+
is_unscheduled, earliest_start_times, np.inf
|
263
|
+
)
|
264
|
+
|
265
|
+
# Find minimum start time for each machine
|
266
|
+
min_start_times = np.min(masked_start_times, axis=1)
|
267
|
+
|
268
|
+
# Handle cases where all operations are scheduled
|
269
|
+
min_start_times = np.where(
|
270
|
+
np.isinf(min_start_times), 0, min_start_times
|
271
|
+
)
|
272
|
+
|
273
|
+
self.features[FeatureType.MACHINES][:, 0] = (
|
274
|
+
min_start_times - current_time
|
275
|
+
)
|
276
|
+
|
277
|
+
def _update_job_features(self):
|
278
|
+
"""Picks the earliest start time of the next operation in the job"""
|
279
|
+
current_time = self.dispatcher.current_time()
|
280
|
+
for job_id, next_operation_idx in enumerate(
|
281
|
+
self.dispatcher.job_next_operation_index
|
282
|
+
):
|
283
|
+
job_length = len(self.dispatcher.instance.jobs[job_id])
|
284
|
+
if next_operation_idx == job_length:
|
285
|
+
continue
|
286
|
+
self.features[FeatureType.JOBS][job_id, 0] = (
|
287
|
+
self.earliest_start_times[job_id, next_operation_idx]
|
288
|
+
- current_time
|
289
|
+
)
|
@@ -0,0 +1,95 @@
|
|
1
|
+
"""Contains factory functions for creating :class:`FeatureObserver`s."""
|
2
|
+
|
3
|
+
from enum import Enum
|
4
|
+
from typing import Union, Type
|
5
|
+
|
6
|
+
from job_shop_lib.dispatching import DispatcherObserverConfig
|
7
|
+
from job_shop_lib.dispatching.feature_observers import (
|
8
|
+
IsReadyObserver,
|
9
|
+
EarliestStartTimeObserver,
|
10
|
+
FeatureObserver,
|
11
|
+
DurationObserver,
|
12
|
+
IsScheduledObserver,
|
13
|
+
PositionInJobObserver,
|
14
|
+
RemainingOperationsObserver,
|
15
|
+
IsCompletedObserver,
|
16
|
+
)
|
17
|
+
|
18
|
+
|
19
|
+
class FeatureObserverType(str, Enum):
|
20
|
+
"""Enumeration of the different feature observers.
|
21
|
+
|
22
|
+
Each :class:`FeatureObserver` is associated with a string value that can be
|
23
|
+
used to create the :class:`FeatureObserver` using the factory function.
|
24
|
+
|
25
|
+
It does not include the :class:`CompositeFeatureObserver` class since this
|
26
|
+
observer is often managed separately from the others. For example, a
|
27
|
+
common use case is to create a list of feature observers and pass them to
|
28
|
+
"""
|
29
|
+
|
30
|
+
IS_READY = "is_ready"
|
31
|
+
EARLIEST_START_TIME = "earliest_start_time"
|
32
|
+
DURATION = "duration"
|
33
|
+
IS_SCHEDULED = "is_scheduled"
|
34
|
+
POSITION_IN_JOB = "position_in_job"
|
35
|
+
REMAINING_OPERATIONS = "remaining_operations"
|
36
|
+
IS_COMPLETED = "is_completed"
|
37
|
+
|
38
|
+
|
39
|
+
# FeatureObserverConfig = DispatcherObserverConfig[
|
40
|
+
# Type[FeatureObserver] | FeatureObserverType | str
|
41
|
+
# ]
|
42
|
+
# FeatureObserverConfig = DispatcherObserverConfig[
|
43
|
+
# Union[Type[FeatureObserver], FeatureObserverType, str]
|
44
|
+
# ]
|
45
|
+
FeatureObserverConfig = (
|
46
|
+
DispatcherObserverConfig[Type[FeatureObserver]]
|
47
|
+
| DispatcherObserverConfig[FeatureObserverType]
|
48
|
+
| DispatcherObserverConfig[str]
|
49
|
+
)
|
50
|
+
|
51
|
+
|
52
|
+
def feature_observer_factory(
|
53
|
+
feature_observer_type: Union[
|
54
|
+
str,
|
55
|
+
FeatureObserverType,
|
56
|
+
Type[FeatureObserver],
|
57
|
+
FeatureObserverConfig
|
58
|
+
],
|
59
|
+
**kwargs,
|
60
|
+
) -> FeatureObserver:
|
61
|
+
"""Creates and returns a :class:`FeatureObserver` based on the specified
|
62
|
+
:class:`FeatureObserver` type.
|
63
|
+
|
64
|
+
Args:
|
65
|
+
feature_creator_type:
|
66
|
+
The type of :class:`FeatureObserver` to create.
|
67
|
+
**kwargs:
|
68
|
+
Additional keyword arguments to pass to the
|
69
|
+
:class:`FeatureObserver` constructor.
|
70
|
+
|
71
|
+
Returns:
|
72
|
+
A :class:`FeatureObserver` instance.
|
73
|
+
"""
|
74
|
+
if isinstance(feature_observer_type, DispatcherObserverConfig):
|
75
|
+
return feature_observer_factory(
|
76
|
+
feature_observer_type.class_type,
|
77
|
+
**feature_observer_type.kwargs,
|
78
|
+
**kwargs,
|
79
|
+
)
|
80
|
+
# if the instance is of type Type[FeatureObserver] we can just
|
81
|
+
# call the object constructor with the keyword arguments
|
82
|
+
if isinstance(feature_observer_type, type):
|
83
|
+
return feature_observer_type(**kwargs)
|
84
|
+
|
85
|
+
mapping: dict[FeatureObserverType, Type[FeatureObserver]] = {
|
86
|
+
FeatureObserverType.IS_READY: IsReadyObserver,
|
87
|
+
FeatureObserverType.EARLIEST_START_TIME: EarliestStartTimeObserver,
|
88
|
+
FeatureObserverType.DURATION: DurationObserver,
|
89
|
+
FeatureObserverType.IS_SCHEDULED: IsScheduledObserver,
|
90
|
+
FeatureObserverType.POSITION_IN_JOB: PositionInJobObserver,
|
91
|
+
FeatureObserverType.REMAINING_OPERATIONS: RemainingOperationsObserver,
|
92
|
+
FeatureObserverType.IS_COMPLETED: IsCompletedObserver,
|
93
|
+
}
|
94
|
+
feature_observer = mapping[feature_observer_type] # type: ignore[index]
|
95
|
+
return feature_observer(**kwargs)
|