job-shop-lib 0.5.0__py3-none-any.whl → 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. job_shop_lib/__init__.py +19 -8
  2. job_shop_lib/{base_solver.py → _base_solver.py} +1 -1
  3. job_shop_lib/{job_shop_instance.py → _job_shop_instance.py} +155 -81
  4. job_shop_lib/_operation.py +118 -0
  5. job_shop_lib/{schedule.py → _schedule.py} +102 -84
  6. job_shop_lib/{scheduled_operation.py → _scheduled_operation.py} +25 -49
  7. job_shop_lib/benchmarking/__init__.py +66 -43
  8. job_shop_lib/benchmarking/_load_benchmark.py +88 -0
  9. job_shop_lib/constraint_programming/__init__.py +13 -0
  10. job_shop_lib/{cp_sat/ortools_solver.py → constraint_programming/_ortools_solver.py} +77 -22
  11. job_shop_lib/dispatching/__init__.py +51 -42
  12. job_shop_lib/dispatching/{dispatcher.py → _dispatcher.py} +223 -130
  13. job_shop_lib/dispatching/_dispatcher_observer_config.py +67 -0
  14. job_shop_lib/dispatching/_factories.py +135 -0
  15. job_shop_lib/dispatching/{history_tracker.py → _history_observer.py} +6 -7
  16. job_shop_lib/dispatching/_optimal_operations_observer.py +113 -0
  17. job_shop_lib/dispatching/_ready_operation_filters.py +168 -0
  18. job_shop_lib/dispatching/_unscheduled_operations_observer.py +70 -0
  19. job_shop_lib/dispatching/feature_observers/__init__.py +51 -13
  20. job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +212 -0
  21. job_shop_lib/dispatching/feature_observers/{duration_observer.py → _duration_observer.py} +20 -18
  22. job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +289 -0
  23. job_shop_lib/dispatching/feature_observers/_factory.py +95 -0
  24. job_shop_lib/dispatching/feature_observers/_feature_observer.py +228 -0
  25. job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +97 -0
  26. job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +35 -0
  27. job_shop_lib/dispatching/feature_observers/{is_scheduled_observer.py → _is_scheduled_observer.py} +9 -5
  28. job_shop_lib/dispatching/feature_observers/{position_in_job_observer.py → _position_in_job_observer.py} +8 -10
  29. job_shop_lib/dispatching/feature_observers/{remaining_operations_observer.py → _remaining_operations_observer.py} +8 -26
  30. job_shop_lib/dispatching/rules/__init__.py +87 -0
  31. job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +84 -0
  32. job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +201 -0
  33. job_shop_lib/dispatching/{dispatching_rules.py → rules/_dispatching_rules_functions.py} +70 -16
  34. job_shop_lib/dispatching/rules/_machine_chooser_factory.py +71 -0
  35. job_shop_lib/dispatching/rules/_utils.py +128 -0
  36. job_shop_lib/exceptions.py +18 -0
  37. job_shop_lib/generation/__init__.py +19 -0
  38. job_shop_lib/generation/_general_instance_generator.py +165 -0
  39. job_shop_lib/generation/_instance_generator.py +133 -0
  40. job_shop_lib/{generators/transformations.py → generation/_transformations.py} +16 -12
  41. job_shop_lib/generation/_utils.py +124 -0
  42. job_shop_lib/graphs/__init__.py +30 -12
  43. job_shop_lib/graphs/{build_disjunctive_graph.py → _build_disjunctive_graph.py} +41 -3
  44. job_shop_lib/graphs/{build_agent_task_graph.py → _build_resource_task_graphs.py} +28 -26
  45. job_shop_lib/graphs/_constants.py +38 -0
  46. job_shop_lib/graphs/_job_shop_graph.py +320 -0
  47. job_shop_lib/graphs/_node.py +182 -0
  48. job_shop_lib/graphs/graph_updaters/__init__.py +26 -0
  49. job_shop_lib/graphs/graph_updaters/_disjunctive_graph_updater.py +108 -0
  50. job_shop_lib/graphs/graph_updaters/_graph_updater.py +57 -0
  51. job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +155 -0
  52. job_shop_lib/graphs/graph_updaters/_utils.py +25 -0
  53. job_shop_lib/py.typed +0 -0
  54. job_shop_lib/reinforcement_learning/__init__.py +68 -0
  55. job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +398 -0
  56. job_shop_lib/reinforcement_learning/_resource_task_graph_observation.py +329 -0
  57. job_shop_lib/reinforcement_learning/_reward_observers.py +87 -0
  58. job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +443 -0
  59. job_shop_lib/reinforcement_learning/_types_and_constants.py +62 -0
  60. job_shop_lib/reinforcement_learning/_utils.py +199 -0
  61. job_shop_lib/visualization/__init__.py +0 -25
  62. job_shop_lib/visualization/gantt/__init__.py +48 -0
  63. job_shop_lib/visualization/gantt/_gantt_chart_creator.py +257 -0
  64. job_shop_lib/visualization/gantt/_gantt_chart_video_and_gif_creation.py +422 -0
  65. job_shop_lib/visualization/{gantt_chart.py → gantt/_plot_gantt_chart.py} +84 -21
  66. job_shop_lib/visualization/graphs/__init__.py +29 -0
  67. job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py +418 -0
  68. job_shop_lib/visualization/graphs/_plot_resource_task_graph.py +389 -0
  69. {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/METADATA +87 -55
  70. job_shop_lib-1.0.0.dist-info/RECORD +73 -0
  71. {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/WHEEL +1 -1
  72. job_shop_lib/benchmarking/load_benchmark.py +0 -142
  73. job_shop_lib/cp_sat/__init__.py +0 -5
  74. job_shop_lib/dispatching/dispatching_rule_solver.py +0 -119
  75. job_shop_lib/dispatching/factories.py +0 -206
  76. job_shop_lib/dispatching/feature_observers/composite_feature_observer.py +0 -87
  77. job_shop_lib/dispatching/feature_observers/earliest_start_time_observer.py +0 -156
  78. job_shop_lib/dispatching/feature_observers/factory.py +0 -58
  79. job_shop_lib/dispatching/feature_observers/feature_observer.py +0 -113
  80. job_shop_lib/dispatching/feature_observers/is_completed_observer.py +0 -98
  81. job_shop_lib/dispatching/feature_observers/is_ready_observer.py +0 -40
  82. job_shop_lib/dispatching/pruning_functions.py +0 -116
  83. job_shop_lib/generators/__init__.py +0 -7
  84. job_shop_lib/generators/basic_generator.py +0 -197
  85. job_shop_lib/graphs/constants.py +0 -21
  86. job_shop_lib/graphs/job_shop_graph.py +0 -202
  87. job_shop_lib/graphs/node.py +0 -166
  88. job_shop_lib/operation.py +0 -122
  89. job_shop_lib/visualization/agent_task_graph.py +0 -257
  90. job_shop_lib/visualization/create_gif.py +0 -209
  91. job_shop_lib/visualization/disjunctive_graph.py +0 -210
  92. job_shop_lib-0.5.0.dist-info/RECORD +0 -48
  93. {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/LICENSE +0 -0
@@ -0,0 +1,443 @@
1
+ """Home of the `SingleJobShopGraphEnv` class."""
2
+
3
+ from copy import deepcopy
4
+ from collections.abc import Callable, Sequence
5
+ from typing import Any, Dict, Tuple, List, Optional, Type, Union
6
+
7
+ import matplotlib.pyplot as plt
8
+ import gymnasium as gym
9
+ import numpy as np
10
+ from numpy.typing import NDArray
11
+
12
+ from job_shop_lib import JobShopInstance, Operation
13
+ from job_shop_lib.graphs import JobShopGraph
14
+ from job_shop_lib.graphs.graph_updaters import (
15
+ GraphUpdater,
16
+ ResidualGraphUpdater,
17
+ )
18
+ from job_shop_lib.exceptions import ValidationError
19
+ from job_shop_lib.dispatching import (
20
+ Dispatcher,
21
+ filter_dominated_operations,
22
+ DispatcherObserverConfig,
23
+ )
24
+ from job_shop_lib.dispatching.feature_observers import (
25
+ FeatureObserverConfig,
26
+ CompositeFeatureObserver,
27
+ FeatureObserver,
28
+ FeatureObserverType,
29
+ )
30
+ from job_shop_lib.visualization.gantt import GanttChartCreator
31
+ from job_shop_lib.reinforcement_learning import (
32
+ RewardObserver,
33
+ MakespanReward,
34
+ add_padding,
35
+ RenderConfig,
36
+ ObservationSpaceKey,
37
+ ObservationDict,
38
+ )
39
+
40
+
41
+ class SingleJobShopGraphEnv(gym.Env):
42
+ """A Gymnasium environment for solving a specific instance of the Job Shop
43
+ Scheduling Problem represented as a graph.
44
+
45
+ This environment manages the scheduling process for a single Job Shop
46
+ instance, using a graph representation and various observers to track the
47
+ state and compute rewards.
48
+
49
+ Observation Space:
50
+ A dictionary with the following keys:
51
+
52
+ - "removed_nodes": Binary vector indicating removed graph nodes.
53
+ - "edge_list": Matrix of graph edges in COO format.
54
+ - Feature matrices: Keys corresponding to the composite observer
55
+ features (e.g., "operations", "jobs", "machines").
56
+
57
+ Action Space:
58
+ MultiDiscrete space representing (job_id, machine_id) pairs.
59
+
60
+ Render Modes:
61
+
62
+ - "human": Displays the current Gantt chart.
63
+ - "save_video": Saves a video of the complete Gantt chart.
64
+ - "save_gif": Saves a GIF of the complete Gantt chart.
65
+
66
+ Attributes:
67
+ dispatcher:
68
+ Manages the scheduling process. See
69
+ :class:`~job_shop_lib.dispatching.Dispatcher`.
70
+
71
+ composite_observer:
72
+ A :class:`~job_shop_lib.dispatching.feature_observers.
73
+ CompositeFeatureObserver` which aggregates features from multiple
74
+ observers.
75
+
76
+ graph_updater:
77
+ Updates the graph representation after each action. See
78
+ :class:`~job_shop_lib.graphs.GraphUpdater`.
79
+
80
+ reward_function:
81
+ Computes rewards for actions taken. See
82
+ :class:`~job_shop_lib.reinforcement_learning.RewardObserver`.
83
+
84
+ action_space:
85
+ Defines the action space. The action is a tuple of two integers
86
+ (job_id, machine_id). The machine_id can be -1 if the selected
87
+ operation can only be scheduled in one machine.
88
+
89
+ observation_space:
90
+ Defines the observation space. The observation is a dictionary
91
+ with the following keys:
92
+
93
+ - "removed_nodes": Binary vector indicating removed graph nodes.
94
+ - "edge_list": Matrix of graph edges in COO format.
95
+ - Feature matrices: Keys corresponding to the composite observer
96
+ features (e.g., "operations", "jobs", "machines").
97
+
98
+ render_mode:
99
+ The mode for rendering the environment ("human", "save_video",
100
+ "save_gif").
101
+
102
+ gantt_chart_creator:
103
+ Creates Gantt chart visualizations. See
104
+ :class:`~job_shop_lib.visualization.GanttChartCreator`.
105
+
106
+ use_padding:
107
+ Whether to use padding in observations. Padding maintains the
108
+ observation space shape when the number of nodes changes.
109
+
110
+ Args:
111
+ job_shop_graph:
112
+ The JobShopGraph instance representing the job shop problem.
113
+ feature_observer_configs:
114
+ A list of FeatureObserverConfig instances for the feature
115
+ observers.
116
+ reward_function_config:
117
+ The configuration for the reward function.
118
+ graph_updater_config:
119
+ The configuration for the graph updater.
120
+ ready_operations_filter:
121
+ The function to use for pruning dominated operations.
122
+ render_mode:
123
+ The mode for rendering the environment ("human", "save_video",
124
+ "save_gif").
125
+ render_config:
126
+ Configuration for rendering (e.g., paths for saving videos
127
+ or GIFs). See :class:`~job_shop_lib.visualization.RenderConfig`.
128
+ use_padding:
129
+ Whether to use padding in observations. Padding maintains the
130
+ observation space shape when the number of nodes changes.
131
+ """
132
+
133
+ metadata = {"render_modes": ["human", "save_video", "save_gif"]}
134
+
135
+ # I think the class is easier to use this way. We could initiliaze the
136
+ # class from Dispatcher or an already initialized RewardFunction. However,
137
+ # it would be impossible to add good default values.
138
+ # pylint: disable=too-many-arguments
139
+ def __init__(
140
+ self,
141
+ job_shop_graph: JobShopGraph,
142
+ feature_observer_configs: Sequence[
143
+ Union[
144
+ str,
145
+ FeatureObserverType,
146
+ Type[FeatureObserver],
147
+ FeatureObserverConfig,
148
+ ],
149
+ ],
150
+ reward_function_config: DispatcherObserverConfig[
151
+ Type[RewardObserver]
152
+ ] = DispatcherObserverConfig(class_type=MakespanReward),
153
+ graph_updater_config: DispatcherObserverConfig[
154
+ Type[GraphUpdater]
155
+ ] = DispatcherObserverConfig(class_type=ResidualGraphUpdater),
156
+ ready_operations_filter: Optional[
157
+ Callable[[Dispatcher, List[Operation]], List[Operation]]
158
+ ] = filter_dominated_operations,
159
+ render_mode: Optional[str] = None,
160
+ render_config: Optional[RenderConfig] = None,
161
+ use_padding: bool = True,
162
+ ) -> None:
163
+ super().__init__()
164
+ # Used for resetting the environment
165
+ self.initial_job_shop_graph = deepcopy(job_shop_graph)
166
+
167
+ self.dispatcher = Dispatcher(
168
+ job_shop_graph.instance,
169
+ ready_operations_filter=ready_operations_filter,
170
+ )
171
+
172
+ # Observers added to track the environment state
173
+ self.composite_observer = (
174
+ CompositeFeatureObserver.from_feature_observer_configs(
175
+ self.dispatcher, feature_observer_configs
176
+ )
177
+ )
178
+ self.graph_updater = graph_updater_config.class_type(
179
+ dispatcher=self.dispatcher,
180
+ job_shop_graph=job_shop_graph,
181
+ **graph_updater_config.kwargs,
182
+ )
183
+ self.reward_function = reward_function_config.class_type(
184
+ dispatcher=self.dispatcher, **reward_function_config.kwargs
185
+ )
186
+ self.action_space = gym.spaces.MultiDiscrete(
187
+ [self.instance.num_jobs, self.instance.num_machines], start=[0, -1]
188
+ )
189
+ self.observation_space: gym.spaces.Dict = self._get_observation_space()
190
+ self.render_mode = render_mode
191
+ if render_config is None:
192
+ render_config = {}
193
+ self.gantt_chart_creator = GanttChartCreator(
194
+ dispatcher=self.dispatcher, **render_config
195
+ )
196
+ self.use_padding = use_padding
197
+
198
+ @property
199
+ def instance(self) -> JobShopInstance:
200
+ """Returns the instance the environment is working on."""
201
+ return self.job_shop_graph.instance
202
+
203
+ @property
204
+ def job_shop_graph(self) -> JobShopGraph:
205
+ """Returns the job shop graph."""
206
+ return self.graph_updater.job_shop_graph
207
+
208
+ def current_makespan(self) -> int:
209
+ """Returns current makespan of partial schedule."""
210
+ return self.dispatcher.schedule.makespan()
211
+
212
+ def machine_utilization(self) -> NDArray[np.float32]:
213
+ """Returns utilization percentage for each machine."""
214
+ total_time = max(1, self.current_makespan()) # Avoid division by zero
215
+ machine_busy_time = np.zeros(self.instance.num_machines)
216
+
217
+ for m_id, m_schedule in enumerate(self.dispatcher.schedule.schedule):
218
+ machine_busy_time[m_id] = sum(
219
+ op.operation.duration for op in m_schedule
220
+ )
221
+
222
+ return machine_busy_time / total_time
223
+
224
+ def _get_observation_space(self) -> gym.spaces.Dict:
225
+ """Returns the observation space dictionary."""
226
+ num_edges = self.job_shop_graph.num_edges
227
+ dict_space: Dict[str, gym.Space] = {
228
+ ObservationSpaceKey.REMOVED_NODES.value: gym.spaces.MultiBinary(
229
+ len(self.job_shop_graph.nodes)
230
+ ),
231
+ ObservationSpaceKey.EDGE_INDEX.value: gym.spaces.MultiDiscrete(
232
+ np.full(
233
+ (2, num_edges),
234
+ fill_value=len(self.job_shop_graph.nodes) + 1,
235
+ dtype=np.int32,
236
+ ),
237
+ start=np.full(
238
+ (2, num_edges),
239
+ fill_value=-1, # -1 is used for padding
240
+ dtype=np.int32,
241
+ ),
242
+ ),
243
+ }
244
+ for feature_type, matrix in self.composite_observer.features.items():
245
+ dict_space[feature_type.value] = gym.spaces.Box(
246
+ low=-np.inf, high=np.inf, shape=matrix.shape
247
+ )
248
+ return gym.spaces.Dict(dict_space)
249
+
250
+ def reset(
251
+ self,
252
+ *,
253
+ seed: Optional[int] = None,
254
+ options: Optional[Dict[str, Any]] = None,
255
+ ) -> Tuple[ObservationDict, dict[str, Any]]:
256
+ """Resets the environment.
257
+
258
+ Args:
259
+ seed:
260
+ Added to match the signature of the parent class. It is not
261
+ used in this method.
262
+ options:
263
+ Additional options to pass to the environment. Not used in
264
+ this method.
265
+
266
+ Returns:
267
+ A tuple containing the following elements:
268
+
269
+ - The observation of the environment.
270
+ - A dictionary with additional information, keys
271
+ include: "feature_names", the names of the features in the
272
+ observation; and "available_operations_with_ids", a list of
273
+ available a list of available actions in the form of
274
+ (operation_id, machine_id, job_id).
275
+ """
276
+ super().reset(seed=seed, options=options)
277
+ self.dispatcher.reset()
278
+ obs = self.get_observation()
279
+ return obs, {
280
+ "feature_names": self.composite_observer.column_names,
281
+ "available_operations_with_ids": (
282
+ self.get_available_actions_with_ids()
283
+ ),
284
+ }
285
+
286
+ def step(
287
+ self, action: Tuple[int, int]
288
+ ) -> Tuple[ObservationDict, float, bool, bool, Dict[str, Any]]:
289
+ """Takes a step in the environment.
290
+
291
+ Args:
292
+ action:
293
+ The action to take. The action is a tuple of two integers
294
+ (job_id, machine_id):
295
+ the job ID and the machine ID in which to schedule the
296
+ operation.
297
+
298
+ Returns:
299
+ A tuple containing the following elements:
300
+
301
+ - The observation of the environment.
302
+ - The reward obtained.
303
+ - Whether the environment is done.
304
+ - Whether the episode was truncated (always False).
305
+ - A dictionary with additional information. The dictionary
306
+ contains the following keys: "feature_names", the names of the
307
+ features in the observation; and "available_operations_with_ids",
308
+ a list of available actions in the form of (operation_id,
309
+ machine_id, job_id).
310
+ """
311
+ job_id, machine_id = action
312
+ operation = self.dispatcher.next_operation(job_id)
313
+ if machine_id == -1:
314
+ machine_id = operation.machine_id
315
+
316
+ self.dispatcher.dispatch(operation, machine_id)
317
+
318
+ obs = self.get_observation()
319
+ reward = self.reward_function.last_reward
320
+ done = self.dispatcher.schedule.is_complete()
321
+ truncated = False
322
+ info: Dict[str, Any] = {
323
+ "feature_names": self.composite_observer.column_names,
324
+ "available_operations_with_ids": (
325
+ self.get_available_actions_with_ids()
326
+ ),
327
+ }
328
+ return obs, reward, done, truncated, info
329
+
330
+ def get_observation(self) -> ObservationDict:
331
+ """Returns the current observation of the environment."""
332
+ observation: ObservationDict = {
333
+ ObservationSpaceKey.REMOVED_NODES.value: np.array(
334
+ self.job_shop_graph.removed_nodes, dtype=bool
335
+ ),
336
+ ObservationSpaceKey.EDGE_INDEX.value: self._get_edge_index(),
337
+ }
338
+ for feature_type, matrix in self.composite_observer.features.items():
339
+ observation[feature_type.value] = matrix
340
+ return observation
341
+
342
+ def _get_edge_index(self) -> NDArray[np.int32]:
343
+ """Returns the edge index matrix."""
344
+ edge_index = np.array(
345
+ self.job_shop_graph.graph.edges(), dtype=np.int32
346
+ ).T
347
+
348
+ if self.use_padding:
349
+ output_shape = self.observation_space[
350
+ ObservationSpaceKey.EDGE_INDEX.value
351
+ ].shape
352
+ assert output_shape is not None # For the type checker
353
+ edge_index = add_padding(
354
+ edge_index, output_shape=output_shape, dtype=np.int32
355
+ )
356
+ return edge_index
357
+
358
+ def render(self):
359
+ """Renders the environment.
360
+
361
+ The rendering mode is set by the `render_mode` attribute:
362
+
363
+ - human: Renders the current Gannt chart.
364
+ - save_video: Saves a video of the Gantt chart. Used only if the
365
+ schedule is completed.
366
+ - save_gif: Saves a GIF of the Gantt chart. Used only if the schedule
367
+ is completed.
368
+ """
369
+ if self.render_mode == "human":
370
+ self.gantt_chart_creator.plot_gantt_chart()
371
+ plt.show(block=False)
372
+ elif self.render_mode == "save_video":
373
+ self.gantt_chart_creator.create_video()
374
+ elif self.render_mode == "save_gif":
375
+ self.gantt_chart_creator.create_gif()
376
+
377
+ def get_available_actions_with_ids(self) -> List[Tuple[int, int, int]]:
378
+ """Returns a list of available actions in the form of
379
+ (operation_id, machine_id, job_id)."""
380
+ available_operations = self.dispatcher.available_operations()
381
+ available_operations_with_ids = []
382
+ for operation in available_operations:
383
+ job_id = operation.job_id
384
+ operation_id = operation.operation_id
385
+ for machine_id in operation.machines:
386
+ available_operations_with_ids.append(
387
+ (operation_id, machine_id, job_id)
388
+ )
389
+ return available_operations_with_ids
390
+
391
+ def validate_action(self, action: Tuple[int, int]) -> None:
392
+ """Validates that the action is legal in the current state.
393
+
394
+ Args:
395
+ action:
396
+ The action to validate. The action is a tuple of two integers
397
+ (job_id, machine_id).
398
+
399
+ Raises:
400
+ ValidationError: If the action is invalid.
401
+ """
402
+ job_id, machine_id = action
403
+ if not 0 <= job_id < self.instance.num_jobs:
404
+ raise ValidationError(f"Invalid job_id {job_id}")
405
+
406
+ if not -1 <= machine_id < self.instance.num_machines:
407
+ raise ValidationError(f"Invalid machine_id {machine_id}")
408
+
409
+ # Check if job has operations left
410
+ job = self.instance.jobs[job_id]
411
+ if self.dispatcher.job_next_operation_index[job_id] >= len(job):
412
+ raise ValidationError(f"Job {job_id} has no operations left")
413
+
414
+ next_operation = self.dispatcher.next_operation(job_id)
415
+ if machine_id == -1 and len(next_operation.machines) > 1:
416
+ raise ValidationError(
417
+ f"Operation {next_operation} requires a machine_id"
418
+ )
419
+
420
+
421
+ if __name__ == "__main__":
422
+ from job_shop_lib.dispatching.feature_observers import (
423
+ FeatureObserverType,
424
+ FeatureType,
425
+ )
426
+ from job_shop_lib.graphs import build_disjunctive_graph
427
+ from job_shop_lib.benchmarking import load_benchmark_instance
428
+
429
+ instance = load_benchmark_instance("ft06")
430
+ job_shop_graph_ = build_disjunctive_graph(instance)
431
+ feature_observer_configs_: List[DispatcherObserverConfig] = [
432
+ DispatcherObserverConfig(
433
+ FeatureObserverType.IS_READY,
434
+ kwargs={"feature_types": [FeatureType.JOBS]},
435
+ )
436
+ ]
437
+
438
+ env = SingleJobShopGraphEnv(
439
+ job_shop_graph=job_shop_graph_,
440
+ feature_observer_configs=feature_observer_configs_,
441
+ render_mode="save_video",
442
+ render_config={"video_config": {"fps": 4}},
443
+ )
@@ -0,0 +1,62 @@
1
+ """Contains types and enumerations used in the reinforcement learning
2
+ module."""
3
+
4
+ from enum import Enum
5
+ from typing import TypedDict
6
+
7
+ import numpy as np
8
+ from numpy.typing import NDArray
9
+
10
+ from job_shop_lib.dispatching.feature_observers import FeatureType
11
+ from job_shop_lib.visualization.gantt import (
12
+ PartialGanttChartPlotterConfig,
13
+ GifConfig,
14
+ VideoConfig,
15
+ )
16
+
17
+
18
+ class RenderConfig(TypedDict, total=False):
19
+ """Configuration needed to initialize the `GanttChartCreator` class."""
20
+
21
+ partial_gantt_chart_plotter_config: PartialGanttChartPlotterConfig
22
+ video_config: VideoConfig
23
+ gif_config: GifConfig
24
+
25
+
26
+ class ObservationSpaceKey(str, Enum):
27
+ """Enumeration of the keys for the observation space dictionary."""
28
+
29
+ REMOVED_NODES = "removed_nodes"
30
+ EDGE_INDEX = "edge_index"
31
+ OPERATIONS = FeatureType.OPERATIONS.value
32
+ JOBS = FeatureType.JOBS.value
33
+ MACHINES = FeatureType.MACHINES.value
34
+
35
+
36
+ class _ObservationDictRequired(TypedDict):
37
+ """Required fields for the observation dictionary."""
38
+
39
+ removed_nodes: NDArray[np.bool_]
40
+ edge_index: NDArray[np.int32]
41
+
42
+
43
+ class _ObservationDictOptional(TypedDict, total=False):
44
+ """Optional fields for the observation dictionary."""
45
+
46
+ operations: NDArray[np.float32]
47
+ jobs: NDArray[np.float32]
48
+ machines: NDArray[np.float32]
49
+
50
+
51
+ class ObservationDict(_ObservationDictRequired, _ObservationDictOptional):
52
+ """A dictionary containing the observation of the environment.
53
+
54
+ Required fields:
55
+ removed_nodes: Binary vector indicating removed nodes.
56
+ edge_index: Edge list in COO format.
57
+
58
+ Optional fields:
59
+ operations: Matrix of operation features.
60
+ jobs: Matrix of job features.
61
+ machines: Matrix of machine features.
62
+ """
@@ -0,0 +1,199 @@
1
+ """Utility functions for reinforcement learning."""
2
+
3
+ from typing import TypeVar, Any, Type
4
+
5
+ import numpy as np
6
+ from numpy.typing import NDArray
7
+
8
+ from job_shop_lib.exceptions import ValidationError
9
+ from job_shop_lib.dispatching import OptimalOperationsObserver
10
+
11
+ T = TypeVar("T", bound=np.number)
12
+
13
+
14
+ def add_padding(
15
+ array: NDArray[Any],
16
+ output_shape: tuple[int, ...],
17
+ padding_value: float = -1,
18
+ dtype: Type[T] | None = None,
19
+ ) -> NDArray[T]:
20
+ """Adds padding to the array.
21
+
22
+ Pads the input array to the specified output shape with a given padding
23
+ value. If the ``dtype`` is not specified, the ``dtype`` of the input array
24
+ is used.
25
+
26
+ Args:
27
+ array:
28
+ The input array to be padded.
29
+ output_shape:
30
+ The desired shape of the output array.
31
+ padding_value:
32
+ The value to use for padding. Defaults to -1.
33
+ dtype:
34
+ The data type for the output array. Defaults to ``None``, in which
35
+ case the dtype of the input array is used.
36
+
37
+ Returns:
38
+ The padded array with the specified output shape.
39
+
40
+ Raises:
41
+ ValidationError:
42
+ If the output shape is smaller than the input shape.
43
+
44
+ Examples:
45
+
46
+ .. doctest::
47
+
48
+ >>> array = np.array([[1, 2], [3, 4]])
49
+ >>> add_padding(array, (3, 3))
50
+ array([[ 1, 2, -1],
51
+ [ 3, 4, -1],
52
+ [-1, -1, -1]])
53
+
54
+ >>> add_padding(array, (3, 3), padding_value=0)
55
+ array([[1, 2, 0],
56
+ [3, 4, 0],
57
+ [0, 0, 0]])
58
+
59
+ >>> bool_array = np.array([[True, False], [False, True]])
60
+ >>> add_padding(bool_array, (3, 3), padding_value=False, dtype=int)
61
+ array([[1, 0, 0],
62
+ [0, 1, 0],
63
+ [0, 0, 0]])
64
+
65
+ >>> add_padding(bool_array, (3, 3), dtype=int)
66
+ array([[ 1, 0, -1],
67
+ [ 0, 1, -1],
68
+ [-1, -1, -1]])
69
+ """
70
+
71
+ if np.any(np.less(output_shape, array.shape)):
72
+ raise ValidationError(
73
+ "Output shape must be greater than the input shape. "
74
+ f"Got output shape: {output_shape}, input shape: {array.shape}."
75
+ )
76
+
77
+ if dtype is None:
78
+ dtype = array.dtype.type
79
+
80
+ padded_array = np.full(
81
+ output_shape,
82
+ fill_value=padding_value,
83
+ dtype=dtype,
84
+ )
85
+
86
+ if array.size == 0:
87
+ return padded_array
88
+
89
+ slices = tuple(slice(0, dim) for dim in array.shape)
90
+ padded_array[slices] = array
91
+ return padded_array
92
+
93
+
94
+ def create_edge_type_dict(
95
+ edge_index: NDArray[T],
96
+ type_ranges: dict[str, tuple[int, int]],
97
+ relationship: str = "to",
98
+ ) -> dict[tuple[str, str, str], NDArray[T]]:
99
+ """Organizes edges based on node types.
100
+
101
+ Args:
102
+ edge_index:
103
+ numpy array of shape (2, E) where E is number of edges
104
+ type_ranges: dict[str, tuple[int, int]]
105
+ Dictionary mapping type names to their corresponding index ranges
106
+ [start, end) in the ``edge_index`` array.
107
+ relationship:
108
+ A string representing the relationship type between nodes.
109
+
110
+ Returns:
111
+ A dictionary with keys (type_i, relationship, type_j) and values as
112
+ edge indices
113
+ """
114
+ edge_index_dict: dict[tuple[str, str, str], NDArray] = {}
115
+ for type_name_i, (start_i, end_i) in type_ranges.items():
116
+ for type_name_j, (start_j, end_j) in type_ranges.items():
117
+ key: tuple[str, str, str] = (
118
+ type_name_i,
119
+ relationship,
120
+ type_name_j,
121
+ )
122
+ # Find edges where source is in type_i and target is in type_j
123
+ mask = (
124
+ (edge_index[0] >= start_i)
125
+ & (edge_index[0] < end_i)
126
+ & (edge_index[1] >= start_j)
127
+ & (edge_index[1] < end_j)
128
+ )
129
+ edge_index_dict[key] = edge_index[:, mask]
130
+
131
+ return edge_index_dict
132
+
133
+
134
+ def map_values(array: NDArray[T], mapping: dict[int, int]) -> NDArray[T]:
135
+ """Maps values in an array using a mapping.
136
+
137
+ Args:
138
+ array:
139
+ An NumPy array.
140
+
141
+ Returns:
142
+ A NumPy array where each element has been replaced by its
143
+ corresponding value from the mapping.
144
+
145
+ Raises:
146
+ ValidationError:
147
+ If the array contains values that are not in the mapping.
148
+
149
+ Examples:
150
+ >>> map_values(np.array([1, 2, 3]), {1: 10, 2: 20, 3: 30})
151
+ array([10, 20, 30])
152
+
153
+ >>> map_values(np.array([1, 2]), {1: 10, 2: 10, 3: 30})
154
+ array([10, 10])
155
+
156
+ """
157
+ if array.size == 0:
158
+ return array
159
+ try:
160
+ vectorized_mapping = np.vectorize(mapping.get)
161
+ return vectorized_mapping(array)
162
+ except TypeError as e:
163
+ raise ValidationError(
164
+ "The array contains values that are not in the mapping."
165
+ ) from e
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
+
196
+ if __name__ == "__main__":
197
+ import doctest
198
+
199
+ doctest.testmod()