job-shop-lib 0.5.1__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 (95) 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 +10 -2
  38. job_shop_lib/generation/_general_instance_generator.py +165 -0
  39. job_shop_lib/generation/{instance_generator.py → _instance_generator.py} +37 -26
  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.1.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.1.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/generation/general_instance_generator.py +0 -169
  84. job_shop_lib/generation/transformations.py +0 -164
  85. job_shop_lib/generators/__init__.py +0 -8
  86. job_shop_lib/generators/basic_generator.py +0 -200
  87. job_shop_lib/graphs/constants.py +0 -21
  88. job_shop_lib/graphs/job_shop_graph.py +0 -202
  89. job_shop_lib/graphs/node.py +0 -166
  90. job_shop_lib/operation.py +0 -122
  91. job_shop_lib/visualization/agent_task_graph.py +0 -257
  92. job_shop_lib/visualization/create_gif.py +0 -209
  93. job_shop_lib/visualization/disjunctive_graph.py +0 -210
  94. job_shop_lib-0.5.1.dist-info/RECORD +0 -52
  95. {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0.dist-info}/LICENSE +0 -0
@@ -0,0 +1,398 @@
1
+ """Home of the `GraphEnvironment` class."""
2
+
3
+ from collections import defaultdict
4
+ from collections.abc import Callable, Sequence
5
+ from typing import Any, Tuple, Dict, List, Optional, Type
6
+ from copy import deepcopy
7
+
8
+ import gymnasium as gym
9
+ import numpy as np
10
+
11
+ from job_shop_lib import JobShopInstance, Operation
12
+ from job_shop_lib.dispatching import (
13
+ Dispatcher,
14
+ filter_dominated_operations,
15
+ DispatcherObserverConfig,
16
+ )
17
+ from job_shop_lib.dispatching.feature_observers import FeatureObserverConfig
18
+ from job_shop_lib.generation import InstanceGenerator
19
+ from job_shop_lib.graphs import JobShopGraph, build_resource_task_graph
20
+ from job_shop_lib.graphs.graph_updaters import (
21
+ GraphUpdater,
22
+ ResidualGraphUpdater,
23
+ )
24
+ from job_shop_lib.reinforcement_learning import (
25
+ SingleJobShopGraphEnv,
26
+ RewardObserver,
27
+ RenderConfig,
28
+ MakespanReward,
29
+ ObservationDict,
30
+ ObservationSpaceKey,
31
+ add_padding,
32
+ )
33
+
34
+
35
+ class MultiJobShopGraphEnv(gym.Env):
36
+ """Gymnasium environment for solving multiple Job Shop Scheduling Problems
37
+ using reinforcement learning and Graph Neural Networks.
38
+
39
+ This environment generates a new Job Shop Scheduling Problem instance
40
+ for each reset, creates a graph representation, and manages the scheduling
41
+ process using a :class:`~job_shop_lib.dispatching.Dispatcher`.
42
+
43
+ The observation space includes:
44
+
45
+ - removed_nodes: Binary vector indicating removed nodes.
46
+ - edge_index: Edge list in COO format.
47
+ - operations: Matrix of operation features.
48
+ - jobs: Matrix of job features (if applicable).
49
+ - machines: Matrix of machine features (if applicable).
50
+
51
+ Internally, the class creates a
52
+ :class:`~job_shop_lib.reinforcement_learning.SingleJobShopGraphEnv`
53
+ environment to manage the scheduling process for each
54
+ :class:`~job_shop_lib.JobShopInstance`.
55
+
56
+ Attributes:
57
+ instance_generator:
58
+ A :class:`~job_shop_lib.generation.InstanceGenerator` that
59
+ generates a new problem instance on each reset.
60
+
61
+ action_space:
62
+ :class:`gymnasium.spaces.Discrete`) action space with size equal to
63
+ the maximum number of jobs.
64
+
65
+ observation_space:
66
+ Dictionary of observation spaces. Keys are defined in
67
+ :class:`~job_shop_lib.reinforcement_learning.ObservationSpaceKey`.
68
+
69
+ single_job_shop_graph_env:
70
+ Environment for a specific Job Shop Scheduling Problem instance.
71
+ See :class:`SingleJobShopGraphEnv`.
72
+
73
+ graph_initializer:
74
+ Function to create the initial graph representation. It should
75
+ take a :class:`~job_shop_lib.JobShopInstance` as input and return
76
+ a :class:`~job_shop_lib.graphs.JobShopGraph`.
77
+
78
+ render_mode:
79
+ Rendering mode for visualization. Supported modes are:
80
+
81
+ - human: Renders the current Gannt chart.
82
+ - save_video: Saves a video of the Gantt chart. Used only if the
83
+ schedule is completed.
84
+ - save_gif: Saves a GIF of the Gantt chart. Used only if the
85
+ schedule is completed.
86
+
87
+ render_config:
88
+ Configuration for rendering. See
89
+ :class:`~job_shop_lib.RenderConfig`.
90
+
91
+ feature_observer_configs:
92
+ List of :class:`~job_shop_lib.dispatching.DispatcherObserverConfig`
93
+ for feature observers.
94
+ reward_function_config:
95
+ Configuration for the reward function. See
96
+ :class:`~job_shop_lib.dispatching.DispatcherObserverConfig` and
97
+ :class:`~job_shop_lib.dispatching.RewardObserver`.
98
+
99
+ graph_updater_config:
100
+ Configuration for the graph updater. The graph updater is used to
101
+ update the graph representation after each action. See
102
+ :class:`~job_shop_lib.dispatching.DispatcherObserverConfig` and
103
+ :class:`~job_shop_lib.graphs.GraphUpdater`.
104
+ Args:
105
+ instance_generator:
106
+ A :class:`~job_shop_lib.generation.InstanceGenerator` that
107
+ generates a new problem instance on each reset.
108
+
109
+ feature_observer_configs:
110
+ Configurations for feature observers. Each configuration
111
+ should be a
112
+ :class:`~job_shop_lib.dispatching.DispatcherObserverConfig`
113
+ with a class type that inherits from
114
+ :class:`~job_shop_lib.dispatching.FeatureObserver` or a string
115
+ or enum that represents a built-in feature observer.
116
+
117
+ graph_initializer:
118
+ Function to create the initial graph representation.
119
+ If ``None``, the default graph initializer is used:
120
+ :func:`~job_shop_lib.graphs.build_resource_task_graph`.
121
+ graph_updater_config:
122
+ Configuration for the graph updater. The graph updater is used
123
+ to update the graph representation after each action. If
124
+ ``None``, the default graph updater is used:
125
+ :class:`~job_shop_lib.graphs.ResidualGraphUpdater`.
126
+
127
+ ready_operations_filter:
128
+ Function to filter ready operations. If ``None``, the default
129
+ filter is used:
130
+ :func:`~job_shop_lib.dispatching.filter_dominated_operations`.
131
+
132
+ reward_function_config:
133
+ Configuration for the reward function. If ``None``, the default
134
+ reward function is used:
135
+ :class:`~job_shop_lib.dispatching.MakespanReward`.
136
+
137
+ render_mode:
138
+ Rendering mode for visualization. Supported modes are:
139
+
140
+ - human: Renders the current Gannt chart.
141
+ - save_video: Saves a video of the Gantt chart. Used only if
142
+ the schedule is completed.
143
+ - save_gif: Saves a GIF of the Gantt chart. Used only if the
144
+ schedule is completed.
145
+ render_config:
146
+ Configuration for rendering. See
147
+ :class:`~job_shop_lib.RenderConfig`.
148
+
149
+ use_padding:
150
+ Whether to use padding in observations. If True, all matrices
151
+ are padded to fixed sizes based on the maximum instance size.
152
+ Values are padded with -1, except for the "removed_nodes" key,
153
+ which is padded with ``True``, indicating that the node is
154
+ removed.
155
+ """
156
+
157
+ def __init__(
158
+ self,
159
+ instance_generator: InstanceGenerator,
160
+ feature_observer_configs: Sequence[FeatureObserverConfig],
161
+ graph_initializer: Callable[
162
+ [JobShopInstance], JobShopGraph
163
+ ] = build_resource_task_graph,
164
+ graph_updater_config: DispatcherObserverConfig[
165
+ Type[GraphUpdater]
166
+ ] = DispatcherObserverConfig(class_type=ResidualGraphUpdater),
167
+ ready_operations_filter: Callable[
168
+ [Dispatcher, List[Operation]], List[Operation]
169
+ ] = filter_dominated_operations,
170
+ reward_function_config: DispatcherObserverConfig[
171
+ Type[RewardObserver]
172
+ ] = DispatcherObserverConfig(class_type=MakespanReward),
173
+ render_mode: Optional[str] = None,
174
+ render_config: Optional[RenderConfig] = None,
175
+ use_padding: bool = True,
176
+ ) -> None:
177
+ super().__init__()
178
+
179
+ # Create an instance with the maximum size
180
+ instance_with_max_size = instance_generator.generate(
181
+ num_jobs=instance_generator.max_num_jobs,
182
+ num_machines=instance_generator.max_num_machines,
183
+ )
184
+ graph = graph_initializer(instance_with_max_size)
185
+
186
+ self.single_job_shop_graph_env = SingleJobShopGraphEnv(
187
+ job_shop_graph=graph,
188
+ feature_observer_configs=feature_observer_configs,
189
+ reward_function_config=reward_function_config,
190
+ graph_updater_config=graph_updater_config,
191
+ ready_operations_filter=ready_operations_filter,
192
+ render_mode=render_mode,
193
+ render_config=render_config,
194
+ use_padding=use_padding,
195
+ )
196
+ self.instance_generator = instance_generator
197
+ self.graph_initializer = graph_initializer
198
+ self.render_mode = render_mode
199
+ self.render_config = render_config
200
+ self.feature_observer_configs = feature_observer_configs
201
+ self.reward_function_config = reward_function_config
202
+ self.graph_updater_config = graph_updater_config
203
+
204
+ self.action_space = deepcopy(
205
+ self.single_job_shop_graph_env.action_space
206
+ )
207
+ self.observation_space: gym.spaces.Dict = deepcopy(
208
+ self.single_job_shop_graph_env.observation_space
209
+ )
210
+
211
+ @property
212
+ def dispatcher(self) -> Dispatcher:
213
+ """Returns the current dispatcher instance."""
214
+ return self.single_job_shop_graph_env.dispatcher
215
+
216
+ @property
217
+ def reward_function(self) -> RewardObserver:
218
+ """Returns the current reward function instance."""
219
+ return self.single_job_shop_graph_env.reward_function
220
+
221
+ @reward_function.setter
222
+ def reward_function(self, reward_function: RewardObserver) -> None:
223
+ """Sets the reward function instance."""
224
+ self.single_job_shop_graph_env.reward_function = reward_function
225
+
226
+ @property
227
+ def ready_operations_filter(
228
+ self,
229
+ ) -> Optional[Callable[[Dispatcher, List[Operation]], List[Operation]]]:
230
+ """Returns the current ready operations filter."""
231
+ return (
232
+ self.single_job_shop_graph_env.dispatcher.ready_operations_filter
233
+ )
234
+
235
+ @ready_operations_filter.setter
236
+ def ready_operations_filter(
237
+ self,
238
+ ready_operations_filter: Callable[
239
+ [Dispatcher, List[Operation]], List[Operation]
240
+ ],
241
+ ) -> None:
242
+ """Sets the ready operations filter."""
243
+ self.single_job_shop_graph_env.dispatcher.ready_operations_filter = (
244
+ ready_operations_filter
245
+ )
246
+
247
+ @property
248
+ def use_padding(self) -> bool:
249
+ """Returns whether the padding is used."""
250
+ return self.single_job_shop_graph_env.use_padding
251
+
252
+ @use_padding.setter
253
+ def use_padding(self, use_padding: bool) -> None:
254
+ """Sets whether the padding is used."""
255
+ self.single_job_shop_graph_env.use_padding = use_padding
256
+
257
+ @property
258
+ def job_shop_graph(self) -> JobShopGraph:
259
+ """Returns the current job shop graph."""
260
+ return self.single_job_shop_graph_env.job_shop_graph
261
+
262
+ @property
263
+ def instance(self) -> JobShopInstance:
264
+ """Returns the current job shop instance."""
265
+ return self.single_job_shop_graph_env.instance
266
+
267
+ def reset(
268
+ self,
269
+ *,
270
+ seed: Optional[int] = None,
271
+ options: Dict[str, Any] | None = None,
272
+ ) -> Tuple[ObservationDict, Dict[str, Any]]:
273
+ """Resets the environment and returns the initial observation.
274
+
275
+ Args:
276
+ seed: Random seed for reproducibility.
277
+ options: Additional options for reset (currently unused).
278
+
279
+ Returns:
280
+ A tuple containing:
281
+ - ObservationDict: The initial observation of the environment.
282
+ - dict: An info dictionary containing additional information about
283
+ the reset state. This may include details about the generated
284
+ instance or initial graph structure.
285
+ """
286
+ instance = self.instance_generator.generate()
287
+ graph = self.graph_initializer(instance)
288
+ self.single_job_shop_graph_env = SingleJobShopGraphEnv(
289
+ job_shop_graph=graph,
290
+ feature_observer_configs=self.feature_observer_configs,
291
+ reward_function_config=self.reward_function_config,
292
+ ready_operations_filter=self.ready_operations_filter,
293
+ render_mode=self.render_mode,
294
+ render_config=self.render_config,
295
+ use_padding=self.single_job_shop_graph_env.use_padding,
296
+ )
297
+ obs, info = self.single_job_shop_graph_env.reset(
298
+ seed=seed, options=options
299
+ )
300
+ if self.use_padding:
301
+ obs = self._add_padding_to_observation(obs)
302
+
303
+ return obs, info
304
+
305
+ def step(
306
+ self, action: Tuple[int, int]
307
+ ) -> Tuple[ObservationDict, float, bool, bool, Dict[str, Any]]:
308
+ """Takes a step in the environment.
309
+
310
+ Args:
311
+ action:
312
+ The action to take. The action is a tuple of two integers
313
+ (job_id, machine_id):
314
+ the job ID and the machine ID in which to schedule the
315
+ operation.
316
+
317
+ Returns:
318
+ A tuple containing the following elements:
319
+
320
+ - The observation of the environment.
321
+ - The reward obtained.
322
+ - Whether the environment is done.
323
+ - Whether the episode was truncated (always False).
324
+ - A dictionary with additional information. The dictionary
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).
329
+ """
330
+ obs, reward, done, truncated, info = (
331
+ self.single_job_shop_graph_env.step(action)
332
+ )
333
+ if self.use_padding:
334
+ obs = self._add_padding_to_observation(obs)
335
+
336
+ return obs, reward, done, truncated, info
337
+
338
+ def _add_padding_to_observation(
339
+ self, observation: ObservationDict
340
+ ) -> ObservationDict:
341
+ """Adds padding to the observation.
342
+
343
+ "removed_nodes":
344
+ input_shape: (num_nodes,)
345
+ output_shape: (max_num_nodes,) (padded with True)
346
+ "edge_index":
347
+ input_shape: (2, num_edges)
348
+ output_shape: (2, max_num_edges) (padded with -1)
349
+ "operations":
350
+ input_shape: (num_operations, num_features)
351
+ output_shape: (max_num_operations, num_features) (padded with -1)
352
+ "jobs":
353
+ input_shape: (num_jobs, num_features)
354
+ output_shape: (max_num_jobs, num_features) (padded with -1)
355
+ "machines":
356
+ input_shape: (num_machines, num_features)
357
+ output_shape: (max_num_machines, num_features) (padded with -1)
358
+ """
359
+ padding_value: Dict[str, float | bool] = defaultdict(lambda: -1)
360
+ padding_value[ObservationSpaceKey.REMOVED_NODES.value] = True
361
+ for key, value in observation.items():
362
+ if not isinstance(value, np.ndarray): # Make mypy happy
363
+ continue
364
+ expected_shape = self._get_output_shape(key)
365
+ observation[key] = add_padding( # type: ignore[literal-required]
366
+ value,
367
+ expected_shape,
368
+ padding_value=padding_value[key],
369
+ )
370
+ return observation
371
+
372
+ def _get_output_shape(self, key: str) -> Tuple[int, ...]:
373
+ """Returns the output shape of the observation space key."""
374
+ output_shape = self.observation_space[key].shape
375
+ assert output_shape is not None # Make mypy happy
376
+ return output_shape
377
+
378
+ def render(self) -> None:
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)