job-shop-lib 1.0.0b5__py3-none-any.whl → 1.0.1__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 +1 -1
- job_shop_lib/_operation.py +9 -3
- job_shop_lib/_scheduled_operation.py +3 -0
- job_shop_lib/dispatching/_dispatcher.py +6 -13
- job_shop_lib/dispatching/_factories.py +3 -3
- job_shop_lib/dispatching/_optimal_operations_observer.py +0 -2
- job_shop_lib/dispatching/_ready_operation_filters.py +4 -4
- job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +10 -5
- job_shop_lib/dispatching/feature_observers/_factory.py +8 -3
- job_shop_lib/dispatching/feature_observers/_feature_observer.py +1 -1
- job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +35 -67
- job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +1 -1
- job_shop_lib/dispatching/rules/_machine_chooser_factory.py +3 -2
- job_shop_lib/graphs/__init__.py +2 -0
- job_shop_lib/graphs/_build_resource_task_graphs.py +1 -1
- job_shop_lib/graphs/_job_shop_graph.py +38 -19
- job_shop_lib/graphs/graph_updaters/__init__.py +3 -0
- job_shop_lib/graphs/graph_updaters/_disjunctive_graph_updater.py +108 -0
- job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +3 -1
- job_shop_lib/graphs/graph_updaters/_utils.py +2 -2
- job_shop_lib/py.typed +0 -0
- job_shop_lib/reinforcement_learning/__init__.py +4 -0
- job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +1 -1
- job_shop_lib/reinforcement_learning/_resource_task_graph_observation.py +102 -24
- job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +11 -2
- job_shop_lib/reinforcement_learning/_types_and_constants.py +11 -10
- job_shop_lib/reinforcement_learning/_utils.py +29 -0
- job_shop_lib/visualization/gantt/_gantt_chart_video_and_gif_creation.py +5 -2
- job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py +53 -19
- {job_shop_lib-1.0.0b5.dist-info → job_shop_lib-1.0.1.dist-info}/METADATA +4 -10
- {job_shop_lib-1.0.0b5.dist-info → job_shop_lib-1.0.1.dist-info}/RECORD +33 -31
- {job_shop_lib-1.0.0b5.dist-info → job_shop_lib-1.0.1.dist-info}/LICENSE +0 -0
- {job_shop_lib-1.0.0b5.dist-info → job_shop_lib-1.0.1.dist-info}/WHEEL +0 -0
@@ -13,6 +13,8 @@
|
|
13
13
|
RenderConfig
|
14
14
|
add_padding
|
15
15
|
create_edge_type_dict
|
16
|
+
map_values
|
17
|
+
get_optimal_actions
|
16
18
|
ResourceTaskGraphObservation
|
17
19
|
ResourceTaskGraphObservationDict
|
18
20
|
|
@@ -34,6 +36,7 @@ from job_shop_lib.reinforcement_learning._utils import (
|
|
34
36
|
add_padding,
|
35
37
|
create_edge_type_dict,
|
36
38
|
map_values,
|
39
|
+
get_optimal_actions,
|
37
40
|
)
|
38
41
|
|
39
42
|
from job_shop_lib.reinforcement_learning._single_job_shop_graph_env import (
|
@@ -61,4 +64,5 @@ __all__ = [
|
|
61
64
|
"ResourceTaskGraphObservation",
|
62
65
|
"map_values",
|
63
66
|
"ResourceTaskGraphObservationDict",
|
67
|
+
"get_optimal_actions",
|
64
68
|
]
|
@@ -117,7 +117,7 @@ class MultiJobShopGraphEnv(gym.Env):
|
|
117
117
|
graph_initializer:
|
118
118
|
Function to create the initial graph representation.
|
119
119
|
If ``None``, the default graph initializer is used:
|
120
|
-
:func:`~job_shop_lib.graphs.
|
120
|
+
:func:`~job_shop_lib.graphs.build_resource_task_graph`.
|
121
121
|
graph_updater_config:
|
122
122
|
Configuration for the graph updater. The graph updater is used
|
123
123
|
to update the graph representation after each action. If
|
@@ -1,6 +1,6 @@
|
|
1
1
|
"""Contains wrappers for the environments."""
|
2
2
|
|
3
|
-
from typing import TypeVar, TypedDict, Generic
|
3
|
+
from typing import TypeVar, TypedDict, Generic, Any
|
4
4
|
from gymnasium import ObservationWrapper
|
5
5
|
import numpy as np
|
6
6
|
from numpy.typing import NDArray
|
@@ -20,11 +20,22 @@ EnvType = TypeVar( # pylint: disable=invalid-name
|
|
20
20
|
"EnvType", bound=SingleJobShopGraphEnv | MultiJobShopGraphEnv
|
21
21
|
)
|
22
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
|
+
|
23
34
|
|
24
35
|
class ResourceTaskGraphObservationDict(TypedDict):
|
25
36
|
"""Represents a dictionary for resource task graph observations."""
|
26
37
|
|
27
|
-
edge_index_dict: dict[str, NDArray[np.
|
38
|
+
edge_index_dict: dict[tuple[str, str, str], NDArray[np.int32]]
|
28
39
|
node_features_dict: dict[str, NDArray[np.float32]]
|
29
40
|
original_ids_dict: dict[str, NDArray[np.int32]]
|
30
41
|
|
@@ -40,6 +51,12 @@ class ResourceTaskGraphObservation(ObservationWrapper, Generic[EnvType]):
|
|
40
51
|
``node_type_j`` are the node types of the source and target nodes,
|
41
52
|
respectively.
|
42
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
|
+
|
43
60
|
Attributes:
|
44
61
|
global_to_local_id: A dictionary mapping global node IDs to local node
|
45
62
|
IDs for each node type.
|
@@ -55,6 +72,7 @@ class ResourceTaskGraphObservation(ObservationWrapper, Generic[EnvType]):
|
|
55
72
|
self.env = env # Unnecessary, but makes mypy happy
|
56
73
|
self.global_to_local_id = self._compute_id_mappings()
|
57
74
|
self.type_ranges = self._compute_node_type_ranges()
|
75
|
+
self._start_from_zero_mapping: dict[str, dict[int, int]] = {}
|
58
76
|
|
59
77
|
def step(self, action: tuple[int, int]):
|
60
78
|
"""Takes a step in the environment.
|
@@ -80,7 +98,9 @@ class ResourceTaskGraphObservation(ObservationWrapper, Generic[EnvType]):
|
|
80
98
|
machine_id, job_id).
|
81
99
|
"""
|
82
100
|
observation, reward, done, truncated, info = self.env.step(action)
|
83
|
-
|
101
|
+
new_observation = self.observation(observation)
|
102
|
+
new_info = self._info(info)
|
103
|
+
return new_observation, reward, done, truncated, new_info
|
84
104
|
|
85
105
|
def reset(self, *, seed: int | None = None, options: dict | None = None):
|
86
106
|
"""Resets the environment.
|
@@ -104,7 +124,34 @@ class ResourceTaskGraphObservation(ObservationWrapper, Generic[EnvType]):
|
|
104
124
|
(operation_id, machine_id, job_id).
|
105
125
|
"""
|
106
126
|
observation, info = self.env.reset()
|
107
|
-
|
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
|
108
155
|
|
109
156
|
def _compute_id_mappings(self) -> dict[int, int]:
|
110
157
|
"""Computes mappings from global node IDs to type-local IDs.
|
@@ -145,21 +192,50 @@ class ResourceTaskGraphObservation(ObservationWrapper, Generic[EnvType]):
|
|
145
192
|
|
146
193
|
return type_ranges
|
147
194
|
|
148
|
-
def observation(
|
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
|
+
"""
|
149
213
|
edge_index_dict = create_edge_type_dict(
|
150
214
|
observation["edge_index"],
|
151
215
|
type_ranges=self.type_ranges,
|
152
216
|
relationship="to",
|
153
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
|
+
|
154
223
|
# mapping from global node ID to local node ID
|
155
224
|
for key, edge_index in edge_index_dict.items():
|
156
225
|
edge_index_dict[key] = map_values(
|
157
226
|
edge_index, self.global_to_local_id
|
158
227
|
)
|
159
|
-
|
160
|
-
|
161
|
-
|
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
|
162
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
|
+
)
|
163
239
|
|
164
240
|
return {
|
165
241
|
"edge_index_dict": edge_index_dict,
|
@@ -167,6 +243,15 @@ class ResourceTaskGraphObservation(ObservationWrapper, Generic[EnvType]):
|
|
167
243
|
"original_ids_dict": original_ids_dict,
|
168
244
|
}
|
169
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
|
+
|
170
255
|
def _create_node_features_dict(
|
171
256
|
self, observation: ObservationDict
|
172
257
|
) -> dict[str, NDArray]:
|
@@ -178,14 +263,10 @@ class ResourceTaskGraphObservation(ObservationWrapper, Generic[EnvType]):
|
|
178
263
|
Returns:
|
179
264
|
Dictionary mapping node type names to node features.
|
180
265
|
"""
|
181
|
-
|
182
|
-
NodeType.OPERATION: FeatureType.OPERATIONS,
|
183
|
-
NodeType.MACHINE: FeatureType.MACHINES,
|
184
|
-
NodeType.JOB: FeatureType.JOBS,
|
185
|
-
}
|
266
|
+
|
186
267
|
node_features_dict = {}
|
187
|
-
for node_type, feature_type in
|
188
|
-
if
|
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]:
|
189
270
|
node_features_dict[feature_type.value] = observation[
|
190
271
|
feature_type.value
|
191
272
|
]
|
@@ -211,9 +292,9 @@ class ResourceTaskGraphObservation(ObservationWrapper, Generic[EnvType]):
|
|
211
292
|
|
212
293
|
def _remove_nodes(
|
213
294
|
self,
|
214
|
-
node_features_dict: dict[str, NDArray[
|
295
|
+
node_features_dict: dict[str, NDArray[T]],
|
215
296
|
removed_nodes: NDArray[np.bool_],
|
216
|
-
) -> tuple[dict[str, NDArray[
|
297
|
+
) -> tuple[dict[str, NDArray[T]], dict[str, NDArray[np.int32]]]:
|
217
298
|
"""Removes nodes from the node features dictionary.
|
218
299
|
|
219
300
|
Args:
|
@@ -223,15 +304,12 @@ class ResourceTaskGraphObservation(ObservationWrapper, Generic[EnvType]):
|
|
223
304
|
The node features dictionary with the nodes removed and a
|
224
305
|
dictionary containing the original node ids.
|
225
306
|
"""
|
226
|
-
removed_nodes_dict: dict[str, NDArray[
|
307
|
+
removed_nodes_dict: dict[str, NDArray[T]] = {}
|
227
308
|
original_ids_dict: dict[str, NDArray[np.int32]] = {}
|
228
|
-
feature_type_to_node_type = {
|
229
|
-
FeatureType.OPERATIONS.value: NodeType.OPERATION,
|
230
|
-
FeatureType.MACHINES.value: NodeType.MACHINE,
|
231
|
-
FeatureType.JOBS.value: NodeType.JOB,
|
232
|
-
}
|
233
309
|
for feature_type, features in node_features_dict.items():
|
234
|
-
node_type =
|
310
|
+
node_type = _FEATURE_TYPE_STR_TO_NODE_TYPE[
|
311
|
+
feature_type
|
312
|
+
].name.lower()
|
235
313
|
if node_type not in self.type_ranges:
|
236
314
|
continue
|
237
315
|
start, end = self.type_ranges[node_type]
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
from copy import deepcopy
|
4
4
|
from collections.abc import Callable, Sequence
|
5
|
-
from typing import Any, Dict, Tuple, List, Optional, Type
|
5
|
+
from typing import Any, Dict, Tuple, List, Optional, Type, Union
|
6
6
|
|
7
7
|
import matplotlib.pyplot as plt
|
8
8
|
import gymnasium as gym
|
@@ -24,6 +24,8 @@ from job_shop_lib.dispatching import (
|
|
24
24
|
from job_shop_lib.dispatching.feature_observers import (
|
25
25
|
FeatureObserverConfig,
|
26
26
|
CompositeFeatureObserver,
|
27
|
+
FeatureObserver,
|
28
|
+
FeatureObserverType,
|
27
29
|
)
|
28
30
|
from job_shop_lib.visualization.gantt import GanttChartCreator
|
29
31
|
from job_shop_lib.reinforcement_learning import (
|
@@ -137,7 +139,14 @@ class SingleJobShopGraphEnv(gym.Env):
|
|
137
139
|
def __init__(
|
138
140
|
self,
|
139
141
|
job_shop_graph: JobShopGraph,
|
140
|
-
feature_observer_configs: Sequence[
|
142
|
+
feature_observer_configs: Sequence[
|
143
|
+
Union[
|
144
|
+
str,
|
145
|
+
FeatureObserverType,
|
146
|
+
Type[FeatureObserver],
|
147
|
+
FeatureObserverConfig,
|
148
|
+
],
|
149
|
+
],
|
141
150
|
reward_function_config: DispatcherObserverConfig[
|
142
151
|
Type[RewardObserver]
|
143
152
|
] = DispatcherObserverConfig(class_type=MakespanReward),
|
@@ -5,6 +5,7 @@ from enum import Enum
|
|
5
5
|
from typing import TypedDict
|
6
6
|
|
7
7
|
import numpy as np
|
8
|
+
from numpy.typing import NDArray
|
8
9
|
|
9
10
|
from job_shop_lib.dispatching.feature_observers import FeatureType
|
10
11
|
from job_shop_lib.visualization.gantt import (
|
@@ -35,27 +36,27 @@ class ObservationSpaceKey(str, Enum):
|
|
35
36
|
class _ObservationDictRequired(TypedDict):
|
36
37
|
"""Required fields for the observation dictionary."""
|
37
38
|
|
38
|
-
removed_nodes: np.
|
39
|
-
edge_index: np.
|
39
|
+
removed_nodes: NDArray[np.bool_]
|
40
|
+
edge_index: NDArray[np.int32]
|
40
41
|
|
41
42
|
|
42
43
|
class _ObservationDictOptional(TypedDict, total=False):
|
43
44
|
"""Optional fields for the observation dictionary."""
|
44
45
|
|
45
|
-
operations: np.
|
46
|
-
jobs: np.
|
47
|
-
machines: np.
|
46
|
+
operations: NDArray[np.float32]
|
47
|
+
jobs: NDArray[np.float32]
|
48
|
+
machines: NDArray[np.float32]
|
48
49
|
|
49
50
|
|
50
51
|
class ObservationDict(_ObservationDictRequired, _ObservationDictOptional):
|
51
52
|
"""A dictionary containing the observation of the environment.
|
52
53
|
|
53
54
|
Required fields:
|
54
|
-
removed_nodes
|
55
|
-
edge_index
|
55
|
+
removed_nodes: Binary vector indicating removed nodes.
|
56
|
+
edge_index: Edge list in COO format.
|
56
57
|
|
57
58
|
Optional fields:
|
58
|
-
operations
|
59
|
-
jobs
|
60
|
-
machines
|
59
|
+
operations: Matrix of operation features.
|
60
|
+
jobs: Matrix of job features.
|
61
|
+
machines: Matrix of machine features.
|
61
62
|
"""
|
@@ -6,6 +6,7 @@ import numpy as np
|
|
6
6
|
from numpy.typing import NDArray
|
7
7
|
|
8
8
|
from job_shop_lib.exceptions import ValidationError
|
9
|
+
from job_shop_lib.dispatching import OptimalOperationsObserver
|
9
10
|
|
10
11
|
T = TypeVar("T", bound=np.number)
|
11
12
|
|
@@ -164,6 +165,34 @@ def map_values(array: NDArray[T], mapping: dict[int, int]) -> NDArray[T]:
|
|
164
165
|
) from e
|
165
166
|
|
166
167
|
|
168
|
+
def get_optimal_actions(
|
169
|
+
optimal_ops_observer: OptimalOperationsObserver,
|
170
|
+
available_operations_with_ids: list[tuple[int, int, int]],
|
171
|
+
) -> dict[tuple[int, int, int], int]:
|
172
|
+
"""Indicates if each action is optimal according to a
|
173
|
+
:class:`~job_shop_lib.dispatching.OptimalOperationsObserver` instance.
|
174
|
+
|
175
|
+
Args:
|
176
|
+
optimal_ops_observer: The observer that provides optimal operations.
|
177
|
+
available_operations_with_ids: List of available operations with their
|
178
|
+
IDs (operation_id, machine_id, job_id).
|
179
|
+
|
180
|
+
Returns:
|
181
|
+
A dictionary mapping each tuple
|
182
|
+
(operation_id, machine_id, job_id) in the available actions to a binary
|
183
|
+
indicator (1 if optimal, 0 otherwise).
|
184
|
+
"""
|
185
|
+
optimal_actions = {}
|
186
|
+
optimal_ops = optimal_ops_observer.optimal_available
|
187
|
+
optimal_ops_ids = [
|
188
|
+
(op.operation_id, op.machine_id, op.job_id) for op in optimal_ops
|
189
|
+
]
|
190
|
+
for operation_id, machine_id, job_id in available_operations_with_ids:
|
191
|
+
is_optimal = (operation_id, machine_id, job_id) in optimal_ops_ids
|
192
|
+
optimal_actions[(operation_id, machine_id, job_id)] = int(is_optimal)
|
193
|
+
return optimal_actions
|
194
|
+
|
195
|
+
|
167
196
|
if __name__ == "__main__":
|
168
197
|
import doctest
|
169
198
|
|
@@ -3,7 +3,7 @@
|
|
3
3
|
import os
|
4
4
|
import pathlib
|
5
5
|
import shutil
|
6
|
-
from typing import Sequence, Protocol, Optional, List
|
6
|
+
from typing import Sequence, Protocol, Optional, List, Any
|
7
7
|
|
8
8
|
import imageio
|
9
9
|
import matplotlib.pyplot as plt
|
@@ -68,6 +68,7 @@ def get_partial_gantt_chart_plotter(
|
|
68
68
|
title: Optional[str] = None,
|
69
69
|
cmap: str = "viridis",
|
70
70
|
show_available_operations: bool = False,
|
71
|
+
**kwargs: Any,
|
71
72
|
) -> PartialGanttChartPlotter:
|
72
73
|
"""Returns a function that plots a Gantt chart for an unfinished schedule.
|
73
74
|
|
@@ -76,6 +77,8 @@ def get_partial_gantt_chart_plotter(
|
|
76
77
|
cmap: The name of the colormap to use.
|
77
78
|
show_available_operations:
|
78
79
|
Whether to show the available operations in the Gantt chart.
|
80
|
+
**kwargs: Additional keyword arguments to pass to the
|
81
|
+
:func:`plot_gantt_chart` function.
|
79
82
|
|
80
83
|
Returns:
|
81
84
|
A function that plots a Gantt chart for a schedule. The function takes
|
@@ -97,7 +100,7 @@ def get_partial_gantt_chart_plotter(
|
|
97
100
|
current_time: Optional[int] = None,
|
98
101
|
) -> Figure:
|
99
102
|
fig, ax = plot_gantt_chart(
|
100
|
-
schedule, title=title, cmap_name=cmap, xlim=makespan
|
103
|
+
schedule, title=title, cmap_name=cmap, xlim=makespan, **kwargs
|
101
104
|
)
|
102
105
|
|
103
106
|
if show_available_operations and available_operations is not None:
|
@@ -7,6 +7,7 @@ import warnings
|
|
7
7
|
import copy
|
8
8
|
|
9
9
|
import matplotlib
|
10
|
+
import matplotlib.colors
|
10
11
|
import matplotlib.pyplot as plt
|
11
12
|
import networkx as nx
|
12
13
|
from networkx.drawing.nx_agraph import graphviz_layout
|
@@ -66,6 +67,9 @@ def plot_disjunctive_graph(
|
|
66
67
|
alpha: float = 0.95,
|
67
68
|
operation_node_labeler: Callable[[Node], str] = duration_labeler,
|
68
69
|
node_font_color: str = "white",
|
70
|
+
machine_colors: Optional[
|
71
|
+
Dict[int, Tuple[float, float, float, float]]
|
72
|
+
] = None,
|
69
73
|
color_map: str = "Dark2_r",
|
70
74
|
disjunctive_edge_color: str = "red",
|
71
75
|
conjunctive_edge_color: str = "black",
|
@@ -114,6 +118,12 @@ def plot_disjunctive_graph(
|
|
114
118
|
with their duration.
|
115
119
|
node_font_color:
|
116
120
|
The color of the node labels (default is ``"white"``).
|
121
|
+
machine_colors:
|
122
|
+
A dictionary that maps machine ids to colors. If not provided,
|
123
|
+
the colors are generated using the ``color_map``. If provided,
|
124
|
+
the colors are used as the base for the node colors. The
|
125
|
+
dictionary should have the form ``{machine_id: (r, g, b, a)}``.
|
126
|
+
For source and sink nodes use ``-1`` as the machine id.
|
117
127
|
color_map:
|
118
128
|
The color map to use for the nodes (default is ``"Dark2_r"``).
|
119
129
|
disjunctive_edge_color:
|
@@ -229,12 +239,40 @@ def plot_disjunctive_graph(
|
|
229
239
|
|
230
240
|
# Draw nodes
|
231
241
|
# ----------
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
242
|
+
operation_nodes = job_shop_graph.nodes_by_type[NodeType.OPERATION]
|
243
|
+
cmap_func: Optional[matplotlib.colors.Colormap] = None
|
244
|
+
if machine_colors is None:
|
245
|
+
machine_colors = {}
|
246
|
+
cmap_func = matplotlib.colormaps.get_cmap(color_map)
|
247
|
+
remaining_machines = job_shop_graph.instance.num_machines
|
248
|
+
for operation_node in operation_nodes:
|
249
|
+
if job_shop_graph.is_removed(operation_node.node_id):
|
250
|
+
continue
|
251
|
+
machine_id = operation_node.operation.machine_id
|
252
|
+
if machine_id not in machine_colors:
|
253
|
+
machine_colors[machine_id] = cmap_func(
|
254
|
+
(_get_node_color(operation_node) + 1)
|
255
|
+
/ job_shop_graph.instance.num_machines
|
256
|
+
)
|
257
|
+
remaining_machines -= 1
|
258
|
+
if remaining_machines == 0:
|
259
|
+
break
|
260
|
+
node_colors: list[Any] = [
|
261
|
+
_get_node_color(node)
|
262
|
+
for node in job_shop_graph.nodes
|
263
|
+
if not job_shop_graph.is_removed(node.node_id)
|
264
|
+
]
|
265
|
+
else:
|
266
|
+
node_colors = []
|
267
|
+
for node in job_shop_graph.nodes:
|
268
|
+
if job_shop_graph.is_removed(node.node_id):
|
269
|
+
continue
|
270
|
+
if node.node_type == NodeType.OPERATION:
|
271
|
+
machine_id = node.operation.machine_id
|
272
|
+
else:
|
273
|
+
machine_id = -1
|
274
|
+
node_colors.append(machine_colors[machine_id])
|
275
|
+
|
238
276
|
nx.draw_networkx_nodes(
|
239
277
|
job_shop_graph.graph,
|
240
278
|
pos,
|
@@ -292,24 +330,20 @@ def plot_disjunctive_graph(
|
|
292
330
|
|
293
331
|
# Draw node labels
|
294
332
|
# ----------------
|
295
|
-
operation_nodes = job_shop_graph.nodes_by_type[NodeType.OPERATION]
|
296
333
|
labels = {}
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
334
|
+
if job_shop_graph.nodes_by_type[NodeType.SOURCE]:
|
335
|
+
source_node = job_shop_graph.nodes_by_type[NodeType.SOURCE][0]
|
336
|
+
if not job_shop_graph.is_removed(source_node.node_id):
|
337
|
+
labels[source_node] = start_node_label
|
338
|
+
if job_shop_graph.nodes_by_type[NodeType.SINK]:
|
339
|
+
sink_node = job_shop_graph.nodes_by_type[NodeType.SINK][0]
|
340
|
+
# check if the sink node is removed
|
341
|
+
if not job_shop_graph.is_removed(sink_node.node_id):
|
342
|
+
labels[sink_node] = end_node_label
|
303
343
|
for operation_node in operation_nodes:
|
304
344
|
if job_shop_graph.is_removed(operation_node.node_id):
|
305
345
|
continue
|
306
346
|
labels[operation_node] = operation_node_labeler(operation_node)
|
307
|
-
machine_id = operation_node.operation.machine_id
|
308
|
-
if machine_id not in machine_colors:
|
309
|
-
machine_colors[machine_id] = cmap_func(
|
310
|
-
(_get_node_color(operation_node) + 1)
|
311
|
-
/ job_shop_graph.instance.num_machines
|
312
|
-
)
|
313
347
|
|
314
348
|
nx.draw_networkx_labels(
|
315
349
|
job_shop_graph.graph,
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: job-shop-lib
|
3
|
-
Version: 1.0.
|
3
|
+
Version: 1.0.1
|
4
4
|
Summary: An easy-to-use and modular Python library for the Job Shop Scheduling Problem (JSSP)
|
5
5
|
License: MIT
|
6
6
|
Author: Pabloo22
|
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
14
14
|
Provides-Extra: pygraphviz
|
15
|
-
Requires-Dist: gymnasium (>=0.
|
15
|
+
Requires-Dist: gymnasium (>=1.0.0,<2.0.0)
|
16
16
|
Requires-Dist: imageio[ffmpeg] (>=2.34.1,<3.0.0)
|
17
17
|
Requires-Dist: matplotlib (>=3,<4)
|
18
18
|
Requires-Dist: networkx (>=3,<4)
|
@@ -48,7 +48,7 @@ See the [documentation](https://job-shop-lib.readthedocs.io/en/latest/) for more
|
|
48
48
|
|
49
49
|
JobShopLib is distributed on [PyPI](https://pypi.org/project/job-shop-lib/) and it supports Python 3.10+.
|
50
50
|
|
51
|
-
You can install the latest stable version
|
51
|
+
You can install the latest stable version using `pip`:
|
52
52
|
|
53
53
|
```bash
|
54
54
|
pip install job-shop-lib
|
@@ -57,13 +57,7 @@ pip install job-shop-lib
|
|
57
57
|
See [this](https://colab.research.google.com/drive/1XV_Rvq1F2ns6DFG8uNj66q_rcowwTZ4H?usp=sharing) Google Colab notebook for a quick start guide!
|
58
58
|
|
59
59
|
|
60
|
-
|
61
|
-
|
62
|
-
```bash
|
63
|
-
pip install job-shop-lib==1.0.0b5
|
64
|
-
```
|
65
|
-
|
66
|
-
Although this version is not stable and may contain breaking changes in subsequent releases, it is recommended to install it to access the new reinforcement learning environments and familiarize yourself with new changes (see the [latest pull requests](https://github.com/Pabloo22/job_shop_lib/pulls?q=is%3Apr+is%3Aclosed)). There is a [documentation page](https://job-shop-lib.readthedocs.io/en/latest/) for versions 1.0.0a3 and onward.
|
60
|
+
There is a [documentation page](https://job-shop-lib.readthedocs.io/en/latest/) for versions 1.0.0a3 and onward. See see the [latest pull requests](https://github.com/Pabloo22/job_shop_lib/pulls?q=is%3Apr+is%3Aclosed) for the latest changes.
|
67
61
|
|
68
62
|
<!-- end installation -->
|
69
63
|
|