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,422 @@
1
+ """Module for creating a GIF or a video of the schedule being built."""
2
+
3
+ import os
4
+ import pathlib
5
+ import shutil
6
+ from typing import Sequence, Protocol, Optional, List, Any
7
+
8
+ import imageio
9
+ import matplotlib.pyplot as plt
10
+ from matplotlib.figure import Figure
11
+ import numpy as np
12
+
13
+ from job_shop_lib import (
14
+ JobShopInstance,
15
+ Schedule,
16
+ Operation,
17
+ ScheduledOperation,
18
+ )
19
+ from job_shop_lib.exceptions import ValidationError
20
+ from job_shop_lib.dispatching import (
21
+ Dispatcher,
22
+ HistoryObserver,
23
+ )
24
+ from job_shop_lib.dispatching.rules import DispatchingRuleSolver
25
+ from job_shop_lib.visualization.gantt._plot_gantt_chart import plot_gantt_chart
26
+
27
+
28
+ # This class serves as a more meaningful type hint than simply:
29
+ # PlotFunction = Callable[
30
+ # [Schedule, int | None, list[Operation] | None, int | None], Figure
31
+ # ]
32
+ # That's why it doesn't have more methods or attributes. It is a protocol
33
+ # for functions, not for classes.
34
+ # pylint: disable=too-few-public-methods
35
+ class PartialGanttChartPlotter(Protocol):
36
+ """A protocol for a function that plots an uncompleted Gantt chart
37
+ for a schedule.
38
+
39
+ This kind of functions are created using the
40
+ :func:`plot_gantt_chart_wrapper` function.
41
+ """
42
+
43
+ def __call__(
44
+ self,
45
+ schedule: Schedule,
46
+ makespan: Optional[int] = None,
47
+ available_operations: Optional[List[Operation]] = None,
48
+ current_time: Optional[int] = None,
49
+ ) -> Figure:
50
+ """Plots a Gantt chart for an unfinished schedule.
51
+
52
+ Args:
53
+ schedule:
54
+ The schedule to plot.
55
+ makespan:
56
+ The makespan of the schedule if known. Can be used to fix
57
+ the x-axis limits.
58
+ available_operations:
59
+ A list of available operations. If ``None``,
60
+ the available operations are not shown.
61
+ current_time:
62
+ The current time in the schedule. If provided, a red
63
+ vertical line is plotted at this time.
64
+ """
65
+
66
+
67
+ def get_partial_gantt_chart_plotter(
68
+ title: Optional[str] = None,
69
+ cmap: str = "viridis",
70
+ show_available_operations: bool = False,
71
+ **kwargs: Any,
72
+ ) -> PartialGanttChartPlotter:
73
+ """Returns a function that plots a Gantt chart for an unfinished schedule.
74
+
75
+ Args:
76
+ title: The title of the Gantt chart.
77
+ cmap: The name of the colormap to use.
78
+ show_available_operations:
79
+ Whether to show the available operations in the Gantt chart.
80
+ **kwargs: Additional keyword arguments to pass to the
81
+ :func:`plot_gantt_chart` function.
82
+
83
+ Returns:
84
+ A function that plots a Gantt chart for a schedule. The function takes
85
+ the following arguments:
86
+
87
+ - schedule: The schedule to plot.
88
+ - makespan: The makespan of the schedule.
89
+ - available_operations: A list of available operations. If None,
90
+ the available operations are not shown.
91
+ - current_time: The current time in the schedule. If provided, a
92
+ red vertical line is plotted at this time.
93
+
94
+ """
95
+
96
+ def plot_function(
97
+ schedule: Schedule,
98
+ makespan: Optional[int] = None,
99
+ available_operations: Optional[List[Operation]] = None,
100
+ current_time: Optional[int] = None,
101
+ ) -> Figure:
102
+ fig, ax = plot_gantt_chart(
103
+ schedule, title=title, cmap_name=cmap, xlim=makespan, **kwargs
104
+ )
105
+
106
+ if show_available_operations and available_operations is not None:
107
+
108
+ operations_text = "\n".join(
109
+ str(operation) for operation in available_operations
110
+ )
111
+ text = f"Available operations:\n{operations_text}"
112
+ # Print the available operations at the bottom right corner
113
+ # of the Gantt chart
114
+ fig.text(
115
+ 1.25,
116
+ 0.05,
117
+ text,
118
+ ha="right",
119
+ va="bottom",
120
+ transform=ax.transAxes,
121
+ bbox={
122
+ "facecolor": "white",
123
+ "alpha": 0.5,
124
+ "boxstyle": "round,pad=0.5",
125
+ },
126
+ )
127
+ if current_time is not None:
128
+ ax.axvline(current_time, color="red", linestyle="--")
129
+ return fig
130
+
131
+ return plot_function
132
+
133
+
134
+ # Most of the arguments are optional with default values. There is no way to
135
+ # reduce the number of arguments without losing functionality.
136
+ # pylint: disable=too-many-arguments
137
+ def create_gantt_chart_gif(
138
+ instance: JobShopInstance,
139
+ gif_path: Optional[str] = None,
140
+ solver: Optional[DispatchingRuleSolver] = None,
141
+ plot_function: Optional[PartialGanttChartPlotter] = None,
142
+ fps: int = 1,
143
+ remove_frames: bool = True,
144
+ frames_dir: Optional[str] = None,
145
+ plot_current_time: bool = True,
146
+ schedule_history: Optional[Sequence[ScheduledOperation]] = None,
147
+ ) -> None:
148
+ """Creates a GIF of the schedule being built.
149
+
150
+ Args:
151
+ instance:
152
+ The instance of the job shop problem to be scheduled.
153
+ gif_path:
154
+ The path to save the GIF file. It should end with ".gif". If not
155
+ provided, the name of the instance is used.
156
+ solver:
157
+ The dispatching rule solver to use. If not provided, the history
158
+ of scheduled operations should be provided.
159
+ plot_function:
160
+ A :class:`PlotFunction` that plots a Gantt chart for a schedule. It
161
+ should take a `Schedule` object and the makespan of the schedule as
162
+ input and return a `Figure` object. If not provided, a default
163
+ function is used.
164
+ fps:
165
+ The number of frames per second in the GIF.
166
+ remove_frames:
167
+ Whether to remove the frames after creating the GIF.
168
+ frames_dir:
169
+ The directory to save the frames in. If not provided,
170
+ ``gif_path.replace(".gif", "") + "_frames"`` is used.
171
+ plot_current_time:
172
+ Whether to plot a vertical line at the current time.
173
+ schedule_history:
174
+ A sequence of scheduled operations. If not provided, the solver
175
+ will be used to generate the history.
176
+ """
177
+ if gif_path is None:
178
+ gif_path = f"{instance.name}_gantt_chart.gif"
179
+
180
+ if plot_function is None:
181
+ plot_function = get_partial_gantt_chart_plotter()
182
+
183
+ if frames_dir is None:
184
+ # Use the name of the GIF file as the directory name
185
+ frames_dir = gif_path.replace(".gif", "") + "_frames"
186
+ path = pathlib.Path(frames_dir)
187
+ path.mkdir(exist_ok=True)
188
+ frames_dir = str(path)
189
+ create_gantt_chart_frames(
190
+ frames_dir,
191
+ instance,
192
+ solver,
193
+ plot_function,
194
+ plot_current_time,
195
+ schedule_history,
196
+ )
197
+ create_gif_from_frames(frames_dir, gif_path, fps)
198
+
199
+ if remove_frames:
200
+ shutil.rmtree(frames_dir)
201
+
202
+
203
+ # Most of the arguments are optional with default values. There is no way to
204
+ # reduce the number of arguments without losing functionality.
205
+ # pylint: disable=too-many-arguments
206
+ def create_gantt_chart_video(
207
+ instance: JobShopInstance,
208
+ video_path: Optional[str] = None,
209
+ solver: Optional[DispatchingRuleSolver] = None,
210
+ plot_function: Optional[PartialGanttChartPlotter] = None,
211
+ fps: int = 1,
212
+ remove_frames: bool = True,
213
+ frames_dir: Optional[str] = None,
214
+ plot_current_time: bool = True,
215
+ schedule_history: Optional[Sequence[ScheduledOperation]] = None,
216
+ ) -> None:
217
+ """Creates a video of the schedule being built.
218
+
219
+ Args:
220
+ instance:
221
+ The instance of the job shop problem to be scheduled.
222
+ video_path:
223
+ The path to save the video file.
224
+ solver:
225
+ The dispatching rule solver to use. If not provided, the history
226
+ of scheduled operations should be provided.
227
+ plot_function:
228
+ A function that plots a Gantt chart for a schedule. It
229
+ should take a :class:`Schedule` object and the makespan of the
230
+ schedule as input and return a ``Figure`` object. If not provided,
231
+ a default function is used.
232
+ fps:
233
+ The number of frames per second in the video.
234
+ remove_frames:
235
+ Whether to remove the frames after creating the video.
236
+ frames_dir:
237
+ The directory to save the frames in. If not provided,
238
+ ``name_without_the_extension + "_frames"`` is used.
239
+ plot_current_time:
240
+ Whether to plot a vertical line at the current time.
241
+ schedule_history:
242
+ A sequence of scheduled operations. If not provided, the solver
243
+ will be used to generate the history.
244
+ """
245
+ if video_path is None:
246
+ video_path = f"{instance.name}_gantt_chart.mp4"
247
+
248
+ if plot_function is None:
249
+ plot_function = get_partial_gantt_chart_plotter()
250
+
251
+ if frames_dir is None:
252
+ extension = video_path.split(".")[-1]
253
+ frames_dir = video_path.replace(f".{extension}", "") + "_frames"
254
+ path = pathlib.Path(frames_dir)
255
+ path.mkdir(exist_ok=True)
256
+ frames_dir = str(path)
257
+ create_gantt_chart_frames(
258
+ frames_dir,
259
+ instance,
260
+ solver,
261
+ plot_function,
262
+ plot_current_time,
263
+ schedule_history,
264
+ )
265
+ create_video_from_frames(frames_dir, video_path, fps)
266
+
267
+ if remove_frames:
268
+ shutil.rmtree(frames_dir)
269
+
270
+
271
+ def create_gantt_chart_frames(
272
+ frames_dir: str,
273
+ instance: JobShopInstance,
274
+ solver: Optional[DispatchingRuleSolver],
275
+ plot_function: PartialGanttChartPlotter,
276
+ plot_current_time: bool = True,
277
+ schedule_history: Optional[Sequence[ScheduledOperation]] = None,
278
+ ) -> None:
279
+ """Creates frames of the Gantt chart for the schedule being built.
280
+
281
+ Args:
282
+ frames_dir:
283
+ The directory to save the frames in.
284
+ instance:
285
+ The instance of the job shop problem to be scheduled.
286
+ solver:
287
+ The dispatching rule solver to use. If not provided, the history
288
+ of scheduled operations should be provided.
289
+ plot_function:
290
+ A function that plots a Gantt chart for a schedule. It
291
+ should take a `Schedule` object and the makespan of the schedule as
292
+ input and return a `Figure` object.
293
+ plot_current_time:
294
+ Whether to plot a vertical line at the current time.
295
+ scheduled_history:
296
+ A sequence of scheduled operations. If not provided, the solver
297
+ """
298
+ if solver is not None and schedule_history is None:
299
+ dispatcher = Dispatcher(
300
+ instance, ready_operations_filter=solver.ready_operations_filter
301
+ )
302
+ history_tracker = HistoryObserver(dispatcher)
303
+ makespan = solver.solve(instance, dispatcher).makespan()
304
+ dispatcher.unsubscribe(history_tracker)
305
+ dispatcher.reset()
306
+ schedule_history = history_tracker.history
307
+ elif schedule_history is not None and solver is None:
308
+ dispatcher = Dispatcher(instance)
309
+ makespan = max(
310
+ scheduled_operation.end_time
311
+ for scheduled_operation in schedule_history
312
+ )
313
+ elif schedule_history is not None and solver is not None:
314
+ raise ValidationError(
315
+ "Only one of 'solver' and 'history' should be provided."
316
+ )
317
+ else:
318
+ raise ValidationError(
319
+ "Either 'solver' or 'history' should be provided."
320
+ )
321
+
322
+ for i, scheduled_operation in enumerate(schedule_history, start=1):
323
+ dispatcher.dispatch(
324
+ scheduled_operation.operation, scheduled_operation.machine_id
325
+ )
326
+ current_time = (
327
+ None if not plot_current_time else dispatcher.current_time()
328
+ )
329
+ fig = plot_function(
330
+ dispatcher.schedule,
331
+ makespan,
332
+ dispatcher.available_operations(),
333
+ current_time,
334
+ )
335
+ _save_frame(fig, frames_dir, i)
336
+
337
+
338
+ def _save_frame(figure: Figure, frames_dir: str, number: int) -> None:
339
+ figure.savefig(f"{frames_dir}/frame_{number:02d}.png", bbox_inches="tight")
340
+ plt.close(figure)
341
+
342
+
343
+ def create_gif_from_frames(
344
+ frames_dir: str, gif_path: str, fps: int, loop: int = 0
345
+ ) -> None:
346
+ """Creates a GIF or video from the frames in the given directory.
347
+
348
+ Args:
349
+ frames_dir:
350
+ The directory containing the frames to be used in the GIF.
351
+ gif_path:
352
+ The path to save the GIF file. It should end with ".gif".
353
+ fps:
354
+ The number of frames per second in the GIF.
355
+ loop:
356
+ The number of times the GIF should loop. Default is 0, which means
357
+ the GIF will loop indefinitely. If set to 1, the GIF will loop
358
+ once. Added in version 0.6.0.
359
+ """
360
+ images = _load_images(frames_dir)
361
+ imageio.mimsave(gif_path, images, fps=fps, loop=loop)
362
+
363
+
364
+ def create_video_from_frames(
365
+ frames_dir: str, gif_path: str, fps: int, macro_block_size: int = 16
366
+ ) -> None:
367
+ """Creates a GIF or video from the frames in the given directory.
368
+
369
+ Args:
370
+ frames_dir:
371
+ The directory containing the frames to be used in the video.
372
+ gif_path:
373
+ The path to save the video.
374
+ fps:
375
+ The number of frames per second.
376
+ """
377
+ images = _load_images(frames_dir)
378
+ resized_images = [
379
+ resize_image_to_macro_block(image, macro_block_size=macro_block_size)
380
+ for image in images
381
+ ]
382
+ imageio.mimsave(
383
+ gif_path, resized_images, fps=fps # type: ignore[arg-type]
384
+ )
385
+
386
+
387
+ def resize_image_to_macro_block(
388
+ image: np.ndarray, macro_block_size: int = 16
389
+ ) -> np.ndarray:
390
+ """Resizes the image to ensure its dimensions are divisible by the macro
391
+ block size.
392
+
393
+ Args:
394
+ image (numpy.ndarray): The input image.
395
+ macro_block_size (int): The macro block size, typically 16.
396
+
397
+ Returns:
398
+ numpy.ndarray: The resized image.
399
+ """
400
+ height, width, channels = image.shape
401
+ new_height = (
402
+ (height + macro_block_size - 1) // macro_block_size * macro_block_size
403
+ )
404
+ new_width = (
405
+ (width + macro_block_size - 1) // macro_block_size * macro_block_size
406
+ )
407
+
408
+ if (new_height, new_width) != (height, width):
409
+ resized_image = np.zeros(
410
+ (new_height, new_width, channels), dtype=image.dtype
411
+ )
412
+ resized_image[:height, :width] = image
413
+ return resized_image
414
+ return image
415
+
416
+
417
+ def _load_images(frames_dir: str) -> List:
418
+ frames = [
419
+ os.path.join(frames_dir, frame)
420
+ for frame in sorted(os.listdir(frames_dir))
421
+ ]
422
+ return [imageio.imread(frame) for frame in frames]
@@ -1,6 +1,6 @@
1
1
  """Module for plotting static Gantt charts for job shop schedules."""
2
2
 
3
- from typing import Optional
3
+ from typing import Optional, List, Tuple, Dict
4
4
 
5
5
  from matplotlib.figure import Figure
6
6
  import matplotlib.pyplot as plt
@@ -9,27 +9,49 @@ from matplotlib.patches import Patch
9
9
 
10
10
  from job_shop_lib import Schedule, ScheduledOperation
11
11
 
12
-
13
12
  _BASE_Y_POSITION = 1
14
13
  _Y_POSITION_INCREMENT = 10
15
14
 
16
15
 
17
16
  def plot_gantt_chart(
18
17
  schedule: Schedule,
19
- title: str | None = None,
18
+ title: Optional[str] = None,
20
19
  cmap_name: str = "viridis",
21
- xlim: int | None = None,
20
+ xlim: Optional[int] = None,
22
21
  number_of_x_ticks: int = 15,
23
- ) -> tuple[Figure, plt.Axes]:
22
+ job_labels: Optional[List[str]] = None,
23
+ machine_labels: Optional[List[str]] = None,
24
+ legend_title: str = "",
25
+ x_label: str = "Time units",
26
+ y_label: str = "Machines",
27
+ ) -> Tuple[Figure, plt.Axes]:
24
28
  """Plots a Gantt chart for the schedule.
25
29
 
30
+ This function generates a Gantt chart that visualizes the schedule of jobs
31
+ across multiple machines. Each job is represented with a unique color,
32
+ and operations are plotted as bars on the corresponding machines over time.
33
+
34
+ The Gantt chart helps to understand the flow of jobs on machines and
35
+ visualize the makespan of the schedule, i.e., the time it takes to
36
+ complete all jobs.
37
+
38
+ The Gantt chart includes:
39
+
40
+ - X-axis: Time units, representing the progression of the schedule.
41
+ - Y-axis: Machines, which are assigned jobs at various time slots.
42
+ - Legend: A list of jobs, labeled and color-coded for clarity.
43
+
44
+ .. note::
45
+ The last tick on the x-axis always represents the makespan for easy
46
+ identification of the completion time.
47
+
26
48
  Args:
27
49
  schedule:
28
50
  The schedule to plot.
29
51
  title:
30
52
  The title of the plot. If not provided, the title:
31
- `f"Gantt Chart for {schedule.instance.name} instance"`
32
- is used.
53
+ ``f"Gantt Chart for {schedule.instance.name} instance"``
54
+ is used. To remove the title, provide an empty string.
33
55
  cmap_name:
34
56
  The name of the colormap to use. Default is "viridis".
35
57
  xlim:
@@ -37,21 +59,45 @@ def plot_gantt_chart(
37
59
  the schedule is used.
38
60
  number_of_x_ticks:
39
61
  The number of ticks to use in the x-axis.
62
+ job_labels:
63
+ A list of labels for each job. If ``None``, the labels are
64
+ automatically generated as "Job 0", "Job 1", etc.
65
+ machine_labels:
66
+ A list of labels for each machine. If ``None``, the labels are
67
+ automatically generated as "0", "1", etc.
68
+ legend_title:
69
+ The title of the legend. If not provided, the legend will not have
70
+ a title.
71
+ x_label:
72
+ The label for the x-axis. Default is "Time units". To remove the
73
+ label, provide an empty string.
74
+ y_label:
75
+ The label for the y-axis. Default is "Machines". To remove the
76
+ label, provide an empty string.
77
+
78
+ Returns:
79
+ - A ``matplotlib.figure.Figure`` object.
80
+ - A ``matplotlib.axes.Axes`` object where the Gantt chart is plotted.
40
81
  """
41
- fig, ax = _initialize_plot(schedule, title)
42
- legend_handles = _plot_machine_schedules(schedule, ax, cmap_name)
43
- _configure_legend(ax, legend_handles)
44
- _configure_axes(schedule, ax, xlim, number_of_x_ticks)
82
+ fig, ax = _initialize_plot(schedule, title, x_label, y_label)
83
+ legend_handles = _plot_machine_schedules(
84
+ schedule, ax, cmap_name, job_labels
85
+ )
86
+ _configure_legend(ax, legend_handles, legend_title)
87
+ _configure_axes(schedule, ax, xlim, number_of_x_ticks, machine_labels)
45
88
  return fig, ax
46
89
 
47
90
 
48
91
  def _initialize_plot(
49
- schedule: Schedule, title: str | None
50
- ) -> tuple[Figure, plt.Axes]:
92
+ schedule: Schedule,
93
+ title: Optional[str],
94
+ x_label: str = "Time units",
95
+ y_label: str = "Machines",
96
+ ) -> Tuple[Figure, plt.Axes]:
51
97
  """Initializes the plot."""
52
98
  fig, ax = plt.subplots()
53
- ax.set_xlabel("Time units")
54
- ax.set_ylabel("Machines")
99
+ ax.set_xlabel(x_label)
100
+ ax.set_ylabel(y_label)
55
101
  ax.grid(True, which="both", axis="x", linestyle="--", linewidth=0.5)
56
102
  ax.yaxis.grid(False)
57
103
  if title is None:
@@ -61,8 +107,11 @@ def _initialize_plot(
61
107
 
62
108
 
63
109
  def _plot_machine_schedules(
64
- schedule: Schedule, ax: plt.Axes, cmap_name: str
65
- ) -> dict[int, Patch]:
110
+ schedule: Schedule,
111
+ ax: plt.Axes,
112
+ cmap_name: str,
113
+ job_labels: Optional[List[str]],
114
+ ) -> Dict[int, Patch]:
66
115
  """Plots the schedules for each machine."""
67
116
  max_job_id = schedule.instance.num_jobs - 1
68
117
  cmap = plt.get_cmap(cmap_name, max_job_id + 1)
@@ -81,12 +130,20 @@ def _plot_machine_schedules(
81
130
  )
82
131
  if scheduled_op.job_id not in legend_handles:
83
132
  legend_handles[scheduled_op.job_id] = Patch(
84
- facecolor=color, label=f"Job {scheduled_op.job_id}"
133
+ facecolor=color,
134
+ label=_get_job_label(job_labels, scheduled_op.job_id),
85
135
  )
86
136
 
87
137
  return legend_handles
88
138
 
89
139
 
140
+ def _get_job_label(job_labels: Optional[List[str]], job_id: int) -> str:
141
+ """Returns the label for the job."""
142
+ if job_labels is None:
143
+ return f"Job {job_id}"
144
+ return job_labels[job_id]
145
+
146
+
90
147
  def _plot_scheduled_operation(
91
148
  ax: plt.Axes,
92
149
  scheduled_op: ScheduledOperation,
@@ -103,7 +160,9 @@ def _plot_scheduled_operation(
103
160
  )
104
161
 
105
162
 
106
- def _configure_legend(ax: plt.Axes, legend_handles: dict[int, Patch]):
163
+ def _configure_legend(
164
+ ax: plt.Axes, legend_handles: Dict[int, Patch], legend_title: str
165
+ ):
107
166
  """Configures the legend for the plot."""
108
167
  sorted_legend_handles = [
109
168
  legend_handles[job_id] for job_id in sorted(legend_handles)
@@ -111,7 +170,8 @@ def _configure_legend(ax: plt.Axes, legend_handles: dict[int, Patch]):
111
170
  ax.legend(
112
171
  handles=sorted_legend_handles,
113
172
  loc="upper left",
114
- bbox_to_anchor=(1.01, 1),
173
+ bbox_to_anchor=(1, 1),
174
+ title=legend_title,
115
175
  )
116
176
 
117
177
 
@@ -120,6 +180,7 @@ def _configure_axes(
120
180
  ax: plt.Axes,
121
181
  xlim: Optional[int],
122
182
  number_of_x_ticks: int,
183
+ machine_labels: list[str] | None,
123
184
  ):
124
185
  """Sets the limits and labels for the axes."""
125
186
  num_machines = len(schedule.schedule)
@@ -132,7 +193,9 @@ def _configure_axes(
132
193
  for i in range(num_machines)
133
194
  ]
134
195
  )
135
- ax.set_yticklabels([str(i) for i in range(num_machines)])
196
+ if machine_labels is None:
197
+ machine_labels = [str(i) for i in range(num_machines)]
198
+ ax.set_yticklabels(machine_labels)
136
199
  makespan = schedule.makespan()
137
200
  xlim = xlim if xlim is not None else makespan
138
201
  ax.set_xlim(0, xlim)
@@ -0,0 +1,29 @@
1
+ """Contains functions and classes for visualizing job shop scheduling problems.
2
+
3
+ .. autosummary::
4
+
5
+ plot_disjunctive_graph
6
+ plot_resource_task_graph
7
+ three_columns_layout
8
+ duration_labeler
9
+ color_nodes_by_machine
10
+
11
+ """
12
+
13
+ from ._plot_disjunctive_graph import (
14
+ plot_disjunctive_graph,
15
+ duration_labeler,
16
+ )
17
+ from ._plot_resource_task_graph import (
18
+ plot_resource_task_graph,
19
+ three_columns_layout,
20
+ color_nodes_by_machine,
21
+ )
22
+
23
+ __all__ = [
24
+ "plot_disjunctive_graph",
25
+ "plot_resource_task_graph",
26
+ "three_columns_layout",
27
+ "duration_labeler",
28
+ "color_nodes_by_machine",
29
+ ]