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,329 @@
|
|
1
|
+
"""Contains wrappers for the environments."""
|
2
|
+
|
3
|
+
from typing import TypeVar, TypedDict, Generic, Any
|
4
|
+
from gymnasium import ObservationWrapper
|
5
|
+
import numpy as np
|
6
|
+
from numpy.typing import NDArray
|
7
|
+
|
8
|
+
from job_shop_lib.reinforcement_learning import (
|
9
|
+
ObservationDict,
|
10
|
+
SingleJobShopGraphEnv,
|
11
|
+
MultiJobShopGraphEnv,
|
12
|
+
create_edge_type_dict,
|
13
|
+
map_values,
|
14
|
+
)
|
15
|
+
from job_shop_lib.graphs import NodeType
|
16
|
+
from job_shop_lib.dispatching.feature_observers import FeatureType
|
17
|
+
|
18
|
+
T = TypeVar("T", bound=np.number)
|
19
|
+
EnvType = TypeVar( # pylint: disable=invalid-name
|
20
|
+
"EnvType", bound=SingleJobShopGraphEnv | MultiJobShopGraphEnv
|
21
|
+
)
|
22
|
+
|
23
|
+
_NODE_TYPE_TO_FEATURE_TYPE = {
|
24
|
+
NodeType.OPERATION: FeatureType.OPERATIONS,
|
25
|
+
NodeType.MACHINE: FeatureType.MACHINES,
|
26
|
+
NodeType.JOB: FeatureType.JOBS,
|
27
|
+
}
|
28
|
+
_FEATURE_TYPE_STR_TO_NODE_TYPE = {
|
29
|
+
FeatureType.OPERATIONS.value: NodeType.OPERATION,
|
30
|
+
FeatureType.MACHINES.value: NodeType.MACHINE,
|
31
|
+
FeatureType.JOBS.value: NodeType.JOB,
|
32
|
+
}
|
33
|
+
|
34
|
+
|
35
|
+
class ResourceTaskGraphObservationDict(TypedDict):
|
36
|
+
"""Represents a dictionary for resource task graph observations."""
|
37
|
+
|
38
|
+
edge_index_dict: dict[tuple[str, str, str], NDArray[np.int32]]
|
39
|
+
node_features_dict: dict[str, NDArray[np.float32]]
|
40
|
+
original_ids_dict: dict[str, NDArray[np.int32]]
|
41
|
+
|
42
|
+
|
43
|
+
# pylint: disable=line-too-long
|
44
|
+
class ResourceTaskGraphObservation(ObservationWrapper, Generic[EnvType]):
|
45
|
+
"""Observation wrapper that converts an observation following the
|
46
|
+
:class:`ObservationDict` format to a format suitable to PyG's
|
47
|
+
[`HeteroData`](https://pytorch-geometric.readthedocs.io/en/latest/generated/torch_geometric.data.HeteroData.html).
|
48
|
+
|
49
|
+
In particular, the ``edge_index`` is converted into a ``edge_index_dict``
|
50
|
+
with keys ``(node_type_i, "to", node_type_j)``. The ``node_type_i`` and
|
51
|
+
``node_type_j`` are the node types of the source and target nodes,
|
52
|
+
respectively.
|
53
|
+
|
54
|
+
Additionally, the node features are stored in a dictionary with keys
|
55
|
+
corresponding to the node type names under the ``node_features_dict`` key.
|
56
|
+
|
57
|
+
The node IDs are mapped to local IDs starting from 0. The
|
58
|
+
``original_ids_dict`` contains the original node IDs before removing nodes.
|
59
|
+
|
60
|
+
Attributes:
|
61
|
+
global_to_local_id: A dictionary mapping global node IDs to local node
|
62
|
+
IDs for each node type.
|
63
|
+
type_ranges: A dictionary mapping node type names to (start, end) index
|
64
|
+
ranges.
|
65
|
+
|
66
|
+
Args:
|
67
|
+
env: The environment to wrap.
|
68
|
+
"""
|
69
|
+
|
70
|
+
def __init__(self, env: EnvType):
|
71
|
+
super().__init__(env)
|
72
|
+
self.env = env # Unnecessary, but makes mypy happy
|
73
|
+
self.global_to_local_id = self._compute_id_mappings()
|
74
|
+
self.type_ranges = self._compute_node_type_ranges()
|
75
|
+
self._start_from_zero_mapping: dict[str, dict[int, int]] = {}
|
76
|
+
|
77
|
+
def step(self, action: tuple[int, int]):
|
78
|
+
"""Takes a step in the environment.
|
79
|
+
|
80
|
+
Args:
|
81
|
+
action:
|
82
|
+
The action to take. The action is a tuple of two integers
|
83
|
+
(job_id, machine_id):
|
84
|
+
the job ID and the machine ID in which to schedule the
|
85
|
+
operation.
|
86
|
+
|
87
|
+
Returns:
|
88
|
+
A tuple containing the following elements:
|
89
|
+
|
90
|
+
- The observation of the environment.
|
91
|
+
- The reward obtained.
|
92
|
+
- Whether the environment is done.
|
93
|
+
- Whether the episode was truncated (always False).
|
94
|
+
- A dictionary with additional information. The dictionary
|
95
|
+
contains the following keys: "feature_names", the names of the
|
96
|
+
features in the observation; and "available_operations_with_ids",
|
97
|
+
a list of available actions in the form of (operation_id,
|
98
|
+
machine_id, job_id).
|
99
|
+
"""
|
100
|
+
observation, reward, done, truncated, info = self.env.step(action)
|
101
|
+
new_observation = self.observation(observation)
|
102
|
+
new_info = self._info(info)
|
103
|
+
return new_observation, reward, done, truncated, new_info
|
104
|
+
|
105
|
+
def reset(self, *, seed: int | None = None, options: dict | None = None):
|
106
|
+
"""Resets the environment.
|
107
|
+
|
108
|
+
Args:
|
109
|
+
seed:
|
110
|
+
Added to match the signature of the parent class. It is not
|
111
|
+
used in this method.
|
112
|
+
options:
|
113
|
+
Additional options to pass to the environment. Not used in
|
114
|
+
this method.
|
115
|
+
|
116
|
+
Returns:
|
117
|
+
A tuple containing the following elements:
|
118
|
+
|
119
|
+
- The observation of the environment.
|
120
|
+
- A dictionary with additional information, keys
|
121
|
+
include: "feature_names", the names of the features in the
|
122
|
+
observation; and "available_operations_with_ids", a list of
|
123
|
+
available a list of available actions in the form of
|
124
|
+
(operation_id, machine_id, job_id).
|
125
|
+
"""
|
126
|
+
observation, info = self.env.reset()
|
127
|
+
new_observation = self.observation(observation)
|
128
|
+
new_info = self._info(info)
|
129
|
+
return new_observation, new_info
|
130
|
+
|
131
|
+
def _info(self, info: dict[str, Any]) -> dict[str, Any]:
|
132
|
+
"""Updates the "available_operations_with_ids" key in the info
|
133
|
+
dictionary so that they start from 0 using the
|
134
|
+
`_start_from_zero_mapping` attribute.
|
135
|
+
"""
|
136
|
+
new_available_operations_ids = []
|
137
|
+
for operation_id, machine_id, job_id in info[
|
138
|
+
"available_operations_with_ids"
|
139
|
+
]:
|
140
|
+
if "operation" in self._start_from_zero_mapping:
|
141
|
+
operation_id = self._start_from_zero_mapping["operation"][
|
142
|
+
operation_id
|
143
|
+
]
|
144
|
+
if "machine" in self._start_from_zero_mapping:
|
145
|
+
machine_id = self._start_from_zero_mapping["machine"][
|
146
|
+
machine_id
|
147
|
+
]
|
148
|
+
if "job" in self._start_from_zero_mapping:
|
149
|
+
job_id = self._start_from_zero_mapping["job"][job_id]
|
150
|
+
new_available_operations_ids.append(
|
151
|
+
(operation_id, machine_id, job_id)
|
152
|
+
)
|
153
|
+
info["available_operations_with_ids"] = new_available_operations_ids
|
154
|
+
return info
|
155
|
+
|
156
|
+
def _compute_id_mappings(self) -> dict[int, int]:
|
157
|
+
"""Computes mappings from global node IDs to type-local IDs.
|
158
|
+
|
159
|
+
Returns:
|
160
|
+
A dictionary mapping global node IDs to local node IDs for each
|
161
|
+
node type.
|
162
|
+
"""
|
163
|
+
mappings = {}
|
164
|
+
for node_type in NodeType:
|
165
|
+
type_nodes = self.unwrapped.job_shop_graph.nodes_by_type[node_type]
|
166
|
+
if not type_nodes:
|
167
|
+
continue
|
168
|
+
# Create mapping from global ID to local ID
|
169
|
+
# (0 to len(type_nodes)-1)
|
170
|
+
type_mapping = {
|
171
|
+
node.node_id: local_id
|
172
|
+
for local_id, node in enumerate(type_nodes)
|
173
|
+
}
|
174
|
+
mappings.update(type_mapping)
|
175
|
+
|
176
|
+
return mappings
|
177
|
+
|
178
|
+
def _compute_node_type_ranges(self) -> dict[str, tuple[int, int]]:
|
179
|
+
"""Computes index ranges for each node type.
|
180
|
+
|
181
|
+
Returns:
|
182
|
+
Dictionary mapping node type names to (start, end) index ranges
|
183
|
+
"""
|
184
|
+
type_ranges = {}
|
185
|
+
for node_type in NodeType:
|
186
|
+
type_nodes = self.unwrapped.job_shop_graph.nodes_by_type[node_type]
|
187
|
+
if not type_nodes:
|
188
|
+
continue
|
189
|
+
start = min(node.node_id for node in type_nodes)
|
190
|
+
end = max(node.node_id for node in type_nodes) + 1
|
191
|
+
type_ranges[node_type.name.lower()] = (start, end)
|
192
|
+
|
193
|
+
return type_ranges
|
194
|
+
|
195
|
+
def observation(
|
196
|
+
self, observation: ObservationDict
|
197
|
+
) -> ResourceTaskGraphObservationDict:
|
198
|
+
"""Processes the observation data into the resource task graph format.
|
199
|
+
|
200
|
+
Args:
|
201
|
+
observation: The observation dictionary. It must NOT have padding.
|
202
|
+
|
203
|
+
Returns:
|
204
|
+
A dictionary containing the following keys:
|
205
|
+
|
206
|
+
- "edge_index_dict": A dictionary mapping edge types to edge index
|
207
|
+
arrays.
|
208
|
+
- "node_features_dict": A dictionary mapping node type names to
|
209
|
+
node feature arrays.
|
210
|
+
- "original_ids_dict": A dictionary mapping node type names to the
|
211
|
+
original node IDs before removing nodes.
|
212
|
+
"""
|
213
|
+
edge_index_dict = create_edge_type_dict(
|
214
|
+
observation["edge_index"],
|
215
|
+
type_ranges=self.type_ranges,
|
216
|
+
relationship="to",
|
217
|
+
)
|
218
|
+
node_features_dict = self._create_node_features_dict(observation)
|
219
|
+
node_features_dict, original_ids_dict = self._remove_nodes(
|
220
|
+
node_features_dict, observation["removed_nodes"]
|
221
|
+
)
|
222
|
+
|
223
|
+
# mapping from global node ID to local node ID
|
224
|
+
for key, edge_index in edge_index_dict.items():
|
225
|
+
edge_index_dict[key] = map_values(
|
226
|
+
edge_index, self.global_to_local_id
|
227
|
+
)
|
228
|
+
# mapping so that ids start from 0 in edge index
|
229
|
+
self._start_from_zero_mapping = self._get_start_from_zero_mappings(
|
230
|
+
original_ids_dict
|
231
|
+
)
|
232
|
+
for (type_1, to, type_2), edge_index in edge_index_dict.items():
|
233
|
+
edge_index_dict[(type_1, to, type_2)][0] = map_values(
|
234
|
+
edge_index[0], self._start_from_zero_mapping[type_1]
|
235
|
+
)
|
236
|
+
edge_index_dict[(type_1, to, type_2)][1] = map_values(
|
237
|
+
edge_index[1], self._start_from_zero_mapping[type_2]
|
238
|
+
)
|
239
|
+
|
240
|
+
return {
|
241
|
+
"edge_index_dict": edge_index_dict,
|
242
|
+
"node_features_dict": node_features_dict,
|
243
|
+
"original_ids_dict": original_ids_dict,
|
244
|
+
}
|
245
|
+
|
246
|
+
@staticmethod
|
247
|
+
def _get_start_from_zero_mappings(
|
248
|
+
original_indices_dict: dict[str, NDArray[np.int32]]
|
249
|
+
) -> dict[str, dict[int, int]]:
|
250
|
+
mappings = {}
|
251
|
+
for key, indices in original_indices_dict.items():
|
252
|
+
mappings[key] = {idx: i for i, idx in enumerate(indices)}
|
253
|
+
return mappings
|
254
|
+
|
255
|
+
def _create_node_features_dict(
|
256
|
+
self, observation: ObservationDict
|
257
|
+
) -> dict[str, NDArray]:
|
258
|
+
"""Creates a dictionary of node features for each node type.
|
259
|
+
|
260
|
+
Args:
|
261
|
+
observation: The observation dictionary.
|
262
|
+
|
263
|
+
Returns:
|
264
|
+
Dictionary mapping node type names to node features.
|
265
|
+
"""
|
266
|
+
|
267
|
+
node_features_dict = {}
|
268
|
+
for node_type, feature_type in _NODE_TYPE_TO_FEATURE_TYPE.items():
|
269
|
+
if self.unwrapped.job_shop_graph.nodes_by_type[node_type]:
|
270
|
+
node_features_dict[feature_type.value] = observation[
|
271
|
+
feature_type.value
|
272
|
+
]
|
273
|
+
continue
|
274
|
+
if feature_type != FeatureType.JOBS:
|
275
|
+
continue
|
276
|
+
assert FeatureType.OPERATIONS.value in observation
|
277
|
+
job_features = observation[
|
278
|
+
feature_type.value # type: ignore[literal-required]
|
279
|
+
]
|
280
|
+
job_ids_of_ops = [
|
281
|
+
node.operation.job_id
|
282
|
+
for node in self.unwrapped.job_shop_graph.nodes_by_type[
|
283
|
+
NodeType.OPERATION
|
284
|
+
]
|
285
|
+
]
|
286
|
+
job_features_expanded = job_features[job_ids_of_ops]
|
287
|
+
operation_features = observation[FeatureType.OPERATIONS.value]
|
288
|
+
node_features_dict[FeatureType.OPERATIONS.value] = np.concatenate(
|
289
|
+
(operation_features, job_features_expanded), axis=1
|
290
|
+
)
|
291
|
+
return node_features_dict
|
292
|
+
|
293
|
+
def _remove_nodes(
|
294
|
+
self,
|
295
|
+
node_features_dict: dict[str, NDArray[T]],
|
296
|
+
removed_nodes: NDArray[np.bool_],
|
297
|
+
) -> tuple[dict[str, NDArray[T]], dict[str, NDArray[np.int32]]]:
|
298
|
+
"""Removes nodes from the node features dictionary.
|
299
|
+
|
300
|
+
Args:
|
301
|
+
node_features_dict: The node features dictionary.
|
302
|
+
|
303
|
+
Returns:
|
304
|
+
The node features dictionary with the nodes removed and a
|
305
|
+
dictionary containing the original node ids.
|
306
|
+
"""
|
307
|
+
removed_nodes_dict: dict[str, NDArray[T]] = {}
|
308
|
+
original_ids_dict: dict[str, NDArray[np.int32]] = {}
|
309
|
+
for feature_type, features in node_features_dict.items():
|
310
|
+
node_type = _FEATURE_TYPE_STR_TO_NODE_TYPE[
|
311
|
+
feature_type
|
312
|
+
].name.lower()
|
313
|
+
if node_type not in self.type_ranges:
|
314
|
+
continue
|
315
|
+
start, end = self.type_ranges[node_type]
|
316
|
+
removed_nodes_of_this_type = removed_nodes[start:end]
|
317
|
+
removed_nodes_dict[node_type] = features[
|
318
|
+
~removed_nodes_of_this_type
|
319
|
+
]
|
320
|
+
original_ids_dict[node_type] = np.where(
|
321
|
+
~removed_nodes_of_this_type
|
322
|
+
)[0]
|
323
|
+
|
324
|
+
return removed_nodes_dict, original_ids_dict
|
325
|
+
|
326
|
+
@property
|
327
|
+
def unwrapped(self) -> EnvType:
|
328
|
+
"""Returns the unwrapped environment."""
|
329
|
+
return self.env # type: ignore[return-value]
|
@@ -0,0 +1,87 @@
|
|
1
|
+
"""Rewards functions are defined as `DispatcherObervers` and are used to
|
2
|
+
calculate the reward for a given state."""
|
3
|
+
|
4
|
+
from typing import List
|
5
|
+
|
6
|
+
from job_shop_lib.dispatching import DispatcherObserver, Dispatcher
|
7
|
+
from job_shop_lib import ScheduledOperation
|
8
|
+
|
9
|
+
|
10
|
+
class RewardObserver(DispatcherObserver):
|
11
|
+
"""Base class for all reward functions.
|
12
|
+
|
13
|
+
Attributes:
|
14
|
+
rewards:
|
15
|
+
List of rewards calculated for each operation scheduled by the
|
16
|
+
dispatcher.
|
17
|
+
"""
|
18
|
+
|
19
|
+
def __init__(
|
20
|
+
self, dispatcher: Dispatcher, *, subscribe: bool = True
|
21
|
+
) -> None:
|
22
|
+
super().__init__(dispatcher, subscribe=subscribe)
|
23
|
+
self.rewards: List[float] = []
|
24
|
+
|
25
|
+
@property
|
26
|
+
def last_reward(self) -> float:
|
27
|
+
"""Returns the reward of the last step or 0 if no rewards have been
|
28
|
+
calculated."""
|
29
|
+
return self.rewards[-1] if self.rewards else 0
|
30
|
+
|
31
|
+
def reset(self) -> None:
|
32
|
+
"""Sets rewards attribute to a new empty list."""
|
33
|
+
self.rewards = []
|
34
|
+
|
35
|
+
|
36
|
+
class MakespanReward(RewardObserver):
|
37
|
+
"""Dense reward function based on the negative makespan of the schedule.
|
38
|
+
|
39
|
+
The reward is calculated as the difference between the makespan of the
|
40
|
+
schedule before and after the last operation was scheduled. The makespan
|
41
|
+
is the time at which the last operation is completed.
|
42
|
+
|
43
|
+
Attributes:
|
44
|
+
current_makespan:
|
45
|
+
Makespan of the schedule after the last operation was scheduled.
|
46
|
+
"""
|
47
|
+
|
48
|
+
def __init__(self, dispatcher: Dispatcher, *, subscribe=True) -> None:
|
49
|
+
super().__init__(dispatcher, subscribe=subscribe)
|
50
|
+
self.current_makespan = dispatcher.schedule.makespan()
|
51
|
+
|
52
|
+
def reset(self) -> None:
|
53
|
+
super().reset()
|
54
|
+
self.current_makespan = self.dispatcher.schedule.makespan()
|
55
|
+
|
56
|
+
def update(self, scheduled_operation: ScheduledOperation):
|
57
|
+
last_makespan = self.current_makespan
|
58
|
+
self.current_makespan = max(
|
59
|
+
last_makespan, scheduled_operation.end_time
|
60
|
+
)
|
61
|
+
reward = last_makespan - self.current_makespan
|
62
|
+
self.rewards.append(reward)
|
63
|
+
|
64
|
+
|
65
|
+
class IdleTimeReward(RewardObserver):
|
66
|
+
"""Dense reward function based on the negative idle time of the schedule.
|
67
|
+
|
68
|
+
The reward is calculated as the difference between the idle time of the
|
69
|
+
schedule before and after the last operation was scheduled. The idle time
|
70
|
+
is the sum of the time between the end of the last operation and the start
|
71
|
+
of the next operation.
|
72
|
+
"""
|
73
|
+
|
74
|
+
def update(self, scheduled_operation: ScheduledOperation):
|
75
|
+
machine_id = scheduled_operation.machine_id
|
76
|
+
machine_schedule = self.dispatcher.schedule.schedule[machine_id][:-1]
|
77
|
+
|
78
|
+
if machine_schedule:
|
79
|
+
last_operation = machine_schedule[-1]
|
80
|
+
idle_time = (
|
81
|
+
scheduled_operation.start_time - last_operation.end_time
|
82
|
+
)
|
83
|
+
else:
|
84
|
+
idle_time = scheduled_operation.start_time
|
85
|
+
|
86
|
+
reward = -idle_time
|
87
|
+
self.rewards.append(reward)
|