job-shop-lib 0.5.1__py3-none-any.whl → 1.0.0a1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. job_shop_lib/__init__.py +16 -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} +9 -4
  4. job_shop_lib/_operation.py +95 -0
  5. job_shop_lib/{schedule.py → _schedule.py} +73 -54
  6. job_shop_lib/{scheduled_operation.py → _scheduled_operation.py} +13 -37
  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} +57 -18
  11. job_shop_lib/dispatching/__init__.py +45 -41
  12. job_shop_lib/dispatching/{dispatcher.py → _dispatcher.py} +153 -80
  13. job_shop_lib/dispatching/_dispatcher_observer_config.py +54 -0
  14. job_shop_lib/dispatching/_factories.py +125 -0
  15. job_shop_lib/dispatching/{history_tracker.py → _history_observer.py} +4 -6
  16. job_shop_lib/dispatching/{pruning_functions.py → _ready_operation_filters.py} +6 -35
  17. job_shop_lib/dispatching/_unscheduled_operations_observer.py +69 -0
  18. job_shop_lib/dispatching/feature_observers/__init__.py +16 -10
  19. job_shop_lib/dispatching/feature_observers/{composite_feature_observer.py → _composite_feature_observer.py} +84 -2
  20. job_shop_lib/dispatching/feature_observers/{duration_observer.py → _duration_observer.py} +6 -17
  21. job_shop_lib/dispatching/feature_observers/{earliest_start_time_observer.py → _earliest_start_time_observer.py} +114 -35
  22. job_shop_lib/dispatching/feature_observers/{factory.py → _factory.py} +31 -5
  23. job_shop_lib/dispatching/feature_observers/{feature_observer.py → _feature_observer.py} +59 -16
  24. job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +97 -0
  25. job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +33 -0
  26. job_shop_lib/dispatching/feature_observers/{position_in_job_observer.py → _position_in_job_observer.py} +1 -8
  27. job_shop_lib/dispatching/feature_observers/{remaining_operations_observer.py → _remaining_operations_observer.py} +8 -26
  28. job_shop_lib/dispatching/rules/__init__.py +51 -0
  29. job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +82 -0
  30. job_shop_lib/dispatching/{dispatching_rule_solver.py → rules/_dispatching_rule_solver.py} +44 -15
  31. job_shop_lib/dispatching/{dispatching_rules.py → rules/_dispatching_rules_functions.py} +74 -21
  32. job_shop_lib/dispatching/rules/_machine_chooser_factory.py +69 -0
  33. job_shop_lib/dispatching/rules/_utils.py +127 -0
  34. job_shop_lib/exceptions.py +18 -0
  35. job_shop_lib/generation/__init__.py +2 -2
  36. job_shop_lib/generation/{general_instance_generator.py → _general_instance_generator.py} +26 -7
  37. job_shop_lib/generation/{instance_generator.py → _instance_generator.py} +13 -3
  38. job_shop_lib/graphs/__init__.py +17 -6
  39. job_shop_lib/graphs/{job_shop_graph.py → _job_shop_graph.py} +81 -2
  40. job_shop_lib/graphs/{node.py → _node.py} +18 -12
  41. job_shop_lib/graphs/graph_updaters/__init__.py +13 -0
  42. job_shop_lib/graphs/graph_updaters/_graph_updater.py +59 -0
  43. job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +154 -0
  44. job_shop_lib/graphs/graph_updaters/_utils.py +25 -0
  45. job_shop_lib/reinforcement_learning/__init__.py +41 -0
  46. job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +366 -0
  47. job_shop_lib/reinforcement_learning/_reward_observers.py +85 -0
  48. job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +337 -0
  49. job_shop_lib/reinforcement_learning/_types_and_constants.py +61 -0
  50. job_shop_lib/reinforcement_learning/_utils.py +96 -0
  51. job_shop_lib/visualization/__init__.py +20 -4
  52. job_shop_lib/visualization/{agent_task_graph.py → _agent_task_graph.py} +28 -9
  53. job_shop_lib/visualization/_gantt_chart_creator.py +219 -0
  54. job_shop_lib/visualization/_gantt_chart_video_and_gif_creation.py +388 -0
  55. {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0a1.dist-info}/METADATA +68 -44
  56. job_shop_lib-1.0.0a1.dist-info/RECORD +66 -0
  57. job_shop_lib/benchmarking/load_benchmark.py +0 -142
  58. job_shop_lib/cp_sat/__init__.py +0 -5
  59. job_shop_lib/dispatching/factories.py +0 -206
  60. job_shop_lib/dispatching/feature_observers/is_completed_observer.py +0 -98
  61. job_shop_lib/dispatching/feature_observers/is_ready_observer.py +0 -40
  62. job_shop_lib/generators/__init__.py +0 -8
  63. job_shop_lib/generators/basic_generator.py +0 -200
  64. job_shop_lib/generators/transformations.py +0 -164
  65. job_shop_lib/operation.py +0 -122
  66. job_shop_lib/visualization/create_gif.py +0 -209
  67. job_shop_lib-0.5.1.dist-info/RECORD +0 -52
  68. /job_shop_lib/dispatching/feature_observers/{is_scheduled_observer.py → _is_scheduled_observer.py} +0 -0
  69. /job_shop_lib/generation/{transformations.py → _transformations.py} +0 -0
  70. /job_shop_lib/graphs/{build_agent_task_graph.py → _build_agent_task_graph.py} +0 -0
  71. /job_shop_lib/graphs/{build_disjunctive_graph.py → _build_disjunctive_graph.py} +0 -0
  72. /job_shop_lib/graphs/{constants.py → _constants.py} +0 -0
  73. /job_shop_lib/visualization/{disjunctive_graph.py → _disjunctive_graph.py} +0 -0
  74. /job_shop_lib/visualization/{gantt_chart.py → _gantt_chart.py} +0 -0
  75. {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0a1.dist-info}/LICENSE +0 -0
  76. {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0a1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,219 @@
1
+ """Home of the `GanttChartCreator` class and its configuration types."""
2
+
3
+ from typing import TypedDict
4
+
5
+ import matplotlib.pyplot as plt
6
+
7
+ from job_shop_lib.dispatching import (
8
+ Dispatcher,
9
+ HistoryObserver,
10
+ )
11
+ from job_shop_lib.visualization import (
12
+ create_gantt_chart_video,
13
+ plot_gantt_chart_wrapper,
14
+ create_gif,
15
+ )
16
+
17
+
18
+ class GanttChartWrapperConfig(TypedDict, total=False):
19
+ """Configuration for creating the plot function with the
20
+ `plot_gantt_chart_wrapper` function."""
21
+
22
+ title: str | None
23
+ cmap: str
24
+ show_available_operations: bool
25
+
26
+
27
+ # We can't use Required here because it's not available in Python 3.10
28
+ class _GifConfigRequired(TypedDict):
29
+ """Required configuration for creating the GIF."""
30
+
31
+ gif_path: str | None
32
+
33
+
34
+ class _GifConfigOptional(TypedDict, total=False):
35
+ """Optional configuration for creating the GIF."""
36
+
37
+ fps: int
38
+ remove_frames: bool
39
+ frames_dir: str | None
40
+ plot_current_time: bool
41
+
42
+
43
+ class GifConfig(_GifConfigRequired, _GifConfigOptional):
44
+ """Configuration for creating the GIF using the `create_gannt_chart_video`
45
+ function."""
46
+
47
+
48
+ class VideoConfig(TypedDict, total=False):
49
+ """Configuration for creating the video using the
50
+ `create_gannt_chart_video` function."""
51
+
52
+ video_path: str | None
53
+ fps: int
54
+ remove_frames: bool
55
+ frames_dir: str | None
56
+ plot_current_time: bool
57
+
58
+
59
+ class GanttChartCreator:
60
+ """Facade class that centralizes the creation of Gantt charts, videos
61
+ and GIFs.
62
+
63
+ It leverages a `HistoryObserver` to keep track of the operations being
64
+ scheduled and provides methods to plot the current state
65
+ of the schedule as a Gantt chart and to create a GIF that shows the
66
+ evolution of the schedule over time.
67
+
68
+ It adds a new `HistoryObserver` to the dispatcher if it does
69
+ not have one already. Otherwise, it uses the observer already present.
70
+
71
+ Attributes:
72
+ history_observer:
73
+ The history observer observing the dispatcher's state.
74
+ gantt_chart_config:
75
+ Configuration for plotting the Gantt chart.
76
+ gif_config:
77
+ Configuration for creating the GIF.
78
+ gantt_chart_wrapper_config:
79
+ Configuration for the Gantt chart wrapper function.
80
+ video_config:
81
+ Configuration for creating the video.
82
+ plot_function:
83
+ The function used to plot the Gantt chart when creating the GIF
84
+ or video. Created using the `plot_gantt_chart_wrapper` function.
85
+ """
86
+
87
+ def __init__(
88
+ self,
89
+ dispatcher: Dispatcher,
90
+ gantt_chart_wrapper_config: GanttChartWrapperConfig | None = None,
91
+ gif_config: GifConfig | None = None,
92
+ video_config: VideoConfig | None = None,
93
+ ):
94
+ """Initializes the GanttChartCreator with the given configurations
95
+ and history observer.
96
+
97
+ This class adds a new `HistoryObserver` to the dispatcher if it does
98
+ not have one already. Otherwise, it uses the observer already present.
99
+
100
+ Args:
101
+ dispatcher:
102
+ The `Dispatcher` class that will be tracked using a
103
+ `HistoryObserver`.
104
+ gantt_chart_wrapper_config:
105
+ Configuration for the Gantt chart wrapper function. Valid keys
106
+ are:
107
+ - title: The title of the Gantt chart.
108
+ - cmap: The name of the colormap to use.
109
+ - show_available_operations: Whether to show available
110
+ operations in each step.
111
+
112
+ If `title` or `cmap` are not provided here and
113
+ `infer_gantt_chart_config` is set to True, the values from
114
+ `gantt_chart_config` will be used if they are present.
115
+ gif_config:
116
+ Configuration for creating the GIF. Defaults to None.
117
+ Valid keys are:
118
+ - gif_path: The path to save the GIF.
119
+ - fps: The frames per second of the GIF.
120
+ - remove_frames: Whether to remove the frames after creating
121
+ the GIF.
122
+ - frames_dir: The directory to store the frames.
123
+ - plot_current_time: Whether to plot the current time in the
124
+ Gantt chart.
125
+ video_config:
126
+ Configuration for creating the video. Defaults to None.
127
+ Valid keys are:
128
+ - video_path: The path to save the video.
129
+ - fps: The frames per second of the video.
130
+ - remove_frames: Whether to remove the frames after creating
131
+ the video.
132
+ - frames_dir: The directory to store the frames.
133
+ - plot_current_time: Whether to plot the current time in the
134
+ Gantt chart.
135
+ """
136
+ if gif_config is None:
137
+ gif_config = {"gif_path": None}
138
+ if gantt_chart_wrapper_config is None:
139
+ gantt_chart_wrapper_config = {}
140
+ if video_config is None:
141
+ video_config = {}
142
+
143
+ self.gif_config = gif_config
144
+ self.gannt_chart_wrapper_config = gantt_chart_wrapper_config
145
+ self.video_config = video_config
146
+ self.history_observer: HistoryObserver = (
147
+ dispatcher.create_or_get_observer(HistoryObserver)
148
+ )
149
+ self.plot_function = plot_gantt_chart_wrapper(
150
+ **self.gannt_chart_wrapper_config
151
+ )
152
+
153
+ @property
154
+ def instance(self):
155
+ """The instance being scheduled."""
156
+ return self.history_observer.dispatcher.instance
157
+
158
+ @property
159
+ def schedule(self):
160
+ """The current schedule."""
161
+ return self.history_observer.dispatcher.schedule
162
+
163
+ @property
164
+ def dispatcher(self):
165
+ """The dispatcher being observed."""
166
+ return self.history_observer.dispatcher
167
+
168
+ def plot_gantt_chart(self) -> plt.Figure:
169
+ """Plots the current Gantt chart of the schedule.
170
+
171
+ Returns:
172
+ tuple[plt.Figure, plt.Axes]:
173
+ The figure and axes of the plotted Gantt chart.
174
+ """
175
+ return self.plot_function(
176
+ self.schedule,
177
+ None,
178
+ self.dispatcher.ready_operations(),
179
+ self.dispatcher.current_time(),
180
+ )
181
+
182
+ def create_gif(self) -> None:
183
+ """Creates a GIF of the schedule being built using the recorded
184
+ history.
185
+
186
+ This method uses the history of scheduled operations recorded by the
187
+ `HistoryTracker` to create a GIF that shows the progression of the
188
+ scheduling process.
189
+
190
+ The GIF creation process involves:
191
+ - Using the history of scheduled operations to generate frames for
192
+ each step of the schedule.
193
+ - Creating a GIF from these frames.
194
+ - Optionally, removing the frames after the GIF is created.
195
+
196
+ The configuration for the GIF creation can be customized through the
197
+ `gif_config` attribute.
198
+ """
199
+ create_gif(
200
+ instance=self.history_observer.dispatcher.instance,
201
+ schedule_history=self.history_observer.history,
202
+ plot_function=self.plot_function,
203
+ **self.gif_config
204
+ )
205
+
206
+ def create_video(self) -> None:
207
+ """Creates a video of the schedule being built using the recorded
208
+ history.
209
+
210
+ This method uses the history of scheduled operations recorded by the
211
+ `HistoryTracker` to create a video that shows the progression of the
212
+ scheduling process.
213
+ """
214
+ create_gantt_chart_video(
215
+ instance=self.history_observer.dispatcher.instance,
216
+ schedule_history=self.history_observer.history,
217
+ plot_function=self.plot_function,
218
+ **self.video_config
219
+ )
@@ -0,0 +1,388 @@
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 collections.abc import Callable
7
+ from typing import Sequence
8
+
9
+ import imageio
10
+ import matplotlib.pyplot as plt
11
+ from matplotlib.figure import Figure
12
+ import numpy as np
13
+
14
+ from job_shop_lib import (
15
+ JobShopInstance,
16
+ Schedule,
17
+ Operation,
18
+ ScheduledOperation,
19
+ )
20
+ from job_shop_lib.exceptions import ValidationError
21
+ from job_shop_lib.dispatching import (
22
+ Dispatcher,
23
+ HistoryObserver,
24
+ )
25
+ from job_shop_lib.dispatching.rules import DispatchingRuleSolver
26
+ from job_shop_lib.visualization._gantt_chart import plot_gantt_chart
27
+
28
+
29
+ PlotFunction = Callable[
30
+ [Schedule, int | None, list[Operation] | None, int | None], Figure
31
+ ]
32
+
33
+
34
+ def plot_gantt_chart_wrapper(
35
+ title: str | None = None,
36
+ cmap: str = "viridis",
37
+ show_available_operations: bool = False,
38
+ ) -> PlotFunction:
39
+ """Returns a function that plots a Gantt chart for an unfinished schedule.
40
+
41
+ Args:
42
+ title: The title of the Gantt chart.
43
+ cmap: The name of the colormap to use.
44
+ show_available_operations:
45
+ Whether to show the available operations in the Gantt chart.
46
+
47
+ Returns:
48
+ A function that plots a Gantt chart for a schedule. The function takes
49
+ the following arguments:
50
+ - schedule: The schedule to plot.
51
+ - makespan: The makespan of the schedule.
52
+ - available_operations: A list of available operations. If None,
53
+ the available operations are not shown.
54
+ - current_time: The current time in the schedule. If provided, a
55
+ red vertical line is plotted at this time.
56
+
57
+ """
58
+
59
+ def plot_function(
60
+ schedule: Schedule,
61
+ makespan: int | None = None,
62
+ available_operations: list | None = None,
63
+ current_time: int | None = None,
64
+ ) -> Figure:
65
+ fig, ax = plot_gantt_chart(
66
+ schedule, title=title, cmap_name=cmap, xlim=makespan
67
+ )
68
+
69
+ if show_available_operations and available_operations is not None:
70
+
71
+ operations_text = "\n".join(
72
+ str(operation) for operation in available_operations
73
+ )
74
+ text = f"Available operations:\n{operations_text}"
75
+ # Print the available operations at the bottom right corner
76
+ # of the Gantt chart
77
+ fig.text(
78
+ 1.25,
79
+ 0.05,
80
+ text,
81
+ ha="right",
82
+ va="bottom",
83
+ transform=ax.transAxes,
84
+ bbox={
85
+ "facecolor": "white",
86
+ "alpha": 0.5,
87
+ "boxstyle": "round,pad=0.5",
88
+ },
89
+ )
90
+ if current_time is not None:
91
+ ax.axvline(current_time, color="red", linestyle="--")
92
+ return fig
93
+
94
+ return plot_function
95
+
96
+
97
+ # Most of the arguments are optional with default values. There is no way to
98
+ # reduce the number of arguments without losing functionality.
99
+ # pylint: disable=too-many-arguments
100
+ def create_gif(
101
+ gif_path: str | None,
102
+ instance: JobShopInstance,
103
+ solver: DispatchingRuleSolver | None = None,
104
+ plot_function: PlotFunction | None = None,
105
+ fps: int = 1,
106
+ remove_frames: bool = True,
107
+ frames_dir: str | None = None,
108
+ plot_current_time: bool = True,
109
+ schedule_history: Sequence[ScheduledOperation] | None = None,
110
+ ) -> None:
111
+ """Creates a GIF of the schedule being built by the given solver.
112
+
113
+ Deprecated since version 0.6.0: Use `create_gif_or_video` instead.
114
+
115
+ Args:
116
+ gif_path:
117
+ The path to save the GIF file. It should end with ".gif". If not
118
+ provided, the name of the instance is used. It will be made an
119
+ optional argument in version 1.0.0.
120
+ instance:
121
+ The instance of the job shop problem to be scheduled.
122
+ solver:
123
+ The dispatching rule solver to use. If not provided, the history
124
+ of scheduled operations should be provided.
125
+ plot_function:
126
+ A function that plots a Gantt chart for a schedule. It
127
+ should take a `Schedule` object and the makespan of the schedule as
128
+ input and return a `Figure` object. If not provided, a default
129
+ function is used.
130
+ fps:
131
+ The number of frames per second in the GIF.
132
+ remove_frames:
133
+ Whether to remove the frames after creating the GIF.
134
+ frames_dir:
135
+ The directory to save the frames in. If not provided,
136
+ `gif_path.replace(".gif", "") + "_frames"` is used.
137
+ plot_current_time:
138
+ Whether to plot a vertical line at the current time.
139
+ schedule_history:
140
+ A sequence of scheduled operations. If not provided, the solver
141
+ will be used to generate the history.
142
+ """
143
+ if gif_path is None:
144
+ gif_path = f"{instance.name}_gantt_chart.gif"
145
+
146
+ if plot_function is None:
147
+ plot_function = plot_gantt_chart_wrapper()
148
+
149
+ if frames_dir is None:
150
+ # Use the name of the GIF file as the directory name
151
+ frames_dir = gif_path.replace(".gif", "") + "_frames"
152
+ path = pathlib.Path(frames_dir)
153
+ path.mkdir(exist_ok=True)
154
+ frames_dir = str(path)
155
+ create_gantt_chart_frames(
156
+ frames_dir,
157
+ instance,
158
+ solver,
159
+ plot_function,
160
+ plot_current_time,
161
+ schedule_history,
162
+ )
163
+ create_gif_from_frames(frames_dir, gif_path, fps)
164
+
165
+ if remove_frames:
166
+ shutil.rmtree(frames_dir)
167
+
168
+
169
+ # Most of the arguments are optional with default values. There is no way to
170
+ # reduce the number of arguments without losing functionality.
171
+ # pylint: disable=too-many-arguments
172
+ def create_gantt_chart_video(
173
+ instance: JobShopInstance,
174
+ video_path: str | None = None,
175
+ solver: DispatchingRuleSolver | None = None,
176
+ plot_function: PlotFunction | None = None,
177
+ fps: int = 1,
178
+ remove_frames: bool = True,
179
+ frames_dir: str | None = None,
180
+ plot_current_time: bool = True,
181
+ schedule_history: Sequence[ScheduledOperation] | None = None,
182
+ ) -> None:
183
+ """Creates a GIF of the schedule being built by the given solver.
184
+
185
+ Args:
186
+ instance:
187
+ The instance of the job shop problem to be scheduled.
188
+ video_path:
189
+ The path to save the video file.
190
+ solver:
191
+ The dispatching rule solver to use. If not provided, the history
192
+ of scheduled operations should be provided.
193
+ plot_function:
194
+ A function that plots a Gantt chart for a schedule. It
195
+ should take a `Schedule` object and the makespan of the schedule as
196
+ input and return a `Figure` object. If not provided, a default
197
+ function is used.
198
+ fps:
199
+ The number of frames per second in the GIF.
200
+ remove_frames:
201
+ Whether to remove the frames after creating the GIF.
202
+ frames_dir:
203
+ The directory to save the frames in. If not provided,
204
+ `name_without_the_extension` + "_frames"` is used.
205
+ plot_current_time:
206
+ Whether to plot a vertical line at the current time.
207
+ schedule_history:
208
+ A sequence of scheduled operations. If not provided, the solver
209
+ will be used to generate the history.
210
+ """
211
+ if video_path is None:
212
+ video_path = f"{instance.name}_gantt_chart.mp4"
213
+
214
+ if plot_function is None:
215
+ plot_function = plot_gantt_chart_wrapper()
216
+
217
+ if frames_dir is None:
218
+ extension = video_path.split(".")[-1]
219
+ frames_dir = video_path.replace(f".{extension}", "") + "_frames"
220
+ path = pathlib.Path(frames_dir)
221
+ path.mkdir(exist_ok=True)
222
+ frames_dir = str(path)
223
+ create_gantt_chart_frames(
224
+ frames_dir,
225
+ instance,
226
+ solver,
227
+ plot_function,
228
+ plot_current_time,
229
+ schedule_history,
230
+ )
231
+ create_video_from_frames(frames_dir, video_path, fps)
232
+
233
+ if remove_frames:
234
+ shutil.rmtree(frames_dir)
235
+
236
+
237
+ def create_gantt_chart_frames(
238
+ frames_dir: str,
239
+ instance: JobShopInstance,
240
+ solver: DispatchingRuleSolver | None,
241
+ plot_function: PlotFunction,
242
+ plot_current_time: bool = True,
243
+ schedule_history: Sequence[ScheduledOperation] | None = None,
244
+ ) -> None:
245
+ """Creates frames of the Gantt chart for the schedule being built.
246
+
247
+ Args:
248
+ frames_dir:
249
+ The directory to save the frames in.
250
+ instance:
251
+ The instance of the job shop problem to be scheduled.
252
+ solver:
253
+ The dispatching rule solver to use. If not provided, the history
254
+ of scheduled operations should be provided.
255
+ plot_function:
256
+ A function that plots a Gantt chart for a schedule. It
257
+ should take a `Schedule` object and the makespan of the schedule as
258
+ input and return a `Figure` object.
259
+ plot_current_time:
260
+ Whether to plot a vertical line at the current time.
261
+ scheduled_history:
262
+ A sequence of scheduled operations. If not provided, the solver
263
+ """
264
+ if solver is not None and schedule_history is None:
265
+ dispatcher = Dispatcher(
266
+ instance, ready_operations_filter=solver.pruning_function
267
+ )
268
+ history_tracker = HistoryObserver(dispatcher)
269
+ makespan = solver.solve(instance, dispatcher).makespan()
270
+ dispatcher.unsubscribe(history_tracker)
271
+ dispatcher.reset()
272
+ schedule_history = history_tracker.history
273
+ elif schedule_history is not None and solver is None:
274
+ dispatcher = Dispatcher(instance)
275
+ makespan = max(
276
+ scheduled_operation.end_time
277
+ for scheduled_operation in schedule_history
278
+ )
279
+ elif schedule_history is not None and solver is not None:
280
+ raise ValidationError(
281
+ "Only one of 'solver' and 'history' should be provided."
282
+ )
283
+ else:
284
+ raise ValidationError(
285
+ "Either 'solver' or 'history' should be provided."
286
+ )
287
+
288
+ for i, scheduled_operation in enumerate(schedule_history, start=1):
289
+ dispatcher.dispatch(
290
+ scheduled_operation.operation, scheduled_operation.machine_id
291
+ )
292
+ current_time = (
293
+ None if not plot_current_time else dispatcher.current_time()
294
+ )
295
+ fig = plot_function(
296
+ dispatcher.schedule,
297
+ makespan,
298
+ dispatcher.ready_operations(),
299
+ current_time,
300
+ )
301
+ _save_frame(fig, frames_dir, i)
302
+
303
+
304
+ def _save_frame(figure: Figure, frames_dir: str, number: int) -> None:
305
+ figure.savefig(f"{frames_dir}/frame_{number:02d}.png", bbox_inches="tight")
306
+ plt.close(figure)
307
+
308
+
309
+ def create_gif_from_frames(
310
+ frames_dir: str, gif_path: str, fps: int, loop: int = 0
311
+ ) -> None:
312
+ """Creates a GIF or video from the frames in the given directory.
313
+
314
+ Args:
315
+ frames_dir:
316
+ The directory containing the frames to be used in the GIF.
317
+ gif_path:
318
+ The path to save the GIF file. It should end with ".gif".
319
+ fps:
320
+ The number of frames per second in the GIF.
321
+ loop:
322
+ The number of times the GIF should loop. Default is 0, which means
323
+ the GIF will loop indefinitely. If set to 1, the GIF will loop
324
+ once. Added in version 0.6.0.
325
+ """
326
+ images = _load_images(frames_dir)
327
+ imageio.mimsave(gif_path, images, fps=fps, loop=loop)
328
+
329
+
330
+ def create_video_from_frames(
331
+ frames_dir: str, gif_path: str, fps: int, macro_block_size: int = 16
332
+ ) -> None:
333
+ """Creates a GIF or video from the frames in the given directory.
334
+
335
+ Args:
336
+ frames_dir:
337
+ The directory containing the frames to be used in the video.
338
+ gif_path:
339
+ The path to save the video.
340
+ fps:
341
+ The number of frames per second.
342
+ """
343
+ images = _load_images(frames_dir)
344
+ resized_images = [
345
+ resize_image_to_macro_block(image, macro_block_size=macro_block_size)
346
+ for image in images
347
+ ]
348
+ imageio.mimsave(
349
+ gif_path, resized_images, fps=fps # type: ignore[arg-type]
350
+ )
351
+
352
+
353
+ def resize_image_to_macro_block(
354
+ image: np.ndarray, macro_block_size: int = 16
355
+ ) -> np.ndarray:
356
+ """Resizes the image to ensure its dimensions are divisible by the macro
357
+ block size.
358
+
359
+ Args:
360
+ image (numpy.ndarray): The input image.
361
+ macro_block_size (int): The macro block size, typically 16.
362
+
363
+ Returns:
364
+ numpy.ndarray: The resized image.
365
+ """
366
+ height, width, channels = image.shape
367
+ new_height = (
368
+ (height + macro_block_size - 1) // macro_block_size * macro_block_size
369
+ )
370
+ new_width = (
371
+ (width + macro_block_size - 1) // macro_block_size * macro_block_size
372
+ )
373
+
374
+ if (new_height, new_width) != (height, width):
375
+ resized_image = np.zeros(
376
+ (new_height, new_width, channels), dtype=image.dtype
377
+ )
378
+ resized_image[:height, :width] = image
379
+ return resized_image
380
+ return image
381
+
382
+
383
+ def _load_images(frames_dir: str) -> list:
384
+ frames = [
385
+ os.path.join(frames_dir, frame)
386
+ for frame in sorted(os.listdir(frames_dir))
387
+ ]
388
+ return [imageio.imread(frame) for frame in frames]