job-shop-lib 1.0.0a5__py3-none-any.whl → 1.0.0b2__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.
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
+ ]