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.
- job_shop_lib/__init__.py +16 -8
- job_shop_lib/{base_solver.py → _base_solver.py} +1 -1
- job_shop_lib/{job_shop_instance.py → _job_shop_instance.py} +9 -4
- job_shop_lib/_operation.py +95 -0
- job_shop_lib/{schedule.py → _schedule.py} +73 -54
- job_shop_lib/{scheduled_operation.py → _scheduled_operation.py} +13 -37
- 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} +57 -18
- job_shop_lib/dispatching/__init__.py +45 -41
- job_shop_lib/dispatching/{dispatcher.py → _dispatcher.py} +153 -80
- job_shop_lib/dispatching/_dispatcher_observer_config.py +54 -0
- job_shop_lib/dispatching/_factories.py +125 -0
- job_shop_lib/dispatching/{history_tracker.py → _history_observer.py} +4 -6
- job_shop_lib/dispatching/{pruning_functions.py → _ready_operation_filters.py} +6 -35
- job_shop_lib/dispatching/_unscheduled_operations_observer.py +69 -0
- job_shop_lib/dispatching/feature_observers/__init__.py +16 -10
- job_shop_lib/dispatching/feature_observers/{composite_feature_observer.py → _composite_feature_observer.py} +84 -2
- job_shop_lib/dispatching/feature_observers/{duration_observer.py → _duration_observer.py} +6 -17
- job_shop_lib/dispatching/feature_observers/{earliest_start_time_observer.py → _earliest_start_time_observer.py} +114 -35
- job_shop_lib/dispatching/feature_observers/{factory.py → _factory.py} +31 -5
- job_shop_lib/dispatching/feature_observers/{feature_observer.py → _feature_observer.py} +59 -16
- job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +97 -0
- job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +33 -0
- job_shop_lib/dispatching/feature_observers/{position_in_job_observer.py → _position_in_job_observer.py} +1 -8
- job_shop_lib/dispatching/feature_observers/{remaining_operations_observer.py → _remaining_operations_observer.py} +8 -26
- job_shop_lib/dispatching/rules/__init__.py +51 -0
- job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +82 -0
- job_shop_lib/dispatching/{dispatching_rule_solver.py → rules/_dispatching_rule_solver.py} +44 -15
- job_shop_lib/dispatching/{dispatching_rules.py → rules/_dispatching_rules_functions.py} +74 -21
- job_shop_lib/dispatching/rules/_machine_chooser_factory.py +69 -0
- job_shop_lib/dispatching/rules/_utils.py +127 -0
- job_shop_lib/exceptions.py +18 -0
- job_shop_lib/generation/__init__.py +2 -2
- job_shop_lib/generation/{general_instance_generator.py → _general_instance_generator.py} +26 -7
- job_shop_lib/generation/{instance_generator.py → _instance_generator.py} +13 -3
- job_shop_lib/graphs/__init__.py +17 -6
- job_shop_lib/graphs/{job_shop_graph.py → _job_shop_graph.py} +81 -2
- job_shop_lib/graphs/{node.py → _node.py} +18 -12
- job_shop_lib/graphs/graph_updaters/__init__.py +13 -0
- job_shop_lib/graphs/graph_updaters/_graph_updater.py +59 -0
- job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +154 -0
- job_shop_lib/graphs/graph_updaters/_utils.py +25 -0
- job_shop_lib/reinforcement_learning/__init__.py +41 -0
- job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +366 -0
- job_shop_lib/reinforcement_learning/_reward_observers.py +85 -0
- job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +337 -0
- job_shop_lib/reinforcement_learning/_types_and_constants.py +61 -0
- job_shop_lib/reinforcement_learning/_utils.py +96 -0
- job_shop_lib/visualization/__init__.py +20 -4
- job_shop_lib/visualization/{agent_task_graph.py → _agent_task_graph.py} +28 -9
- job_shop_lib/visualization/_gantt_chart_creator.py +219 -0
- job_shop_lib/visualization/_gantt_chart_video_and_gif_creation.py +388 -0
- {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0a1.dist-info}/METADATA +68 -44
- job_shop_lib-1.0.0a1.dist-info/RECORD +66 -0
- job_shop_lib/benchmarking/load_benchmark.py +0 -142
- job_shop_lib/cp_sat/__init__.py +0 -5
- job_shop_lib/dispatching/factories.py +0 -206
- 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/generators/__init__.py +0 -8
- job_shop_lib/generators/basic_generator.py +0 -200
- job_shop_lib/generators/transformations.py +0 -164
- job_shop_lib/operation.py +0 -122
- job_shop_lib/visualization/create_gif.py +0 -209
- job_shop_lib-0.5.1.dist-info/RECORD +0 -52
- /job_shop_lib/dispatching/feature_observers/{is_scheduled_observer.py → _is_scheduled_observer.py} +0 -0
- /job_shop_lib/generation/{transformations.py → _transformations.py} +0 -0
- /job_shop_lib/graphs/{build_agent_task_graph.py → _build_agent_task_graph.py} +0 -0
- /job_shop_lib/graphs/{build_disjunctive_graph.py → _build_disjunctive_graph.py} +0 -0
- /job_shop_lib/graphs/{constants.py → _constants.py} +0 -0
- /job_shop_lib/visualization/{disjunctive_graph.py → _disjunctive_graph.py} +0 -0
- /job_shop_lib/visualization/{gantt_chart.py → _gantt_chart.py} +0 -0
- {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0a1.dist-info}/LICENSE +0 -0
- {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]
|