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.
- job_shop_lib/__init__.py +19 -8
- job_shop_lib/{base_solver.py → _base_solver.py} +1 -1
- job_shop_lib/{job_shop_instance.py → _job_shop_instance.py} +155 -81
- job_shop_lib/_operation.py +118 -0
- job_shop_lib/{schedule.py → _schedule.py} +102 -84
- job_shop_lib/{scheduled_operation.py → _scheduled_operation.py} +25 -49
- job_shop_lib/benchmarking/__init__.py +66 -43
- job_shop_lib/benchmarking/_load_benchmark.py +88 -0
- job_shop_lib/constraint_programming/__init__.py +13 -0
- job_shop_lib/{cp_sat/ortools_solver.py → constraint_programming/_ortools_solver.py} +77 -22
- job_shop_lib/dispatching/__init__.py +51 -42
- job_shop_lib/dispatching/{dispatcher.py → _dispatcher.py} +223 -130
- job_shop_lib/dispatching/_dispatcher_observer_config.py +67 -0
- job_shop_lib/dispatching/_factories.py +135 -0
- job_shop_lib/dispatching/{history_tracker.py → _history_observer.py} +6 -7
- job_shop_lib/dispatching/_optimal_operations_observer.py +113 -0
- job_shop_lib/dispatching/_ready_operation_filters.py +168 -0
- job_shop_lib/dispatching/_unscheduled_operations_observer.py +70 -0
- job_shop_lib/dispatching/feature_observers/__init__.py +51 -13
- job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +212 -0
- job_shop_lib/dispatching/feature_observers/{duration_observer.py → _duration_observer.py} +20 -18
- job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +289 -0
- job_shop_lib/dispatching/feature_observers/_factory.py +95 -0
- job_shop_lib/dispatching/feature_observers/_feature_observer.py +228 -0
- job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +97 -0
- job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +35 -0
- job_shop_lib/dispatching/feature_observers/{is_scheduled_observer.py → _is_scheduled_observer.py} +9 -5
- job_shop_lib/dispatching/feature_observers/{position_in_job_observer.py → _position_in_job_observer.py} +8 -10
- job_shop_lib/dispatching/feature_observers/{remaining_operations_observer.py → _remaining_operations_observer.py} +8 -26
- job_shop_lib/dispatching/rules/__init__.py +87 -0
- job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +84 -0
- job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +201 -0
- job_shop_lib/dispatching/{dispatching_rules.py → rules/_dispatching_rules_functions.py} +70 -16
- job_shop_lib/dispatching/rules/_machine_chooser_factory.py +71 -0
- job_shop_lib/dispatching/rules/_utils.py +128 -0
- job_shop_lib/exceptions.py +18 -0
- job_shop_lib/generation/__init__.py +10 -2
- job_shop_lib/generation/_general_instance_generator.py +165 -0
- job_shop_lib/generation/{instance_generator.py → _instance_generator.py} +37 -26
- job_shop_lib/{generators/transformations.py → generation/_transformations.py} +16 -12
- job_shop_lib/generation/_utils.py +124 -0
- job_shop_lib/graphs/__init__.py +30 -12
- job_shop_lib/graphs/{build_disjunctive_graph.py → _build_disjunctive_graph.py} +41 -3
- job_shop_lib/graphs/{build_agent_task_graph.py → _build_resource_task_graphs.py} +28 -26
- job_shop_lib/graphs/_constants.py +38 -0
- job_shop_lib/graphs/_job_shop_graph.py +320 -0
- job_shop_lib/graphs/_node.py +182 -0
- job_shop_lib/graphs/graph_updaters/__init__.py +26 -0
- job_shop_lib/graphs/graph_updaters/_disjunctive_graph_updater.py +108 -0
- job_shop_lib/graphs/graph_updaters/_graph_updater.py +57 -0
- job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +155 -0
- job_shop_lib/graphs/graph_updaters/_utils.py +25 -0
- job_shop_lib/py.typed +0 -0
- job_shop_lib/reinforcement_learning/__init__.py +68 -0
- job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +398 -0
- job_shop_lib/reinforcement_learning/_resource_task_graph_observation.py +329 -0
- job_shop_lib/reinforcement_learning/_reward_observers.py +87 -0
- job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +443 -0
- job_shop_lib/reinforcement_learning/_types_and_constants.py +62 -0
- job_shop_lib/reinforcement_learning/_utils.py +199 -0
- job_shop_lib/visualization/__init__.py +0 -25
- job_shop_lib/visualization/gantt/__init__.py +48 -0
- job_shop_lib/visualization/gantt/_gantt_chart_creator.py +257 -0
- job_shop_lib/visualization/gantt/_gantt_chart_video_and_gif_creation.py +422 -0
- job_shop_lib/visualization/{gantt_chart.py → gantt/_plot_gantt_chart.py} +84 -21
- job_shop_lib/visualization/graphs/__init__.py +29 -0
- job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py +418 -0
- job_shop_lib/visualization/graphs/_plot_resource_task_graph.py +389 -0
- {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0.dist-info}/METADATA +87 -55
- job_shop_lib-1.0.0.dist-info/RECORD +73 -0
- {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0.dist-info}/WHEEL +1 -1
- job_shop_lib/benchmarking/load_benchmark.py +0 -142
- job_shop_lib/cp_sat/__init__.py +0 -5
- job_shop_lib/dispatching/dispatching_rule_solver.py +0 -119
- job_shop_lib/dispatching/factories.py +0 -206
- job_shop_lib/dispatching/feature_observers/composite_feature_observer.py +0 -87
- job_shop_lib/dispatching/feature_observers/earliest_start_time_observer.py +0 -156
- job_shop_lib/dispatching/feature_observers/factory.py +0 -58
- job_shop_lib/dispatching/feature_observers/feature_observer.py +0 -113
- job_shop_lib/dispatching/feature_observers/is_completed_observer.py +0 -98
- job_shop_lib/dispatching/feature_observers/is_ready_observer.py +0 -40
- job_shop_lib/dispatching/pruning_functions.py +0 -116
- job_shop_lib/generation/general_instance_generator.py +0 -169
- job_shop_lib/generation/transformations.py +0 -164
- job_shop_lib/generators/__init__.py +0 -8
- job_shop_lib/generators/basic_generator.py +0 -200
- job_shop_lib/graphs/constants.py +0 -21
- job_shop_lib/graphs/job_shop_graph.py +0 -202
- job_shop_lib/graphs/node.py +0 -166
- job_shop_lib/operation.py +0 -122
- job_shop_lib/visualization/agent_task_graph.py +0 -257
- job_shop_lib/visualization/create_gif.py +0 -209
- job_shop_lib/visualization/disjunctive_graph.py +0 -210
- job_shop_lib-0.5.1.dist-info/RECORD +0 -52
- {job_shop_lib-0.5.1.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
|
18
|
+
title: Optional[str] = None,
|
20
19
|
cmap_name: str = "viridis",
|
21
|
-
xlim: int
|
20
|
+
xlim: Optional[int] = None,
|
22
21
|
number_of_x_ticks: int = 15,
|
23
|
-
|
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
|
-
|
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(
|
43
|
-
|
44
|
-
|
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,
|
50
|
-
|
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(
|
54
|
-
ax.set_ylabel(
|
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,
|
65
|
-
|
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,
|
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(
|
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
|
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
|
-
|
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
|
+
]
|