job-shop-lib 1.0.0b3__py3-none-any.whl → 1.0.0b5__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 CHANGED
@@ -19,7 +19,7 @@ from job_shop_lib._schedule import Schedule
19
19
  from job_shop_lib._base_solver import BaseSolver, Solver
20
20
 
21
21
 
22
- __version__ = "1.0.0-b.3"
22
+ __version__ = "1.0.0-b.5"
23
23
 
24
24
  __all__ = [
25
25
  "Operation",
@@ -13,6 +13,8 @@
13
13
  RenderConfig
14
14
  add_padding
15
15
  create_edge_type_dict
16
+ ResourceTaskGraphObservation
17
+ ResourceTaskGraphObservationDict
16
18
 
17
19
  """
18
20
 
@@ -31,6 +33,7 @@ from job_shop_lib.reinforcement_learning._reward_observers import (
31
33
  from job_shop_lib.reinforcement_learning._utils import (
32
34
  add_padding,
33
35
  create_edge_type_dict,
36
+ map_values,
34
37
  )
35
38
 
36
39
  from job_shop_lib.reinforcement_learning._single_job_shop_graph_env import (
@@ -39,6 +42,9 @@ from job_shop_lib.reinforcement_learning._single_job_shop_graph_env import (
39
42
  from job_shop_lib.reinforcement_learning._multi_job_shop_graph_env import (
40
43
  MultiJobShopGraphEnv,
41
44
  )
45
+ from ._resource_task_graph_observation import (
46
+ ResourceTaskGraphObservation, ResourceTaskGraphObservationDict
47
+ )
42
48
 
43
49
 
44
50
  __all__ = [
@@ -52,4 +58,7 @@ __all__ = [
52
58
  "add_padding",
53
59
  "MultiJobShopGraphEnv",
54
60
  "create_edge_type_dict",
61
+ "ResourceTaskGraphObservation",
62
+ "map_values",
63
+ "ResourceTaskGraphObservationDict",
55
64
  ]
@@ -235,13 +235,13 @@ class MultiJobShopGraphEnv(gym.Env):
235
235
  @ready_operations_filter.setter
236
236
  def ready_operations_filter(
237
237
  self,
238
- pruning_function: Callable[
238
+ ready_operations_filter: Callable[
239
239
  [Dispatcher, List[Operation]], List[Operation]
240
240
  ],
241
241
  ) -> None:
242
242
  """Sets the ready operations filter."""
243
243
  self.single_job_shop_graph_env.dispatcher.ready_operations_filter = (
244
- pruning_function
244
+ ready_operations_filter
245
245
  )
246
246
 
247
247
  @property
@@ -0,0 +1,251 @@
1
+ """Contains wrappers for the environments."""
2
+
3
+ from typing import TypeVar, TypedDict, Generic
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
+
24
+ class ResourceTaskGraphObservationDict(TypedDict):
25
+ """Represents a dictionary for resource task graph observations."""
26
+
27
+ edge_index_dict: dict[str, NDArray[np.int64]]
28
+ node_features_dict: dict[str, NDArray[np.float32]]
29
+ original_ids_dict: dict[str, NDArray[np.int32]]
30
+
31
+
32
+ # pylint: disable=line-too-long
33
+ class ResourceTaskGraphObservation(ObservationWrapper, Generic[EnvType]):
34
+ """Observation wrapper that converts an observation following the
35
+ :class:`ObservationDict` format to a format suitable to PyG's
36
+ [`HeteroData`](https://pytorch-geometric.readthedocs.io/en/latest/generated/torch_geometric.data.HeteroData.html).
37
+
38
+ In particular, the ``edge_index`` is converted into a ``edge_index_dict``
39
+ with keys ``(node_type_i, "to", node_type_j)``. The ``node_type_i`` and
40
+ ``node_type_j`` are the node types of the source and target nodes,
41
+ respectively.
42
+
43
+ Attributes:
44
+ global_to_local_id: A dictionary mapping global node IDs to local node
45
+ IDs for each node type.
46
+ type_ranges: A dictionary mapping node type names to (start, end) index
47
+ ranges.
48
+
49
+ Args:
50
+ env: The environment to wrap.
51
+ """
52
+
53
+ def __init__(self, env: EnvType):
54
+ super().__init__(env)
55
+ self.env = env # Unnecessary, but makes mypy happy
56
+ self.global_to_local_id = self._compute_id_mappings()
57
+ self.type_ranges = self._compute_node_type_ranges()
58
+
59
+ def step(self, action: tuple[int, int]):
60
+ """Takes a step in the environment.
61
+
62
+ Args:
63
+ action:
64
+ The action to take. The action is a tuple of two integers
65
+ (job_id, machine_id):
66
+ the job ID and the machine ID in which to schedule the
67
+ operation.
68
+
69
+ Returns:
70
+ A tuple containing the following elements:
71
+
72
+ - The observation of the environment.
73
+ - The reward obtained.
74
+ - Whether the environment is done.
75
+ - Whether the episode was truncated (always False).
76
+ - A dictionary with additional information. The dictionary
77
+ contains the following keys: "feature_names", the names of the
78
+ features in the observation; and "available_operations_with_ids",
79
+ a list of available actions in the form of (operation_id,
80
+ machine_id, job_id).
81
+ """
82
+ observation, reward, done, truncated, info = self.env.step(action)
83
+ return self.observation(observation), reward, done, truncated, info
84
+
85
+ def reset(self, *, seed: int | None = None, options: dict | None = None):
86
+ """Resets the environment.
87
+
88
+ Args:
89
+ seed:
90
+ Added to match the signature of the parent class. It is not
91
+ used in this method.
92
+ options:
93
+ Additional options to pass to the environment. Not used in
94
+ this method.
95
+
96
+ Returns:
97
+ A tuple containing the following elements:
98
+
99
+ - The observation of the environment.
100
+ - A dictionary with additional information, keys
101
+ include: "feature_names", the names of the features in the
102
+ observation; and "available_operations_with_ids", a list of
103
+ available a list of available actions in the form of
104
+ (operation_id, machine_id, job_id).
105
+ """
106
+ observation, info = self.env.reset()
107
+ return self.observation(observation), info
108
+
109
+ def _compute_id_mappings(self) -> dict[int, int]:
110
+ """Computes mappings from global node IDs to type-local IDs.
111
+
112
+ Returns:
113
+ A dictionary mapping global node IDs to local node IDs for each
114
+ node type.
115
+ """
116
+ mappings = {}
117
+ for node_type in NodeType:
118
+ type_nodes = self.unwrapped.job_shop_graph.nodes_by_type[node_type]
119
+ if not type_nodes:
120
+ continue
121
+ # Create mapping from global ID to local ID
122
+ # (0 to len(type_nodes)-1)
123
+ type_mapping = {
124
+ node.node_id: local_id
125
+ for local_id, node in enumerate(type_nodes)
126
+ }
127
+ mappings.update(type_mapping)
128
+
129
+ return mappings
130
+
131
+ def _compute_node_type_ranges(self) -> dict[str, tuple[int, int]]:
132
+ """Computes index ranges for each node type.
133
+
134
+ Returns:
135
+ Dictionary mapping node type names to (start, end) index ranges
136
+ """
137
+ type_ranges = {}
138
+ for node_type in NodeType:
139
+ type_nodes = self.unwrapped.job_shop_graph.nodes_by_type[node_type]
140
+ if not type_nodes:
141
+ continue
142
+ start = min(node.node_id for node in type_nodes)
143
+ end = max(node.node_id for node in type_nodes) + 1
144
+ type_ranges[node_type.name.lower()] = (start, end)
145
+
146
+ return type_ranges
147
+
148
+ def observation(self, observation: ObservationDict):
149
+ edge_index_dict = create_edge_type_dict(
150
+ observation["edge_index"],
151
+ type_ranges=self.type_ranges,
152
+ relationship="to",
153
+ )
154
+ # mapping from global node ID to local node ID
155
+ for key, edge_index in edge_index_dict.items():
156
+ edge_index_dict[key] = map_values(
157
+ edge_index, self.global_to_local_id
158
+ )
159
+ node_features_dict = self._create_node_features_dict(observation)
160
+ node_features_dict, original_ids_dict = self._remove_nodes(
161
+ node_features_dict, observation["removed_nodes"]
162
+ )
163
+
164
+ return {
165
+ "edge_index_dict": edge_index_dict,
166
+ "node_features_dict": node_features_dict,
167
+ "original_ids_dict": original_ids_dict,
168
+ }
169
+
170
+ def _create_node_features_dict(
171
+ self, observation: ObservationDict
172
+ ) -> dict[str, NDArray]:
173
+ """Creates a dictionary of node features for each node type.
174
+
175
+ Args:
176
+ observation: The observation dictionary.
177
+
178
+ Returns:
179
+ Dictionary mapping node type names to node features.
180
+ """
181
+ node_type_to_feature_type = {
182
+ NodeType.OPERATION: FeatureType.OPERATIONS,
183
+ NodeType.MACHINE: FeatureType.MACHINES,
184
+ NodeType.JOB: FeatureType.JOBS,
185
+ }
186
+ node_features_dict = {}
187
+ for node_type, feature_type in node_type_to_feature_type.items():
188
+ if node_type in self.unwrapped.job_shop_graph.nodes_by_type:
189
+ node_features_dict[feature_type.value] = observation[
190
+ feature_type.value
191
+ ]
192
+ continue
193
+ if feature_type != FeatureType.JOBS:
194
+ continue
195
+ assert FeatureType.OPERATIONS.value in observation
196
+ job_features = observation[
197
+ feature_type.value # type: ignore[literal-required]
198
+ ]
199
+ job_ids_of_ops = [
200
+ node.operation.job_id
201
+ for node in self.unwrapped.job_shop_graph.nodes_by_type[
202
+ NodeType.OPERATION
203
+ ]
204
+ ]
205
+ job_features_expanded = job_features[job_ids_of_ops]
206
+ operation_features = observation[FeatureType.OPERATIONS.value]
207
+ node_features_dict[FeatureType.OPERATIONS.value] = np.concatenate(
208
+ (operation_features, job_features_expanded), axis=1
209
+ )
210
+ return node_features_dict
211
+
212
+ def _remove_nodes(
213
+ self,
214
+ node_features_dict: dict[str, NDArray[np.float32]],
215
+ removed_nodes: NDArray[np.bool_],
216
+ ) -> tuple[dict[str, NDArray[np.float32]], dict[str, NDArray[np.int32]]]:
217
+ """Removes nodes from the node features dictionary.
218
+
219
+ Args:
220
+ node_features_dict: The node features dictionary.
221
+
222
+ Returns:
223
+ The node features dictionary with the nodes removed and a
224
+ dictionary containing the original node ids.
225
+ """
226
+ removed_nodes_dict: dict[str, NDArray[np.float32]] = {}
227
+ 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
+ for feature_type, features in node_features_dict.items():
234
+ node_type = feature_type_to_node_type[feature_type].name.lower()
235
+ if node_type not in self.type_ranges:
236
+ continue
237
+ start, end = self.type_ranges[node_type]
238
+ removed_nodes_of_this_type = removed_nodes[start:end]
239
+ removed_nodes_dict[node_type] = features[
240
+ ~removed_nodes_of_this_type
241
+ ]
242
+ original_ids_dict[node_type] = np.where(
243
+ ~removed_nodes_of_this_type
244
+ )[0]
245
+
246
+ return removed_nodes_dict, original_ids_dict
247
+
248
+ @property
249
+ def unwrapped(self) -> EnvType:
250
+ """Returns the unwrapped environment."""
251
+ return self.env # type: ignore[return-value]
@@ -243,8 +243,27 @@ class SingleJobShopGraphEnv(gym.Env):
243
243
  *,
244
244
  seed: Optional[int] = None,
245
245
  options: Optional[Dict[str, Any]] = None,
246
- ) -> Tuple[ObservationDict, dict]:
247
- """Resets the environment."""
246
+ ) -> Tuple[ObservationDict, dict[str, Any]]:
247
+ """Resets the environment.
248
+
249
+ Args:
250
+ seed:
251
+ Added to match the signature of the parent class. It is not
252
+ used in this method.
253
+ options:
254
+ Additional options to pass to the environment. Not used in
255
+ this method.
256
+
257
+ Returns:
258
+ A tuple containing the following elements:
259
+
260
+ - The observation of the environment.
261
+ - A dictionary with additional information, keys
262
+ include: "feature_names", the names of the features in the
263
+ observation; and "available_operations_with_ids", a list of
264
+ available a list of available actions in the form of
265
+ (operation_id, machine_id, job_id).
266
+ """
248
267
  super().reset(seed=seed, options=options)
249
268
  self.dispatcher.reset()
250
269
  obs = self.get_observation()
@@ -1,6 +1,6 @@
1
1
  """Utility functions for reinforcement learning."""
2
2
 
3
- from typing import TypeVar, Any, Type, Literal
3
+ from typing import TypeVar, Any, Type
4
4
 
5
5
  import numpy as np
6
6
  from numpy.typing import NDArray
@@ -91,8 +91,10 @@ def add_padding(
91
91
 
92
92
 
93
93
  def create_edge_type_dict(
94
- edge_index: NDArray[T], type_ranges: dict[str, tuple[int, int]]
95
- ) -> dict[tuple[str, Literal["to"], str], NDArray[T]]:
94
+ edge_index: NDArray[T],
95
+ type_ranges: dict[str, tuple[int, int]],
96
+ relationship: str = "to",
97
+ ) -> dict[tuple[str, str, str], NDArray[T]]:
96
98
  """Organizes edges based on node types.
97
99
 
98
100
  Args:
@@ -101,17 +103,19 @@ def create_edge_type_dict(
101
103
  type_ranges: dict[str, tuple[int, int]]
102
104
  Dictionary mapping type names to their corresponding index ranges
103
105
  [start, end) in the ``edge_index`` array.
106
+ relationship:
107
+ A string representing the relationship type between nodes.
104
108
 
105
109
  Returns:
106
- A dictionary with keys (type_i, "to", type_j) and values as edge
107
- indices
110
+ A dictionary with keys (type_i, relationship, type_j) and values as
111
+ edge indices
108
112
  """
109
- edge_index_dict: dict[tuple[str, Literal["to"], str], NDArray] = {}
113
+ edge_index_dict: dict[tuple[str, str, str], NDArray] = {}
110
114
  for type_name_i, (start_i, end_i) in type_ranges.items():
111
115
  for type_name_j, (start_j, end_j) in type_ranges.items():
112
- key: tuple[str, Literal["to"], str] = (
116
+ key: tuple[str, str, str] = (
113
117
  type_name_i,
114
- "to",
118
+ relationship,
115
119
  type_name_j,
116
120
  )
117
121
  # Find edges where source is in type_i and target is in type_j
@@ -126,6 +130,40 @@ def create_edge_type_dict(
126
130
  return edge_index_dict
127
131
 
128
132
 
133
+ def map_values(array: NDArray[T], mapping: dict[int, int]) -> NDArray[T]:
134
+ """Maps values in an array using a mapping.
135
+
136
+ Args:
137
+ array:
138
+ An NumPy array.
139
+
140
+ Returns:
141
+ A NumPy array where each element has been replaced by its
142
+ corresponding value from the mapping.
143
+
144
+ Raises:
145
+ ValidationError:
146
+ If the array contains values that are not in the mapping.
147
+
148
+ Examples:
149
+ >>> map_values(np.array([1, 2, 3]), {1: 10, 2: 20, 3: 30})
150
+ array([10, 20, 30])
151
+
152
+ >>> map_values(np.array([1, 2]), {1: 10, 2: 10, 3: 30})
153
+ array([10, 10])
154
+
155
+ """
156
+ if array.size == 0:
157
+ return array
158
+ try:
159
+ vectorized_mapping = np.vectorize(mapping.get)
160
+ return vectorized_mapping(array)
161
+ except TypeError as e:
162
+ raise ValidationError(
163
+ "The array contains values that are not in the mapping."
164
+ ) from e
165
+
166
+
129
167
  if __name__ == "__main__":
130
168
  import doctest
131
169
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: job-shop-lib
3
- Version: 1.0.0b3
3
+ Version: 1.0.0b5
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
@@ -60,7 +60,7 @@ See [this](https://colab.research.google.com/drive/1XV_Rvq1F2ns6DFG8uNj66q_rcoww
60
60
  Version 1.0.0 is currently in beta stage and can be installed with:
61
61
 
62
62
  ```bash
63
- pip install job-shop-lib==1.0.0b3
63
+ pip install job-shop-lib==1.0.0b5
64
64
  ```
65
65
 
66
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.
@@ -1,4 +1,4 @@
1
- job_shop_lib/__init__.py,sha256=fj4EeaNf1NTAbpB0EH9OnhS_MjP1MCe03Xr6JNrLC_g,643
1
+ job_shop_lib/__init__.py,sha256=3Ip52vXCTCs_JAhhNCpSs1Q2omWPbPzKzM1g47mO5eA,643
2
2
  job_shop_lib/_base_solver.py,sha256=p17XmtufNc9Y481cqZUT45pEkUmmW1HWG53dfhIBJH8,1363
3
3
  job_shop_lib/_job_shop_instance.py,sha256=hNQGSJj0rEQpS-YhzwWmM6QzCWp6r--89jkghSgLvUs,18380
4
4
  job_shop_lib/_operation.py,sha256=hx2atpP8LPj9fvxpZIfhBFr9Uq6JP-MKAX5JzTvFXso,3847
@@ -50,12 +50,13 @@ job_shop_lib/graphs/graph_updaters/__init__.py,sha256=UhnZL55e3cAv7hVetB6bRmIOn8
50
50
  job_shop_lib/graphs/graph_updaters/_graph_updater.py,sha256=j1f7iWsa62GVszK2BPaMxnKBCEGWa9owm8g4VWUje8w,1967
51
51
  job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py,sha256=SfgmDyMwfW56OBjJPaU76c42IsX5qx9j-eMtrv0DjKk,6047
52
52
  job_shop_lib/graphs/graph_updaters/_utils.py,sha256=X5YfwJA1CCgpm1r9C036Gal2CkDh2SSak7wl7TbdjHw,704
53
- job_shop_lib/reinforcement_learning/__init__.py,sha256=IohAO2-eAdgeAsNG2czRhI5Eu9jKPnDnB8Z-ne_L1as,1124
54
- job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py,sha256=ib1Y6cItVvId4PfesiQ0XKbh9y6h8LVhD0gYDO4wSlk,15732
53
+ job_shop_lib/reinforcement_learning/__init__.py,sha256=opqJyVJ6VPyeaQOQr4hmUTkiUAXOi5tbyCnuNw5jLTI,1421
54
+ job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py,sha256=memQefVWqatRNodt8hBXVvFcgQKJRmnuB8AWqiDl8_k,15746
55
+ job_shop_lib/reinforcement_learning/_resource_task_graph_observation.py,sha256=ROu_oTsjLvuU5cdvZ2A8D5HWlEZ3NfPtjNWJYH5ilJA,9648
55
56
  job_shop_lib/reinforcement_learning/_reward_observers.py,sha256=iWHccnujeAKyTQn2ilQ4BhcEccoSTyJqQ5yOiP5GG_Y,2984
56
- job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py,sha256=DZnXeXmzMGKq-vwFhxukmSDN1UyrkUfbnjpjFtC9_Bs,15845
57
+ job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py,sha256=3mljeI4k9haLDuDZZhv4NcLpb1_xOlQNdEqMoRsh6bw,16592
57
58
  job_shop_lib/reinforcement_learning/_types_and_constants.py,sha256=xozdM_Wabdbe9e1a769p5980OSNBwQqc9yyaSGW2ODQ,1743
58
- job_shop_lib/reinforcement_learning/_utils.py,sha256=PZWUZVc_Du90PYvKeTxe0dLkCuK3KwQ3D5I54kk5x0U,3808
59
+ job_shop_lib/reinforcement_learning/_utils.py,sha256=giikAj9Xl2f_cYq_AKCrgHn79TRLfGhYNqXz7bEnKuk,4827
59
60
  job_shop_lib/visualization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
60
61
  job_shop_lib/visualization/gantt/__init__.py,sha256=HGXwRgDuMAldqU0JBdiZCd5e79XBz1r96qHeDVlzE54,1145
61
62
  job_shop_lib/visualization/gantt/_gantt_chart_creator.py,sha256=LTsVhpB1Fb_2o08HRZPPXSekwzR7fyTSC6h549XMqhU,8638
@@ -64,7 +65,7 @@ job_shop_lib/visualization/gantt/_plot_gantt_chart.py,sha256=9-NSSNsVcW8gYLZtAuF
64
65
  job_shop_lib/visualization/graphs/__init__.py,sha256=282hZFg07EyQu4HVt4GzFfYnY6ZF376IMjnWZ5eg0ZQ,611
65
66
  job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py,sha256=4VBMYiFXXkCGSnGYN9iqNtWrbLJQxAMHojPHhAbdA0s,14387
66
67
  job_shop_lib/visualization/graphs/_plot_resource_task_graph.py,sha256=RgJqHS5hJh3KkyaLbtpG_bER981BFRwGpflz7I7gS64,13271
67
- job_shop_lib-1.0.0b3.dist-info/LICENSE,sha256=9mggivMGd5taAu3xbmBway-VQZMBzurBGHofFopvUsQ,1069
68
- job_shop_lib-1.0.0b3.dist-info/METADATA,sha256=m-NzkaDXbblEXE7O-iQEYOdQQCPDvj6LiRcJKqenpHg,16424
69
- job_shop_lib-1.0.0b3.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
70
- job_shop_lib-1.0.0b3.dist-info/RECORD,,
68
+ job_shop_lib-1.0.0b5.dist-info/LICENSE,sha256=9mggivMGd5taAu3xbmBway-VQZMBzurBGHofFopvUsQ,1069
69
+ job_shop_lib-1.0.0b5.dist-info/METADATA,sha256=NxGDA_C8w6GQlV1zcrkIawyHekRWR8VTvNwQR9s7Ci4,16424
70
+ job_shop_lib-1.0.0b5.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
71
+ job_shop_lib-1.0.0b5.dist-info/RECORD,,