job-shop-lib 1.0.0a5__py3-none-any.whl → 1.0.0b2__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. job_shop_lib/__init__.py +1 -1
  2. job_shop_lib/_job_shop_instance.py +34 -29
  3. job_shop_lib/_operation.py +4 -2
  4. job_shop_lib/_schedule.py +11 -11
  5. job_shop_lib/benchmarking/_load_benchmark.py +3 -3
  6. job_shop_lib/constraint_programming/_ortools_solver.py +6 -6
  7. job_shop_lib/dispatching/__init__.py +4 -3
  8. job_shop_lib/dispatching/_dispatcher.py +19 -19
  9. job_shop_lib/dispatching/_dispatcher_observer_config.py +4 -4
  10. job_shop_lib/dispatching/_factories.py +4 -2
  11. job_shop_lib/dispatching/_history_observer.py +2 -1
  12. job_shop_lib/dispatching/_optimal_operations_observer.py +115 -0
  13. job_shop_lib/dispatching/_ready_operation_filters.py +19 -18
  14. job_shop_lib/dispatching/_unscheduled_operations_observer.py +4 -3
  15. job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +7 -8
  16. job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +3 -1
  17. job_shop_lib/dispatching/feature_observers/_factory.py +13 -14
  18. job_shop_lib/dispatching/feature_observers/_feature_observer.py +9 -8
  19. job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +2 -1
  20. job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +4 -2
  21. job_shop_lib/dispatching/rules/__init__.py +37 -1
  22. job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +4 -2
  23. job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +50 -20
  24. job_shop_lib/dispatching/rules/_dispatching_rules_functions.py +9 -8
  25. job_shop_lib/dispatching/rules/_machine_chooser_factory.py +4 -3
  26. job_shop_lib/dispatching/rules/_utils.py +9 -8
  27. job_shop_lib/generation/__init__.py +8 -0
  28. job_shop_lib/generation/_general_instance_generator.py +42 -64
  29. job_shop_lib/generation/_instance_generator.py +11 -7
  30. job_shop_lib/generation/_transformations.py +5 -4
  31. job_shop_lib/generation/_utils.py +124 -0
  32. job_shop_lib/graphs/__init__.py +7 -7
  33. job_shop_lib/graphs/{_build_agent_task_graph.py → _build_resource_task_graphs.py} +26 -24
  34. job_shop_lib/graphs/_job_shop_graph.py +17 -13
  35. job_shop_lib/graphs/_node.py +6 -4
  36. job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +4 -2
  37. job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +40 -20
  38. job_shop_lib/reinforcement_learning/_reward_observers.py +3 -1
  39. job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +89 -22
  40. job_shop_lib/reinforcement_learning/_types_and_constants.py +1 -1
  41. job_shop_lib/reinforcement_learning/_utils.py +3 -3
  42. job_shop_lib/visualization/__init__.py +0 -60
  43. job_shop_lib/visualization/gantt/__init__.py +48 -0
  44. job_shop_lib/visualization/{_gantt_chart_creator.py → gantt/_gantt_chart_creator.py} +12 -12
  45. job_shop_lib/visualization/{_gantt_chart_video_and_gif_creation.py → gantt/_gantt_chart_video_and_gif_creation.py} +22 -22
  46. job_shop_lib/visualization/{_plot_gantt_chart.py → gantt/_plot_gantt_chart.py} +12 -13
  47. job_shop_lib/visualization/graphs/__init__.py +29 -0
  48. job_shop_lib/visualization/{_plot_disjunctive_graph.py → graphs/_plot_disjunctive_graph.py} +18 -16
  49. job_shop_lib/visualization/graphs/_plot_resource_task_graph.py +389 -0
  50. {job_shop_lib-1.0.0a5.dist-info → job_shop_lib-1.0.0b2.dist-info}/METADATA +21 -15
  51. job_shop_lib-1.0.0b2.dist-info/RECORD +70 -0
  52. job_shop_lib/visualization/_plot_agent_task_graph.py +0 -276
  53. job_shop_lib-1.0.0a5.dist-info/RECORD +0 -66
  54. {job_shop_lib-1.0.0a5.dist-info → job_shop_lib-1.0.0b2.dist-info}/LICENSE +0 -0
  55. {job_shop_lib-1.0.0a5.dist-info → job_shop_lib-1.0.0b2.dist-info}/WHEEL +0 -0
@@ -1,5 +1,6 @@
1
1
  """Home of the `JobShopGraph` class."""
2
2
 
3
+ from typing import List, Union, Dict
3
4
  import collections
4
5
  import networkx as nx
5
6
 
@@ -62,23 +63,23 @@ class JobShopGraph:
62
63
  self.graph = nx.DiGraph()
63
64
  self.instance = instance
64
65
 
65
- self._nodes: list[Node] = []
66
- self._nodes_by_type: dict[NodeType, list[Node]] = (
66
+ self._nodes: List[Node] = []
67
+ self._nodes_by_type: Dict[NodeType, List[Node]] = (
67
68
  collections.defaultdict(list)
68
69
  )
69
- self._nodes_by_machine: list[list[Node]] = [
70
+ self._nodes_by_machine: List[List[Node]] = [
70
71
  [] for _ in range(instance.num_machines)
71
72
  ]
72
- self._nodes_by_job: list[list[Node]] = [
73
+ self._nodes_by_job: List[List[Node]] = [
73
74
  [] for _ in range(instance.num_jobs)
74
75
  ]
75
76
  self._next_node_id = 0
76
- self.removed_nodes: list[bool] = []
77
+ self.removed_nodes: List[bool] = []
77
78
  if add_operation_nodes:
78
79
  self.add_operation_nodes()
79
80
 
80
81
  @property
81
- def nodes(self) -> list[Node]:
82
+ def nodes(self) -> List[Node]:
82
83
  """List of all nodes added to the graph.
83
84
 
84
85
  It may contain nodes that have been removed from the graph.
@@ -86,7 +87,7 @@ class JobShopGraph:
86
87
  return self._nodes
87
88
 
88
89
  @property
89
- def nodes_by_type(self) -> dict[NodeType, list[Node]]:
90
+ def nodes_by_type(self) -> Dict[NodeType, List[Node]]:
90
91
  """Dictionary mapping node types to lists of nodes.
91
92
 
92
93
  It may contain nodes that have been removed from the graph.
@@ -94,7 +95,7 @@ class JobShopGraph:
94
95
  return self._nodes_by_type
95
96
 
96
97
  @property
97
- def nodes_by_machine(self) -> list[list[Node]]:
98
+ def nodes_by_machine(self) -> List[List[Node]]:
98
99
  """List of lists mapping machine ids to operation nodes.
99
100
 
100
101
  It may contain nodes that have been removed from the graph.
@@ -102,7 +103,7 @@ class JobShopGraph:
102
103
  return self._nodes_by_machine
103
104
 
104
105
  @property
105
- def nodes_by_job(self) -> list[list[Node]]:
106
+ def nodes_by_job(self) -> List[List[Node]]:
106
107
  """List of lists mapping job ids to operation nodes.
107
108
 
108
109
  It may contain nodes that have been removed from the graph.
@@ -163,7 +164,10 @@ class JobShopGraph:
163
164
  self._nodes_by_machine[machine_id].append(node_for_adding)
164
165
 
165
166
  def add_edge(
166
- self, u_of_edge: Node | int, v_of_edge: Node | int, **attr
167
+ self,
168
+ u_of_edge: Union[Node, int],
169
+ v_of_edge: Union[Node, int],
170
+ **attr,
167
171
  ) -> None:
168
172
  """Adds an edge to the graph.
169
173
 
@@ -177,7 +181,7 @@ class JobShopGraph:
177
181
  **attr: Additional attributes to be added to the edge.
178
182
 
179
183
  Raises:
180
- JobShopLibError: If `u_of_edge` or `v_of_edge` are not in the
184
+ ValidationError: If `u_of_edge` or `v_of_edge` are not in the
181
185
  graph.
182
186
  """
183
187
  if isinstance(u_of_edge, Node):
@@ -206,7 +210,7 @@ class JobShopGraph:
206
210
 
207
211
  self.graph.remove_nodes_from(isolated_nodes)
208
212
 
209
- def is_removed(self, node: int | Node) -> bool:
213
+ def is_removed(self, node: Union[int, Node]) -> bool:
210
214
  """Returns whether the node is removed from the graph.
211
215
 
212
216
  Args:
@@ -218,7 +222,7 @@ class JobShopGraph:
218
222
  node = node.node_id
219
223
  return self.removed_nodes[node]
220
224
 
221
- def non_removed_nodes(self) -> list[Node]:
225
+ def non_removed_nodes(self) -> List[Node]:
222
226
  """Returns the nodes that are not removed from the graph."""
223
227
  return [node for node in self._nodes if not self.is_removed(node)]
224
228
 
@@ -1,5 +1,7 @@
1
1
  """Home of the `Node` class."""
2
2
 
3
+ from typing import Optional
4
+
3
5
  from job_shop_lib import Operation
4
6
  from job_shop_lib.exceptions import (
5
7
  UninitializedAttributeError,
@@ -80,9 +82,9 @@ class Node:
80
82
  def __init__(
81
83
  self,
82
84
  node_type: NodeType,
83
- operation: Operation | None = None,
84
- machine_id: int | None = None,
85
- job_id: int | None = None,
85
+ operation: Optional[Operation] = None,
86
+ machine_id: Optional[int] = None,
87
+ job_id: Optional[int] = None,
86
88
  ):
87
89
  if node_type == NodeType.OPERATION and operation is None:
88
90
  raise ValidationError("Operation node must have an operation.")
@@ -94,7 +96,7 @@ class Node:
94
96
  raise ValidationError("Job node must have a job_id.")
95
97
 
96
98
  self.node_type: NodeType = node_type
97
- self._node_id: int | None = None
99
+ self._node_id: Optional[int] = None
98
100
 
99
101
  self._operation = operation
100
102
  self._machine_id = machine_id
@@ -1,5 +1,7 @@
1
1
  """Home of the `ResidualGraphUpdater` class."""
2
2
 
3
+ from typing import Optional, List
4
+
3
5
  from job_shop_lib import ScheduledOperation
4
6
  from job_shop_lib.exceptions import UninitializedAttributeError
5
7
  from job_shop_lib.graphs import NodeType, JobShopGraph
@@ -54,7 +56,7 @@ class ResidualGraphUpdater(GraphUpdater):
54
56
  remove_completed_machine_nodes: bool = True,
55
57
  remove_completed_job_nodes: bool = True,
56
58
  ):
57
- self._is_completed_observer: None | IsCompletedObserver = None
59
+ self._is_completed_observer: Optional[IsCompletedObserver] = None
58
60
  self.remove_completed_job_nodes = remove_completed_job_nodes
59
61
  self.remove_completed_machine_nodes = remove_completed_machine_nodes
60
62
  self._initialize_is_completed_observer_attribute(dispatcher)
@@ -80,7 +82,7 @@ class ResidualGraphUpdater(GraphUpdater):
80
82
  return False
81
83
  return True
82
84
 
83
- feature_types: list[FeatureType] = []
85
+ feature_types: List[FeatureType] = []
84
86
  if self.remove_completed_machine_nodes:
85
87
  feature_types.append(FeatureType.MACHINES)
86
88
  if self.remove_completed_job_nodes:
@@ -2,7 +2,7 @@
2
2
 
3
3
  from collections import defaultdict
4
4
  from collections.abc import Callable, Sequence
5
- from typing import Any
5
+ from typing import Any, Tuple, Dict, List, Optional, Type
6
6
  from copy import deepcopy
7
7
 
8
8
  import gymnasium as gym
@@ -16,7 +16,7 @@ from job_shop_lib.dispatching import (
16
16
  )
17
17
  from job_shop_lib.dispatching.feature_observers import FeatureObserverConfig
18
18
  from job_shop_lib.generation import InstanceGenerator
19
- from job_shop_lib.graphs import JobShopGraph, build_agent_task_graph
19
+ from job_shop_lib.graphs import JobShopGraph, build_resource_task_graph
20
20
  from job_shop_lib.graphs.graph_updaters import (
21
21
  GraphUpdater,
22
22
  ResidualGraphUpdater,
@@ -160,18 +160,18 @@ class MultiJobShopGraphEnv(gym.Env):
160
160
  feature_observer_configs: Sequence[FeatureObserverConfig],
161
161
  graph_initializer: Callable[
162
162
  [JobShopInstance], JobShopGraph
163
- ] = build_agent_task_graph,
163
+ ] = build_resource_task_graph,
164
164
  graph_updater_config: DispatcherObserverConfig[
165
- type[GraphUpdater]
165
+ Type[GraphUpdater]
166
166
  ] = DispatcherObserverConfig(class_type=ResidualGraphUpdater),
167
167
  ready_operations_filter: Callable[
168
- [Dispatcher, list[Operation]], list[Operation]
168
+ [Dispatcher, List[Operation]], List[Operation]
169
169
  ] = filter_dominated_operations,
170
170
  reward_function_config: DispatcherObserverConfig[
171
- type[RewardObserver]
171
+ Type[RewardObserver]
172
172
  ] = DispatcherObserverConfig(class_type=MakespanReward),
173
- render_mode: str | None = None,
174
- render_config: RenderConfig | None = None,
173
+ render_mode: Optional[str] = None,
174
+ render_config: Optional[RenderConfig] = None,
175
175
  use_padding: bool = True,
176
176
  ) -> None:
177
177
  super().__init__()
@@ -226,7 +226,7 @@ class MultiJobShopGraphEnv(gym.Env):
226
226
  @property
227
227
  def ready_operations_filter(
228
228
  self,
229
- ) -> Callable[[Dispatcher, list[Operation]], list[Operation]] | None:
229
+ ) -> Optional[Callable[[Dispatcher, List[Operation]], List[Operation]]]:
230
230
  """Returns the current ready operations filter."""
231
231
  return (
232
232
  self.single_job_shop_graph_env.dispatcher.ready_operations_filter
@@ -236,7 +236,7 @@ class MultiJobShopGraphEnv(gym.Env):
236
236
  def ready_operations_filter(
237
237
  self,
238
238
  pruning_function: Callable[
239
- [Dispatcher, list[Operation]], list[Operation]
239
+ [Dispatcher, List[Operation]], List[Operation]
240
240
  ],
241
241
  ) -> None:
242
242
  """Sets the ready operations filter."""
@@ -267,9 +267,9 @@ class MultiJobShopGraphEnv(gym.Env):
267
267
  def reset(
268
268
  self,
269
269
  *,
270
- seed: int | None = None,
271
- options: dict[str, Any] | None = None,
272
- ) -> tuple[ObservationDict, dict]:
270
+ seed: Optional[int] = None,
271
+ options: Dict[str, Any] | None = None,
272
+ ) -> Tuple[ObservationDict, Dict[str, Any]]:
273
273
  """Resets the environment and returns the initial observation.
274
274
 
275
275
  Args:
@@ -303,8 +303,8 @@ class MultiJobShopGraphEnv(gym.Env):
303
303
  return obs, info
304
304
 
305
305
  def step(
306
- self, action: tuple[int, int]
307
- ) -> tuple[ObservationDict, float, bool, bool, dict]:
306
+ self, action: Tuple[int, int]
307
+ ) -> Tuple[ObservationDict, float, bool, bool, Dict[str, Any]]:
308
308
  """Takes a step in the environment.
309
309
 
310
310
  Args:
@@ -322,9 +322,10 @@ class MultiJobShopGraphEnv(gym.Env):
322
322
  - Whether the environment is done.
323
323
  - Whether the episode was truncated (always False).
324
324
  - A dictionary with additional information. The dictionary
325
- contains the following keys: ``"feature_names"``, The names of
326
- the features in the observation; ``"available_operations"``, the
327
- operations that are ready to be scheduled.
325
+ contains the following keys: "feature_names", the names of the
326
+ features in the observation; and "available_operations_with_ids",
327
+ a list of available actions in the form of (operation_id,
328
+ machine_id, job_id).
328
329
  """
329
330
  obs, reward, done, truncated, info = (
330
331
  self.single_job_shop_graph_env.step(action)
@@ -355,7 +356,7 @@ class MultiJobShopGraphEnv(gym.Env):
355
356
  input_shape: (num_machines, num_features)
356
357
  output_shape: (max_num_machines, num_features) (padded with -1)
357
358
  """
358
- padding_value: dict[str, float | bool] = defaultdict(lambda: -1)
359
+ padding_value: Dict[str, float | bool] = defaultdict(lambda: -1)
359
360
  padding_value[ObservationSpaceKey.REMOVED_NODES.value] = True
360
361
  for key, value in observation.items():
361
362
  if not isinstance(value, np.ndarray): # Make mypy happy
@@ -368,7 +369,7 @@ class MultiJobShopGraphEnv(gym.Env):
368
369
  )
369
370
  return observation
370
371
 
371
- def _get_output_shape(self, key: str) -> tuple[int, ...]:
372
+ def _get_output_shape(self, key: str) -> Tuple[int, ...]:
372
373
  """Returns the output shape of the observation space key."""
373
374
  output_shape = self.observation_space[key].shape
374
375
  assert output_shape is not None # Make mypy happy
@@ -376,3 +377,22 @@ class MultiJobShopGraphEnv(gym.Env):
376
377
 
377
378
  def render(self) -> None:
378
379
  self.single_job_shop_graph_env.render()
380
+
381
+ def get_available_actions_with_ids(self) -> List[Tuple[int, int, int]]:
382
+ """Returns a list of available actions in the form of
383
+ (operation_id, machine_id, job_id)."""
384
+ return self.single_job_shop_graph_env.get_available_actions_with_ids()
385
+
386
+ def validate_action(self, action: Tuple[int, int]) -> None:
387
+ """Validates the action.
388
+
389
+ Args:
390
+ action:
391
+ The action to validate. The action is a tuple of two integers
392
+ (job_id, machine_id): the job ID and the machine ID in which
393
+ to schedule the operation.
394
+
395
+ Raises:
396
+ ValidationError: If the action is invalid.
397
+ """
398
+ self.single_job_shop_graph_env.validate_action(action)
@@ -1,6 +1,8 @@
1
1
  """Rewards functions are defined as `DispatcherObervers` and are used to
2
2
  calculate the reward for a given state."""
3
3
 
4
+ from typing import List
5
+
4
6
  from job_shop_lib.dispatching import DispatcherObserver, Dispatcher
5
7
  from job_shop_lib import ScheduledOperation
6
8
 
@@ -18,7 +20,7 @@ class RewardObserver(DispatcherObserver):
18
20
  self, dispatcher: Dispatcher, *, subscribe: bool = True
19
21
  ) -> None:
20
22
  super().__init__(dispatcher, subscribe=subscribe)
21
- self.rewards: list[float] = []
23
+ self.rewards: List[float] = []
22
24
 
23
25
  @property
24
26
  def last_reward(self) -> float:
@@ -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
5
+ from typing import Any, Dict, Tuple, List, Optional, Type
6
6
 
7
7
  import matplotlib.pyplot as plt
8
8
  import gymnasium as gym
@@ -15,6 +15,7 @@ from job_shop_lib.graphs.graph_updaters import (
15
15
  GraphUpdater,
16
16
  ResidualGraphUpdater,
17
17
  )
18
+ from job_shop_lib.exceptions import ValidationError
18
19
  from job_shop_lib.dispatching import (
19
20
  Dispatcher,
20
21
  filter_dominated_operations,
@@ -24,7 +25,7 @@ from job_shop_lib.dispatching.feature_observers import (
24
25
  FeatureObserverConfig,
25
26
  CompositeFeatureObserver,
26
27
  )
27
- from job_shop_lib.visualization import GanttChartCreator
28
+ from job_shop_lib.visualization.gantt import GanttChartCreator
28
29
  from job_shop_lib.reinforcement_learning import (
29
30
  RewardObserver,
30
31
  MakespanReward,
@@ -138,16 +139,16 @@ class SingleJobShopGraphEnv(gym.Env):
138
139
  job_shop_graph: JobShopGraph,
139
140
  feature_observer_configs: Sequence[FeatureObserverConfig],
140
141
  reward_function_config: DispatcherObserverConfig[
141
- type[RewardObserver]
142
+ Type[RewardObserver]
142
143
  ] = DispatcherObserverConfig(class_type=MakespanReward),
143
144
  graph_updater_config: DispatcherObserverConfig[
144
- type[GraphUpdater]
145
+ Type[GraphUpdater]
145
146
  ] = DispatcherObserverConfig(class_type=ResidualGraphUpdater),
146
- ready_operations_filter: (
147
- Callable[[Dispatcher, list[Operation]], list[Operation]] | None
148
- ) = filter_dominated_operations,
149
- render_mode: str | None = None,
150
- render_config: RenderConfig | None = None,
147
+ ready_operations_filter: Optional[
148
+ Callable[[Dispatcher, List[Operation]], List[Operation]]
149
+ ] = filter_dominated_operations,
150
+ render_mode: Optional[str] = None,
151
+ render_config: Optional[RenderConfig] = None,
151
152
  use_padding: bool = True,
152
153
  ) -> None:
153
154
  super().__init__()
@@ -195,10 +196,26 @@ class SingleJobShopGraphEnv(gym.Env):
195
196
  """Returns the job shop graph."""
196
197
  return self.graph_updater.job_shop_graph
197
198
 
199
+ def current_makespan(self) -> int:
200
+ """Returns current makespan of partial schedule."""
201
+ return self.dispatcher.schedule.makespan()
202
+
203
+ def machine_utilization(self) -> NDArray[np.float32]:
204
+ """Returns utilization percentage for each machine."""
205
+ total_time = max(1, self.current_makespan()) # Avoid division by zero
206
+ machine_busy_time = np.zeros(self.instance.num_machines)
207
+
208
+ for m_id, m_schedule in enumerate(self.dispatcher.schedule.schedule):
209
+ machine_busy_time[m_id] = sum(
210
+ op.operation.duration for op in m_schedule
211
+ )
212
+
213
+ return machine_busy_time / total_time
214
+
198
215
  def _get_observation_space(self) -> gym.spaces.Dict:
199
216
  """Returns the observation space dictionary."""
200
217
  num_edges = self.job_shop_graph.num_edges
201
- dict_space: dict[str, gym.Space] = {
218
+ dict_space: Dict[str, gym.Space] = {
202
219
  ObservationSpaceKey.REMOVED_NODES.value: gym.spaces.MultiBinary(
203
220
  len(self.job_shop_graph.nodes)
204
221
  ),
@@ -224,18 +241,23 @@ class SingleJobShopGraphEnv(gym.Env):
224
241
  def reset(
225
242
  self,
226
243
  *,
227
- seed: int | None = None,
228
- options: dict[str, Any] | None = None,
229
- ) -> tuple[ObservationDict, dict]:
244
+ seed: Optional[int] = None,
245
+ options: Optional[Dict[str, Any]] = None,
246
+ ) -> Tuple[ObservationDict, dict]:
230
247
  """Resets the environment."""
231
248
  super().reset(seed=seed, options=options)
232
249
  self.dispatcher.reset()
233
250
  obs = self.get_observation()
234
- return obs, {}
251
+ return obs, {
252
+ "feature_names": self.composite_observer.column_names,
253
+ "available_operations_with_ids": (
254
+ self.get_available_actions_with_ids()
255
+ ),
256
+ }
235
257
 
236
258
  def step(
237
- self, action: tuple[int, int]
238
- ) -> tuple[ObservationDict, float, bool, bool, dict[str, Any]]:
259
+ self, action: Tuple[int, int]
260
+ ) -> Tuple[ObservationDict, float, bool, bool, Dict[str, Any]]:
239
261
  """Takes a step in the environment.
240
262
 
241
263
  Args:
@@ -254,9 +276,9 @@ class SingleJobShopGraphEnv(gym.Env):
254
276
  - Whether the episode was truncated (always False).
255
277
  - A dictionary with additional information. The dictionary
256
278
  contains the following keys: "feature_names", the names of the
257
- features in the observation; "available_operations", the
258
- operations that are ready to be scheduled.
259
-
279
+ features in the observation; and "available_operations_with_ids",
280
+ a list of available actions in the form of (operation_id,
281
+ machine_id, job_id).
260
282
  """
261
283
  job_id, machine_id = action
262
284
  operation = self.dispatcher.next_operation(job_id)
@@ -269,9 +291,11 @@ class SingleJobShopGraphEnv(gym.Env):
269
291
  reward = self.reward_function.last_reward
270
292
  done = self.dispatcher.schedule.is_complete()
271
293
  truncated = False
272
- info: dict[str, Any] = {
294
+ info: Dict[str, Any] = {
273
295
  "feature_names": self.composite_observer.column_names,
274
- "available_operations": self.dispatcher.available_operations(),
296
+ "available_operations_with_ids": (
297
+ self.get_available_actions_with_ids()
298
+ ),
275
299
  }
276
300
  return obs, reward, done, truncated, info
277
301
 
@@ -322,6 +346,49 @@ class SingleJobShopGraphEnv(gym.Env):
322
346
  elif self.render_mode == "save_gif":
323
347
  self.gantt_chart_creator.create_gif()
324
348
 
349
+ def get_available_actions_with_ids(self) -> List[Tuple[int, int, int]]:
350
+ """Returns a list of available actions in the form of
351
+ (operation_id, machine_id, job_id)."""
352
+ available_operations = self.dispatcher.available_operations()
353
+ available_operations_with_ids = []
354
+ for operation in available_operations:
355
+ job_id = operation.job_id
356
+ operation_id = operation.operation_id
357
+ for machine_id in operation.machines:
358
+ available_operations_with_ids.append(
359
+ (operation_id, machine_id, job_id)
360
+ )
361
+ return available_operations_with_ids
362
+
363
+ def validate_action(self, action: Tuple[int, int]) -> None:
364
+ """Validates that the action is legal in the current state.
365
+
366
+ Args:
367
+ action:
368
+ The action to validate. The action is a tuple of two integers
369
+ (job_id, machine_id).
370
+
371
+ Raises:
372
+ ValidationError: If the action is invalid.
373
+ """
374
+ job_id, machine_id = action
375
+ if not 0 <= job_id < self.instance.num_jobs:
376
+ raise ValidationError(f"Invalid job_id {job_id}")
377
+
378
+ if not -1 <= machine_id < self.instance.num_machines:
379
+ raise ValidationError(f"Invalid machine_id {machine_id}")
380
+
381
+ # Check if job has operations left
382
+ job = self.instance.jobs[job_id]
383
+ if self.dispatcher.job_next_operation_index[job_id] >= len(job):
384
+ raise ValidationError(f"Job {job_id} has no operations left")
385
+
386
+ next_operation = self.dispatcher.next_operation(job_id)
387
+ if machine_id == -1 and len(next_operation.machines) > 1:
388
+ raise ValidationError(
389
+ f"Operation {next_operation} requires a machine_id"
390
+ )
391
+
325
392
 
326
393
  if __name__ == "__main__":
327
394
  from job_shop_lib.dispatching.feature_observers import (
@@ -333,7 +400,7 @@ if __name__ == "__main__":
333
400
 
334
401
  instance = load_benchmark_instance("ft06")
335
402
  job_shop_graph_ = build_disjunctive_graph(instance)
336
- feature_observer_configs_ = [
403
+ feature_observer_configs_: List[DispatcherObserverConfig] = [
337
404
  DispatcherObserverConfig(
338
405
  FeatureObserverType.IS_READY,
339
406
  kwargs={"feature_types": [FeatureType.JOBS]},
@@ -7,7 +7,7 @@ from typing import TypedDict
7
7
  import numpy as np
8
8
 
9
9
  from job_shop_lib.dispatching.feature_observers import FeatureType
10
- from job_shop_lib.visualization import (
10
+ from job_shop_lib.visualization.gantt import (
11
11
  PartialGanttChartPlotterConfig,
12
12
  GifConfig,
13
13
  VideoConfig,
@@ -1,6 +1,6 @@
1
1
  """Utility functions for reinforcement learning."""
2
2
 
3
- from typing import TypeVar, Any
3
+ from typing import TypeVar, Any, Tuple, Optional, Type
4
4
 
5
5
  import numpy as np
6
6
  from numpy.typing import NDArray
@@ -12,9 +12,9 @@ T = TypeVar("T", bound=np.number)
12
12
 
13
13
  def add_padding(
14
14
  array: NDArray[Any],
15
- output_shape: tuple[int, ...],
15
+ output_shape: Tuple[int, ...],
16
16
  padding_value: float = -1,
17
- dtype: type[T] | None = None,
17
+ dtype: Optional[Type[T]] = None,
18
18
  ) -> NDArray[T]:
19
19
  """Adds padding to the array.
20
20
 
@@ -1,60 +0,0 @@
1
- """Contains functions and classes for visualizing job shop scheduling problems.
2
-
3
- .. autosummary::
4
-
5
- plot_gantt_chart
6
- get_partial_gantt_chart_plotter
7
- PartialGanttChartPlotter
8
- create_gantt_chart_video
9
- create_gantt_chart_gif
10
- plot_disjunctive_graph
11
- plot_agent_task_graph
12
- GanttChartCreator
13
- GifConfig
14
- VideoConfig
15
-
16
- """
17
-
18
- from job_shop_lib.visualization._plot_gantt_chart import plot_gantt_chart
19
- from job_shop_lib.visualization._gantt_chart_video_and_gif_creation import (
20
- create_gantt_chart_gif,
21
- create_gantt_chart_video,
22
- create_gantt_chart_frames,
23
- get_partial_gantt_chart_plotter,
24
- create_video_from_frames,
25
- create_gif_from_frames,
26
- PartialGanttChartPlotter,
27
- )
28
- from job_shop_lib.visualization._plot_disjunctive_graph import (
29
- plot_disjunctive_graph,
30
- duration_labeler,
31
- )
32
- from job_shop_lib.visualization._plot_agent_task_graph import (
33
- plot_agent_task_graph,
34
- three_columns_layout,
35
- )
36
- from job_shop_lib.visualization._gantt_chart_creator import (
37
- GanttChartCreator,
38
- PartialGanttChartPlotterConfig,
39
- GifConfig,
40
- VideoConfig,
41
- )
42
-
43
- __all__ = [
44
- "plot_gantt_chart",
45
- "create_gantt_chart_video",
46
- "create_gantt_chart_gif",
47
- "create_gantt_chart_frames",
48
- "get_partial_gantt_chart_plotter",
49
- "create_gif_from_frames",
50
- "create_video_from_frames",
51
- "plot_disjunctive_graph",
52
- "plot_agent_task_graph",
53
- "three_columns_layout",
54
- "GanttChartCreator",
55
- "PartialGanttChartPlotterConfig",
56
- "GifConfig",
57
- "VideoConfig",
58
- "PartialGanttChartPlotter",
59
- "duration_labeler",
60
- ]
@@ -0,0 +1,48 @@
1
+ """Contains functions and classes for visualizing job shop scheduling problems.
2
+
3
+ .. autosummary::
4
+
5
+ plot_gantt_chart
6
+ get_partial_gantt_chart_plotter
7
+ PartialGanttChartPlotter
8
+ create_gantt_chart_video
9
+ create_gantt_chart_gif
10
+ GanttChartCreator
11
+ GifConfig
12
+ VideoConfig
13
+ PartialGanttChartPlotterConfig
14
+
15
+ """
16
+
17
+ from ._plot_gantt_chart import plot_gantt_chart
18
+ from ._gantt_chart_video_and_gif_creation import (
19
+ create_gantt_chart_gif,
20
+ create_gantt_chart_video,
21
+ create_gantt_chart_frames,
22
+ get_partial_gantt_chart_plotter,
23
+ create_video_from_frames,
24
+ create_gif_from_frames,
25
+ PartialGanttChartPlotter,
26
+ )
27
+
28
+ from ._gantt_chart_creator import (
29
+ GanttChartCreator,
30
+ PartialGanttChartPlotterConfig,
31
+ GifConfig,
32
+ VideoConfig,
33
+ )
34
+
35
+ __all__ = [
36
+ "plot_gantt_chart",
37
+ "create_gantt_chart_video",
38
+ "create_gantt_chart_gif",
39
+ "create_gantt_chart_frames",
40
+ "get_partial_gantt_chart_plotter",
41
+ "create_gif_from_frames",
42
+ "create_video_from_frames",
43
+ "GanttChartCreator",
44
+ "PartialGanttChartPlotterConfig",
45
+ "GifConfig",
46
+ "VideoConfig",
47
+ "PartialGanttChartPlotter",
48
+ ]