job-shop-lib 0.5.0__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 +19 -0
- job_shop_lib/generation/_general_instance_generator.py +165 -0
- job_shop_lib/generation/_instance_generator.py +133 -0
- 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.0.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.0.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/generators/__init__.py +0 -7
- job_shop_lib/generators/basic_generator.py +0 -197
- 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.0.dist-info/RECORD +0 -48
- {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/LICENSE +0 -0
@@ -0,0 +1,228 @@
|
|
1
|
+
"""Home of the `FeatureObserver` class and `FeatureType` enum."""
|
2
|
+
|
3
|
+
import enum
|
4
|
+
from typing import Optional, Union, Dict, List, Tuple
|
5
|
+
|
6
|
+
import numpy as np
|
7
|
+
|
8
|
+
from job_shop_lib import ScheduledOperation
|
9
|
+
from job_shop_lib.exceptions import ValidationError
|
10
|
+
from job_shop_lib.dispatching import Dispatcher, DispatcherObserver
|
11
|
+
|
12
|
+
|
13
|
+
class FeatureType(str, enum.Enum):
|
14
|
+
"""Types of features that can be extracted."""
|
15
|
+
|
16
|
+
OPERATIONS = "operations"
|
17
|
+
MACHINES = "machines"
|
18
|
+
JOBS = "jobs"
|
19
|
+
|
20
|
+
|
21
|
+
class FeatureObserver(DispatcherObserver):
|
22
|
+
"""Base class for feature observers.
|
23
|
+
|
24
|
+
A :class:`FeatureObserver` is a
|
25
|
+
a subclass of :class:`~job_shop_lib.dispatching.DispatcherObserver` that
|
26
|
+
observes features related to operations, machines, or jobs in the
|
27
|
+
:class:`~job_shop_lib.dispatching.Dispatcher`.
|
28
|
+
|
29
|
+
Attributes are stored in numpy arrays with a shape of (``num_entities``,
|
30
|
+
``feature_size``), where ``num_entities`` is the number of entities being
|
31
|
+
observed (e.g., operations, machines, or jobs) and ``feature_size`` is the
|
32
|
+
number of values being observed for each entity.
|
33
|
+
|
34
|
+
The advantage of using arrays is that they can be easily updated in a
|
35
|
+
vectorized manner, which is more efficient than updating each attribute
|
36
|
+
individually. Furthermore, machine learning models can be trained on these
|
37
|
+
arrays to predict the best dispatching decisions.
|
38
|
+
|
39
|
+
Arrays use the data type ``np.float32``.
|
40
|
+
|
41
|
+
New :class:`FeatureObservers` must inherit from this class, and re-define
|
42
|
+
the class attributes ``_singleton`` (defualt ), ``_feature_size``
|
43
|
+
(default 1) and ``_supported_feature_types`` (default all feature types).
|
44
|
+
|
45
|
+
Feature observers are not singleton by default. This means that more than
|
46
|
+
one instance of the same feature observer type can be subscribed to the
|
47
|
+
dispatcher. This is useful when the first subscriber only observes a subset
|
48
|
+
of the features, and the second subscriber observes a different subset of
|
49
|
+
them. For example, the first subscriber could observe only the
|
50
|
+
operation-related features, while the second subscriber could observe the
|
51
|
+
jobs.
|
52
|
+
|
53
|
+
Args:
|
54
|
+
dispatcher:
|
55
|
+
The :class:`~job_shop_lib.dispatching.Dispatcher` to observe.
|
56
|
+
subscribe:
|
57
|
+
If ``True``, the observer is subscribed to the dispatcher upon
|
58
|
+
initialization. Otherwise, the observer must be subscribed later
|
59
|
+
or manually updated.
|
60
|
+
feature_types:
|
61
|
+
A list of :class:`FeatureType` or a single :class:`FeatureType`
|
62
|
+
that specifies the types of features to observe. They must be a
|
63
|
+
subset of the class attribute :attr:`supported_feature_types`.
|
64
|
+
If ``None``, all supported feature types are tracked.
|
65
|
+
"""
|
66
|
+
|
67
|
+
_is_singleton = False
|
68
|
+
_feature_sizes: Union[Dict[FeatureType, int], int] = 1
|
69
|
+
_supported_feature_types = list(FeatureType)
|
70
|
+
|
71
|
+
__slots__ = {
|
72
|
+
"features": (
|
73
|
+
"A dictionary of numpy arrays with the features. "
|
74
|
+
"Each key is a :class:`FeatureType` and each value is a numpy "
|
75
|
+
"array with the features. The array has shape (``num_entities``, "
|
76
|
+
"``feature_size``), where ``num_entities`` is the number of "
|
77
|
+
"entities being observed (e.g., operations, machines, or jobs) and"
|
78
|
+
" ``feature_size`` is the number of values being observed for each"
|
79
|
+
" entity."
|
80
|
+
)
|
81
|
+
}
|
82
|
+
|
83
|
+
def __init__(
|
84
|
+
self,
|
85
|
+
dispatcher: Dispatcher,
|
86
|
+
*,
|
87
|
+
subscribe: bool = True,
|
88
|
+
feature_types: Optional[Union[List[FeatureType], FeatureType]] = None,
|
89
|
+
):
|
90
|
+
feature_types = self._get_feature_types_list(feature_types)
|
91
|
+
if isinstance(self._feature_sizes, int):
|
92
|
+
feature_size = {
|
93
|
+
feature_type: self._feature_sizes
|
94
|
+
for feature_type in feature_types
|
95
|
+
}
|
96
|
+
super().__init__(dispatcher, subscribe=subscribe)
|
97
|
+
|
98
|
+
number_of_entities = {
|
99
|
+
FeatureType.OPERATIONS: dispatcher.instance.num_operations,
|
100
|
+
FeatureType.MACHINES: dispatcher.instance.num_machines,
|
101
|
+
FeatureType.JOBS: dispatcher.instance.num_jobs,
|
102
|
+
}
|
103
|
+
feature_dimensions = {
|
104
|
+
feature_type: (
|
105
|
+
number_of_entities[feature_type],
|
106
|
+
feature_size[feature_type],
|
107
|
+
)
|
108
|
+
for feature_type in feature_types
|
109
|
+
}
|
110
|
+
self.features = {
|
111
|
+
feature_type: np.zeros(
|
112
|
+
feature_dimensions[feature_type],
|
113
|
+
dtype=np.float32,
|
114
|
+
)
|
115
|
+
for feature_type in feature_types
|
116
|
+
}
|
117
|
+
self.initialize_features()
|
118
|
+
|
119
|
+
@property
|
120
|
+
def feature_sizes(self) -> Dict[FeatureType, int]:
|
121
|
+
"""Returns the size of the features.
|
122
|
+
|
123
|
+
The size of the features is the number of values being observed for
|
124
|
+
each entity. This corresponds to the second dimension of each array.
|
125
|
+
|
126
|
+
This number is typically one (e.g. measuring the duration
|
127
|
+
of each operation), but some feature observers like the
|
128
|
+
:class:`CompositeFeatureObserver` may track more than one value.
|
129
|
+
"""
|
130
|
+
if isinstance(self._feature_sizes, int):
|
131
|
+
return {
|
132
|
+
feature_type: self._feature_sizes
|
133
|
+
for feature_type in self.features
|
134
|
+
}
|
135
|
+
return self._feature_sizes
|
136
|
+
|
137
|
+
@property
|
138
|
+
def supported_feature_types(self) -> List[FeatureType]:
|
139
|
+
"""Returns the supported feature types."""
|
140
|
+
return self._supported_feature_types
|
141
|
+
|
142
|
+
@property
|
143
|
+
def feature_dimensions(self) -> Dict[FeatureType, Tuple[int, int]]:
|
144
|
+
"""A dictionary containing the shape of each :class:`FeatureType`."""
|
145
|
+
feature_dimensions = {}
|
146
|
+
for feature_type, array in self.features.items():
|
147
|
+
feature_dimensions[feature_type] = array.shape
|
148
|
+
return feature_dimensions # type: ignore[return-value]
|
149
|
+
|
150
|
+
def initialize_features(self):
|
151
|
+
"""Initializes the features based on the current state of the
|
152
|
+
dispatcher.
|
153
|
+
|
154
|
+
This method is automatically called after initializing the observer.
|
155
|
+
"""
|
156
|
+
|
157
|
+
def update(self, scheduled_operation: ScheduledOperation):
|
158
|
+
"""Updates the features based on the scheduled operation.
|
159
|
+
|
160
|
+
By default, this method just calls :meth:`initialize_features`.
|
161
|
+
|
162
|
+
Args:
|
163
|
+
ScheduledOperation:
|
164
|
+
The operation that has been scheduled.
|
165
|
+
"""
|
166
|
+
self.initialize_features()
|
167
|
+
|
168
|
+
def reset(self):
|
169
|
+
"""Sets features to zero and calls to :meth:``initialize_features``."""
|
170
|
+
self.set_features_to_zero()
|
171
|
+
self.initialize_features()
|
172
|
+
|
173
|
+
def set_features_to_zero(
|
174
|
+
self, exclude: Optional[Union[FeatureType, List[FeatureType]]] = None
|
175
|
+
):
|
176
|
+
"""Sets all features to zero except for the ones specified in
|
177
|
+
``exclude``.
|
178
|
+
|
179
|
+
Setting a feature to zero means that all values in the feature array
|
180
|
+
are set to this value.
|
181
|
+
|
182
|
+
Args:
|
183
|
+
exclude:
|
184
|
+
A single :class:`FeatureType` or a list of :class:`FeatureType`
|
185
|
+
that specifies the features that should not be set to zero. If
|
186
|
+
``None``, all currently used features are set to zero.
|
187
|
+
"""
|
188
|
+
if exclude is None:
|
189
|
+
exclude = []
|
190
|
+
if isinstance(exclude, FeatureType):
|
191
|
+
exclude = [exclude]
|
192
|
+
|
193
|
+
for feature_type in self.features:
|
194
|
+
if feature_type in exclude:
|
195
|
+
continue
|
196
|
+
self.features[feature_type][:] = 0.0
|
197
|
+
|
198
|
+
def _get_feature_types_list(
|
199
|
+
self,
|
200
|
+
feature_types: Optional[Union[List[FeatureType], FeatureType]],
|
201
|
+
) -> List[FeatureType]:
|
202
|
+
"""Returns a list of feature types.
|
203
|
+
|
204
|
+
Args:
|
205
|
+
feature_types:
|
206
|
+
A list of feature types or a single feature type. If ``None``,
|
207
|
+
all feature types are returned.
|
208
|
+
"""
|
209
|
+
if isinstance(feature_types, FeatureType):
|
210
|
+
return [feature_types]
|
211
|
+
if feature_types is None:
|
212
|
+
return self._supported_feature_types
|
213
|
+
|
214
|
+
for feature_type in feature_types:
|
215
|
+
if feature_type not in self._supported_feature_types:
|
216
|
+
raise ValidationError(
|
217
|
+
f"Feature type {feature_type} is not supported."
|
218
|
+
" Supported feature types are: "
|
219
|
+
f"{self._supported_feature_types}"
|
220
|
+
)
|
221
|
+
return feature_types
|
222
|
+
|
223
|
+
def __str__(self):
|
224
|
+
out = [self.__class__.__name__, ":\n"]
|
225
|
+
out.append("-" * len(out[0]))
|
226
|
+
for feature_type, feature in self.features.items():
|
227
|
+
out.append(f"\n{feature_type.value}:\n{feature}")
|
228
|
+
return "".join(out)
|
@@ -0,0 +1,97 @@
|
|
1
|
+
"""Home of the `IsCompletedObserver` class."""
|
2
|
+
|
3
|
+
from typing import Optional, Union, List
|
4
|
+
import numpy as np
|
5
|
+
|
6
|
+
from job_shop_lib import ScheduledOperation
|
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 IsCompletedObserver(FeatureObserver):
|
15
|
+
"""Adds a binary feature indicating whether each operation,
|
16
|
+
machine, or job has been completed.
|
17
|
+
|
18
|
+
An operation is considered completed if it has been scheduled and the
|
19
|
+
current time is greater than or equal to the sum of the operation's start
|
20
|
+
time and duration.
|
21
|
+
|
22
|
+
A machine or job is considered completed if all of its operations have been
|
23
|
+
completed.
|
24
|
+
|
25
|
+
Args:
|
26
|
+
dispatcher:
|
27
|
+
The :class:`~job_shop_lib.dispatching.Dispatcher` to observe.
|
28
|
+
feature_types:
|
29
|
+
A list of :class:`FeatureType` or a single :class:`FeatureType`
|
30
|
+
that specifies the types of features to observe. They must be a
|
31
|
+
subset of the class attribute :attr:`supported_feature_types`.
|
32
|
+
If ``None``, all supported feature types are tracked.
|
33
|
+
subscribe:
|
34
|
+
If ``True``, the observer is subscribed to the dispatcher upon
|
35
|
+
initialization. Otherwise, the observer must be subscribed later
|
36
|
+
or manually updated.
|
37
|
+
"""
|
38
|
+
|
39
|
+
def __init__(
|
40
|
+
self,
|
41
|
+
dispatcher: Dispatcher,
|
42
|
+
*,
|
43
|
+
feature_types: Optional[Union[List[FeatureType], FeatureType]] = None,
|
44
|
+
subscribe: bool = True,
|
45
|
+
):
|
46
|
+
feature_types = self._get_feature_types_list(feature_types)
|
47
|
+
self._num_of_operations_per_machine = np.array(
|
48
|
+
[
|
49
|
+
len(operations_by_machine)
|
50
|
+
for operations_by_machine in (
|
51
|
+
dispatcher.instance.operations_by_machine
|
52
|
+
)
|
53
|
+
]
|
54
|
+
)
|
55
|
+
self._num_of_operations_per_job = np.array(
|
56
|
+
[len(job) for job in dispatcher.instance.jobs]
|
57
|
+
)
|
58
|
+
super().__init__(
|
59
|
+
dispatcher,
|
60
|
+
feature_types=feature_types,
|
61
|
+
subscribe=subscribe,
|
62
|
+
)
|
63
|
+
|
64
|
+
def initialize_features(self):
|
65
|
+
if FeatureType.OPERATIONS in self.features:
|
66
|
+
completed_operations = [
|
67
|
+
op.operation.operation_id
|
68
|
+
for op in self.dispatcher.completed_operations()
|
69
|
+
]
|
70
|
+
self.features[FeatureType.OPERATIONS][completed_operations, 0] = 1
|
71
|
+
if FeatureType.MACHINES in self.features:
|
72
|
+
num_completed_ops_per_machine = np.zeros(
|
73
|
+
len(self._num_of_operations_per_machine)
|
74
|
+
)
|
75
|
+
for op in self.dispatcher.completed_operations():
|
76
|
+
for machine_id in op.operation.machines:
|
77
|
+
num_completed_ops_per_machine[machine_id] += 1
|
78
|
+
self.features[FeatureType.MACHINES][:, 0] = (
|
79
|
+
num_completed_ops_per_machine
|
80
|
+
== self._num_of_operations_per_machine
|
81
|
+
).astype(np.float32)
|
82
|
+
if FeatureType.JOBS in self.features:
|
83
|
+
num_completed_ops_per_job = np.zeros(
|
84
|
+
len(self._num_of_operations_per_job)
|
85
|
+
)
|
86
|
+
for op in self.dispatcher.completed_operations():
|
87
|
+
num_completed_ops_per_job[op.operation.job_id] += 1
|
88
|
+
self.features[FeatureType.JOBS][:, 0] = (
|
89
|
+
num_completed_ops_per_job
|
90
|
+
== self._num_of_operations_per_job
|
91
|
+
).astype(np.float32)
|
92
|
+
|
93
|
+
def reset(self):
|
94
|
+
self.set_features_to_zero()
|
95
|
+
|
96
|
+
def update(self, scheduled_operation: ScheduledOperation):
|
97
|
+
self.initialize_features()
|
@@ -0,0 +1,35 @@
|
|
1
|
+
"""Home of the `IsReadyObserver` class."""
|
2
|
+
|
3
|
+
from typing import List
|
4
|
+
|
5
|
+
from job_shop_lib.dispatching.feature_observers import (
|
6
|
+
FeatureObserver,
|
7
|
+
FeatureType,
|
8
|
+
)
|
9
|
+
|
10
|
+
|
11
|
+
class IsReadyObserver(FeatureObserver):
|
12
|
+
"""Feature creator that adds a binary feature indicating if the operation,
|
13
|
+
machine or job is ready to be dispatched."""
|
14
|
+
|
15
|
+
def initialize_features(self):
|
16
|
+
self.set_features_to_zero()
|
17
|
+
for feature_type, feature in self.features.items():
|
18
|
+
feature_ids = self._get_ready_feature_ids(feature_type)
|
19
|
+
feature[feature_ids, 0] = 1.0
|
20
|
+
|
21
|
+
def _get_ready_feature_ids(self, feature_type: FeatureType) -> List[int]:
|
22
|
+
if feature_type == FeatureType.OPERATIONS:
|
23
|
+
return self._get_ready_operations()
|
24
|
+
if feature_type == FeatureType.MACHINES:
|
25
|
+
return self.dispatcher.available_machines()
|
26
|
+
if feature_type == FeatureType.JOBS:
|
27
|
+
return self.dispatcher.available_jobs()
|
28
|
+
raise ValueError(f"Feature type {feature_type} is not supported.")
|
29
|
+
|
30
|
+
def reset(self):
|
31
|
+
self.initialize_features()
|
32
|
+
|
33
|
+
def _get_ready_operations(self) -> List[int]:
|
34
|
+
available_operations = self.dispatcher.available_operations()
|
35
|
+
return [operation.operation_id for operation in available_operations]
|
job_shop_lib/dispatching/feature_observers/{is_scheduled_observer.py → _is_scheduled_observer.py}
RENAMED
@@ -8,14 +8,18 @@ from job_shop_lib.dispatching.feature_observers import (
|
|
8
8
|
|
9
9
|
|
10
10
|
class IsScheduledObserver(FeatureObserver):
|
11
|
-
"""
|
11
|
+
"""Updates features based on scheduling operations.
|
12
12
|
|
13
13
|
This observer tracks which operations have been scheduled and updates
|
14
|
-
feature matrices accordingly.
|
15
|
-
|
16
|
-
|
14
|
+
feature matrices accordingly.
|
15
|
+
|
16
|
+
It updates a feature in the
|
17
|
+
:meth:`FeatureType.OPERATIONS` matrix to indicate that an operation has
|
18
|
+
been scheduled.
|
19
|
+
|
20
|
+
Additionally, it counts the number of uncompleted but
|
17
21
|
scheduled operations for each machine and job, updating the respective
|
18
|
-
|
22
|
+
:meth:`FeatureType.MACHINES` and :meth:`FeatureType.JOBS` feature matrices.
|
19
23
|
"""
|
20
24
|
|
21
25
|
def update(self, scheduled_operation: ScheduledOperation):
|
@@ -1,6 +1,5 @@
|
|
1
1
|
"""Home of the `PositionInJobObserver` class."""
|
2
2
|
|
3
|
-
from job_shop_lib.dispatching import Dispatcher
|
4
3
|
from job_shop_lib import ScheduledOperation
|
5
4
|
from job_shop_lib.dispatching.feature_observers import (
|
6
5
|
FeatureObserver,
|
@@ -9,19 +8,18 @@ from job_shop_lib.dispatching.feature_observers import (
|
|
9
8
|
|
10
9
|
|
11
10
|
class PositionInJobObserver(FeatureObserver):
|
12
|
-
"""
|
11
|
+
"""Adds a feature indicating the position of
|
13
12
|
operations in their respective jobs.
|
14
13
|
|
15
|
-
Positions are adjusted dynamically as operations are scheduled.
|
14
|
+
Positions are adjusted dynamically as operations are scheduled. In other
|
15
|
+
words, the position of an operation is the number of unscheduled operations
|
16
|
+
that precede it in the job.
|
17
|
+
|
18
|
+
It only supports the :meth:`~job_shop_lib.FeatureType.OPERATIONS` feature
|
19
|
+
type.
|
16
20
|
"""
|
17
21
|
|
18
|
-
|
19
|
-
super().__init__(
|
20
|
-
dispatcher,
|
21
|
-
feature_types=[FeatureType.OPERATIONS],
|
22
|
-
feature_size=1,
|
23
|
-
subscribe=subscribe,
|
24
|
-
)
|
22
|
+
_supported_feature_types = [FeatureType.OPERATIONS]
|
25
23
|
|
26
24
|
def initialize_features(self):
|
27
25
|
for operation in self.dispatcher.unscheduled_operations():
|
@@ -1,7 +1,7 @@
|
|
1
1
|
"""Home of the `RemainingOperationsObserver` class."""
|
2
2
|
|
3
3
|
from job_shop_lib import ScheduledOperation
|
4
|
-
from job_shop_lib.dispatching import
|
4
|
+
from job_shop_lib.dispatching import UnscheduledOperationsObserver
|
5
5
|
from job_shop_lib.dispatching.feature_observers import (
|
6
6
|
FeatureObserver,
|
7
7
|
FeatureType,
|
@@ -12,38 +12,20 @@ class RemainingOperationsObserver(FeatureObserver):
|
|
12
12
|
"""Adds a feature indicating the number of remaining operations for each
|
13
13
|
job and machine.
|
14
14
|
|
15
|
-
It does not support FeatureType.OPERATIONS
|
15
|
+
It does not support :meth:`FeatureType.OPERATIONS`.
|
16
16
|
"""
|
17
17
|
|
18
|
-
|
19
|
-
self,
|
20
|
-
dispatcher: Dispatcher,
|
21
|
-
feature_types: list[FeatureType] | FeatureType | None = None,
|
22
|
-
subscribe: bool = True,
|
23
|
-
):
|
24
|
-
if feature_types is None:
|
25
|
-
feature_types = [FeatureType.MACHINES, FeatureType.JOBS]
|
26
|
-
|
27
|
-
if (
|
28
|
-
feature_types == FeatureType.OPERATIONS
|
29
|
-
or FeatureType.OPERATIONS in feature_types
|
30
|
-
):
|
31
|
-
raise ValueError("FeatureType.OPERATIONS is not supported.")
|
32
|
-
super().__init__(
|
33
|
-
dispatcher,
|
34
|
-
feature_types=feature_types,
|
35
|
-
feature_size=1,
|
36
|
-
subscribe=subscribe,
|
37
|
-
)
|
18
|
+
_supported_feature_types = [FeatureType.MACHINES, FeatureType.JOBS]
|
38
19
|
|
39
20
|
def initialize_features(self):
|
40
|
-
|
21
|
+
unscheduled_ops_observer = self.dispatcher.create_or_get_observer(
|
22
|
+
UnscheduledOperationsObserver
|
23
|
+
)
|
24
|
+
for operation in unscheduled_ops_observer.unscheduled_operations:
|
41
25
|
if FeatureType.JOBS in self.features:
|
42
26
|
self.features[FeatureType.JOBS][operation.job_id, 0] += 1
|
43
27
|
if FeatureType.MACHINES in self.features:
|
44
|
-
self.features[FeatureType.MACHINES][
|
45
|
-
operation.machine_id, 0
|
46
|
-
] += 1
|
28
|
+
self.features[FeatureType.MACHINES][operation.machines, 0] += 1
|
47
29
|
|
48
30
|
def update(self, scheduled_operation: ScheduledOperation):
|
49
31
|
if FeatureType.JOBS in self.features:
|
@@ -0,0 +1,87 @@
|
|
1
|
+
"""Contains the dispatching rules for the job shop scheduling problem.
|
2
|
+
|
3
|
+
Main objects:
|
4
|
+
|
5
|
+
.. autosummary::
|
6
|
+
|
7
|
+
DispatchingRuleSolver
|
8
|
+
dispatching_rule_factory
|
9
|
+
DispatchingRuleType
|
10
|
+
MachineChooserType
|
11
|
+
dispatching_rule_factory
|
12
|
+
machine_chooser_factory
|
13
|
+
|
14
|
+
Dispatching rules:
|
15
|
+
|
16
|
+
.. autosummary::
|
17
|
+
|
18
|
+
shortest_processing_time_rule
|
19
|
+
first_come_first_served_rule
|
20
|
+
most_work_remaining_rule
|
21
|
+
most_operations_remaining_rule
|
22
|
+
random_operation_rule
|
23
|
+
score_based_rule
|
24
|
+
score_based_rule_with_tie_breaker
|
25
|
+
observer_based_most_work_remaining_rule
|
26
|
+
|
27
|
+
Dispatching rule scorers:
|
28
|
+
|
29
|
+
.. autosummary::
|
30
|
+
|
31
|
+
shortest_processing_time_score
|
32
|
+
first_come_first_served_score
|
33
|
+
MostWorkRemainingScorer
|
34
|
+
most_operations_remaining_score
|
35
|
+
random_score
|
36
|
+
|
37
|
+
"""
|
38
|
+
|
39
|
+
from ._dispatching_rules_functions import (
|
40
|
+
shortest_processing_time_rule,
|
41
|
+
first_come_first_served_rule,
|
42
|
+
most_work_remaining_rule,
|
43
|
+
most_operations_remaining_rule,
|
44
|
+
random_operation_rule,
|
45
|
+
score_based_rule,
|
46
|
+
score_based_rule_with_tie_breaker,
|
47
|
+
shortest_processing_time_score,
|
48
|
+
first_come_first_served_score,
|
49
|
+
MostWorkRemainingScorer,
|
50
|
+
most_operations_remaining_score,
|
51
|
+
random_score,
|
52
|
+
observer_based_most_work_remaining_rule,
|
53
|
+
)
|
54
|
+
from ._machine_chooser_factory import (
|
55
|
+
MachineChooserType,
|
56
|
+
MachineChooser,
|
57
|
+
machine_chooser_factory,
|
58
|
+
)
|
59
|
+
|
60
|
+
from ._dispatching_rule_factory import (
|
61
|
+
dispatching_rule_factory,
|
62
|
+
DispatchingRuleType,
|
63
|
+
)
|
64
|
+
from ._dispatching_rule_solver import DispatchingRuleSolver
|
65
|
+
|
66
|
+
|
67
|
+
__all__ = [
|
68
|
+
"shortest_processing_time_rule",
|
69
|
+
"first_come_first_served_rule",
|
70
|
+
"most_work_remaining_rule",
|
71
|
+
"most_operations_remaining_rule",
|
72
|
+
"random_operation_rule",
|
73
|
+
"score_based_rule",
|
74
|
+
"score_based_rule_with_tie_breaker",
|
75
|
+
"shortest_processing_time_score",
|
76
|
+
"first_come_first_served_score",
|
77
|
+
"MostWorkRemainingScorer",
|
78
|
+
"most_operations_remaining_score",
|
79
|
+
"random_score",
|
80
|
+
"dispatching_rule_factory",
|
81
|
+
"DispatchingRuleType",
|
82
|
+
"MachineChooserType",
|
83
|
+
"machine_chooser_factory",
|
84
|
+
"MachineChooser",
|
85
|
+
"DispatchingRuleSolver",
|
86
|
+
"observer_based_most_work_remaining_rule",
|
87
|
+
]
|
@@ -0,0 +1,84 @@
|
|
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 Dict, Union
|
9
|
+
|
10
|
+
from enum import Enum
|
11
|
+
from collections.abc import Callable
|
12
|
+
|
13
|
+
from job_shop_lib import Operation
|
14
|
+
from job_shop_lib.exceptions import ValidationError
|
15
|
+
from job_shop_lib.dispatching import Dispatcher
|
16
|
+
from job_shop_lib.dispatching.rules import (
|
17
|
+
shortest_processing_time_rule,
|
18
|
+
first_come_first_served_rule,
|
19
|
+
most_operations_remaining_rule,
|
20
|
+
random_operation_rule,
|
21
|
+
most_work_remaining_rule,
|
22
|
+
)
|
23
|
+
|
24
|
+
|
25
|
+
class DispatchingRuleType(str, Enum):
|
26
|
+
"""Enumeration of dispatching rules for the job shop scheduling problem."""
|
27
|
+
|
28
|
+
SHORTEST_PROCESSING_TIME = "shortest_processing_time"
|
29
|
+
FIRST_COME_FIRST_SERVED = "first_come_first_served"
|
30
|
+
MOST_WORK_REMAINING = "most_work_remaining"
|
31
|
+
MOST_OPERATIONS_REMAINING = "most_operations_remaining"
|
32
|
+
RANDOM = "random"
|
33
|
+
|
34
|
+
|
35
|
+
def dispatching_rule_factory(
|
36
|
+
dispatching_rule: Union[str, DispatchingRuleType],
|
37
|
+
) -> Callable[[Dispatcher], Operation]:
|
38
|
+
"""Creates and returns a dispatching rule function based on the specified
|
39
|
+
dispatching rule name.
|
40
|
+
|
41
|
+
The dispatching rule function determines the order in which operations are
|
42
|
+
selected for execution based on certain criteria such as shortest
|
43
|
+
processing time, first come first served, etc.
|
44
|
+
|
45
|
+
Args:
|
46
|
+
dispatching_rule: The name of the dispatching rule to be used.
|
47
|
+
Supported values are 'shortest_processing_time',
|
48
|
+
'first_come_first_served', 'most_work_remaining',
|
49
|
+
and 'random'.
|
50
|
+
|
51
|
+
Returns:
|
52
|
+
A function that takes a Dispatcher instance as input and returns an
|
53
|
+
Operation based on the specified dispatching rule.
|
54
|
+
|
55
|
+
Raises:
|
56
|
+
ValidationError:
|
57
|
+
If the dispatching_rule argument is not recognized or it is
|
58
|
+
not supported.
|
59
|
+
"""
|
60
|
+
dispatching_rules: Dict[
|
61
|
+
DispatchingRuleType,
|
62
|
+
Callable[[Dispatcher], Operation],
|
63
|
+
] = {
|
64
|
+
DispatchingRuleType.SHORTEST_PROCESSING_TIME: (
|
65
|
+
shortest_processing_time_rule
|
66
|
+
),
|
67
|
+
DispatchingRuleType.FIRST_COME_FIRST_SERVED: (
|
68
|
+
first_come_first_served_rule
|
69
|
+
),
|
70
|
+
DispatchingRuleType.MOST_WORK_REMAINING: most_work_remaining_rule,
|
71
|
+
DispatchingRuleType.MOST_OPERATIONS_REMAINING: (
|
72
|
+
most_operations_remaining_rule
|
73
|
+
),
|
74
|
+
DispatchingRuleType.RANDOM: random_operation_rule,
|
75
|
+
}
|
76
|
+
|
77
|
+
dispatching_rule = dispatching_rule.lower()
|
78
|
+
if dispatching_rule not in dispatching_rules:
|
79
|
+
raise ValidationError(
|
80
|
+
f"Dispatching rule {dispatching_rule} not recognized. Available "
|
81
|
+
f"dispatching rules: {', '.join(dispatching_rules)}."
|
82
|
+
)
|
83
|
+
|
84
|
+
return dispatching_rules[dispatching_rule] # type: ignore[index]
|