job-shop-lib 1.0.0a2__py3-none-any.whl → 1.0.0a4__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. job_shop_lib/_job_shop_instance.py +119 -55
  2. job_shop_lib/_operation.py +18 -7
  3. job_shop_lib/_schedule.py +13 -15
  4. job_shop_lib/_scheduled_operation.py +17 -18
  5. job_shop_lib/dispatching/__init__.py +4 -0
  6. job_shop_lib/dispatching/_dispatcher.py +36 -47
  7. job_shop_lib/dispatching/_dispatcher_observer_config.py +15 -2
  8. job_shop_lib/dispatching/_factories.py +10 -2
  9. job_shop_lib/dispatching/_ready_operation_filters.py +80 -0
  10. job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +0 -1
  11. job_shop_lib/dispatching/feature_observers/_factory.py +21 -18
  12. job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +1 -0
  13. job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +1 -1
  14. job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +44 -25
  15. job_shop_lib/dispatching/rules/_dispatching_rules_functions.py +9 -9
  16. job_shop_lib/generation/_general_instance_generator.py +33 -34
  17. job_shop_lib/generation/_instance_generator.py +14 -17
  18. job_shop_lib/generation/_transformations.py +11 -8
  19. job_shop_lib/graphs/__init__.py +3 -0
  20. job_shop_lib/graphs/_build_disjunctive_graph.py +41 -3
  21. job_shop_lib/graphs/graph_updaters/_graph_updater.py +11 -13
  22. job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +17 -20
  23. job_shop_lib/reinforcement_learning/__init__.py +16 -7
  24. job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +69 -57
  25. job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +43 -32
  26. job_shop_lib/reinforcement_learning/_types_and_constants.py +2 -2
  27. job_shop_lib/visualization/__init__.py +29 -10
  28. job_shop_lib/visualization/_gantt_chart_creator.py +122 -84
  29. job_shop_lib/visualization/_gantt_chart_video_and_gif_creation.py +68 -37
  30. job_shop_lib/visualization/_plot_disjunctive_graph.py +382 -0
  31. job_shop_lib/visualization/{_gantt_chart.py → _plot_gantt_chart.py} +78 -14
  32. {job_shop_lib-1.0.0a2.dist-info → job_shop_lib-1.0.0a4.dist-info}/METADATA +15 -3
  33. {job_shop_lib-1.0.0a2.dist-info → job_shop_lib-1.0.0a4.dist-info}/RECORD +36 -36
  34. {job_shop_lib-1.0.0a2.dist-info → job_shop_lib-1.0.0a4.dist-info}/WHEEL +1 -1
  35. job_shop_lib/visualization/_disjunctive_graph.py +0 -210
  36. /job_shop_lib/visualization/{_agent_task_graph.py → _plot_agent_task_graph.py} +0 -0
  37. {job_shop_lib-1.0.0a2.dist-info → job_shop_lib-1.0.0a4.dist-info}/LICENSE +0 -0
@@ -1,7 +1,6 @@
1
1
  """Home of the `GanttChartCreator` class and its configuration types."""
2
2
 
3
3
  from typing import TypedDict
4
-
5
4
  import matplotlib.pyplot as plt
6
5
 
7
6
  from job_shop_lib.dispatching import (
@@ -10,62 +9,92 @@ from job_shop_lib.dispatching import (
10
9
  )
11
10
  from job_shop_lib.visualization import (
12
11
  create_gantt_chart_video,
13
- plot_gantt_chart_wrapper,
14
- create_gif,
12
+ get_partial_gantt_chart_plotter,
13
+ create_gantt_chart_gif,
15
14
  )
16
15
 
17
16
 
18
- class GanttChartWrapperConfig(TypedDict, total=False):
19
- """Configuration for creating the plot function with the
20
- `plot_gantt_chart_wrapper` function."""
17
+ class PartialGanttChartPlotterConfig(TypedDict, total=False):
18
+ """A dictionary with the configuration for creating the
19
+ :class:`PartialGanttChartPlotter` function.
20
+
21
+ .. seealso::
22
+
23
+ - :class:`PartialGanttChartPlotter`
24
+ - :func:`get_partial_gantt_chart_plotter`
25
+ """
21
26
 
22
27
  title: str | None
28
+ """The title of the Gantt chart."""
29
+
23
30
  cmap: str
31
+ """The colormap to use in the Gantt chart."""
32
+
24
33
  show_available_operations: bool
34
+ """Whether to show available operations in each step."""
25
35
 
26
36
 
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."""
37
+ class GifConfig(TypedDict, total=False):
38
+ """A dictionary with the configuration for creating the GIF using the
39
+ :func:`create_gantt_chart_gif` function.
30
40
 
31
- gif_path: str | None
41
+ .. seealso::
32
42
 
43
+ :func:`create_gantt_chart_gif`
44
+ """
33
45
 
34
- class _GifConfigOptional(TypedDict, total=False):
35
- """Optional configuration for creating the GIF."""
46
+ gif_path: str | None
47
+ """The path to save the GIF. It must end with '.gif'."""
36
48
 
37
49
  fps: int
50
+ """The frames per second of the GIF. Defaults to 1."""
51
+
38
52
  remove_frames: bool
39
- frames_dir: str | None
40
- plot_current_time: bool
53
+ """Whether to remove the frames after creating the GIF."""
41
54
 
55
+ frames_dir: str | None
56
+ """The directory to store the frames."""
42
57
 
43
- class GifConfig(_GifConfigRequired, _GifConfigOptional):
44
- """Configuration for creating the GIF using the `create_gannt_chart_video`
45
- function."""
58
+ plot_current_time: bool
59
+ """Whether to plot the current time in the Gantt chart."""
46
60
 
47
61
 
48
62
  class VideoConfig(TypedDict, total=False):
49
63
  """Configuration for creating the video using the
50
- `create_gannt_chart_video` function."""
64
+ :func:`create_gantt_chart_video` function.
65
+
66
+ .. seealso::
67
+
68
+ :func:`create_gantt_chart_video`
69
+ """
51
70
 
52
71
  video_path: str | None
72
+ """The path to save the video. It must end with a valid video extension
73
+ (e.g., '.mp4')."""
74
+
53
75
  fps: int
76
+ """The frames per second of the video. Defaults to 1."""
77
+
54
78
  remove_frames: bool
79
+ """Whether to remove the frames after creating the video."""
80
+
55
81
  frames_dir: str | None
82
+ """The directory to store the frames."""
83
+
56
84
  plot_current_time: bool
85
+ """Whether to plot the current time in the Gantt chart."""
57
86
 
58
87
 
59
88
  class GanttChartCreator:
60
89
  """Facade class that centralizes the creation of Gantt charts, videos
61
90
  and GIFs.
62
91
 
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.
92
+ It leverages a :class:`HistoryObserver` to keep track of the operations
93
+ being scheduled and provides methods to plot the current state
94
+ of the schedule as a Gantt chart and to create a GIF or video that shows
95
+ the evolution of the schedule over time.
67
96
 
68
- It adds a new `HistoryObserver` to the dispatcher if it does
97
+ It adds a new :class:`HistoryObserver` to the dispatcher if it does
69
98
  not have one already. Otherwise, it uses the observer already present.
70
99
 
71
100
  Attributes:
@@ -81,72 +110,80 @@ class GanttChartCreator:
81
110
  Configuration for creating the video.
82
111
  plot_function:
83
112
  The function used to plot the Gantt chart when creating the GIF
84
- or video. Created using the `plot_gantt_chart_wrapper` function.
113
+ or video. Created using the :func:`get_partial_gantt_chart_plotter`
114
+ function.
115
+
116
+ Args:
117
+ dispatcher:
118
+ The :class:`Dispatcher` class that will be tracked using a
119
+ :class:`HistoryObserver`.
120
+ partial_gantt_chart_plotter_config:
121
+ Configuration for the Gantt chart wrapper function through the
122
+ :class:`PartialGanttChartPlotterConfig` class. Defaults to
123
+ ``None``. Valid keys are:
124
+
125
+ - title: The title of the Gantt chart.
126
+ - cmap: The name of the colormap to use.
127
+ - show_available_operations: Whether to show available
128
+ operations in each step.
129
+
130
+ If ``title`` or ``cmap`` are not provided here and
131
+ ``infer_gantt_chart_config`` is set to ``True``, the values from
132
+ ``gantt_chart_config`` will be used if they are present.
133
+
134
+ .. seealso::
135
+
136
+ - :class:`PartialGanttChartPlotterConfig`
137
+ - :func:`get_partial_gantt_chart_plotter`
138
+ - :class:`PartialGanttChartPlotter`
139
+
140
+ gif_config:
141
+ Configuration for creating the GIF. Defaults to ``None``.
142
+ Valid keys are:
143
+
144
+ - gif_path: The path to save the GIF.
145
+ - fps: The frames per second of the GIF.
146
+ - remove_frames: Whether to remove the frames after creating
147
+ the GIF.
148
+ - frames_dir: The directory to store the frames.
149
+ - plot_current_time: Whether to plot the current time in the
150
+ Gantt chart.
151
+ video_config:
152
+ Configuration for creating the video. Defaults to ``None``.
153
+ Valid keys are:
154
+
155
+ - video_path: The path to save the video.
156
+ - fps: The frames per second of the video.
157
+ - remove_frames: Whether to remove the frames after creating
158
+ the video.
159
+ - frames_dir: The directory to store the frames.
160
+ - plot_current_time: Whether to plot the current time in the
161
+ Gantt chart.
85
162
  """
86
163
 
87
164
  def __init__(
88
165
  self,
89
166
  dispatcher: Dispatcher,
90
- gantt_chart_wrapper_config: GanttChartWrapperConfig | None = None,
167
+ partial_gantt_chart_plotter_config: (
168
+ PartialGanttChartPlotterConfig | None
169
+ ) = None,
91
170
  gif_config: GifConfig | None = None,
92
171
  video_config: VideoConfig | None = None,
93
172
  ):
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
173
  if gif_config is None:
137
- gif_config = {"gif_path": None}
138
- if gantt_chart_wrapper_config is None:
139
- gantt_chart_wrapper_config = {}
174
+ gif_config = {}
175
+ if partial_gantt_chart_plotter_config is None:
176
+ partial_gantt_chart_plotter_config = {}
140
177
  if video_config is None:
141
178
  video_config = {}
142
179
 
143
180
  self.gif_config = gif_config
144
- self.gannt_chart_wrapper_config = gantt_chart_wrapper_config
181
+ self.gannt_chart_wrapper_config = partial_gantt_chart_plotter_config
145
182
  self.video_config = video_config
146
183
  self.history_observer: HistoryObserver = (
147
184
  dispatcher.create_or_get_observer(HistoryObserver)
148
185
  )
149
- self.plot_function = plot_gantt_chart_wrapper(
186
+ self.partial_gantt_chart_plotter = get_partial_gantt_chart_plotter(
150
187
  **self.gannt_chart_wrapper_config
151
188
  )
152
189
 
@@ -169,37 +206,38 @@ class GanttChartCreator:
169
206
  """Plots the current Gantt chart of the schedule.
170
207
 
171
208
  Returns:
172
- tuple[plt.Figure, plt.Axes]:
173
- The figure and axes of the plotted Gantt chart.
209
+ The figure of the plotted Gantt chart.
174
210
  """
175
- return self.plot_function(
211
+ a = self.partial_gantt_chart_plotter(
176
212
  self.schedule,
177
213
  None,
178
- self.dispatcher.ready_operations(),
214
+ self.dispatcher.available_operations(),
179
215
  self.dispatcher.current_time(),
180
216
  )
217
+ return a
181
218
 
182
219
  def create_gif(self) -> None:
183
220
  """Creates a GIF of the schedule being built using the recorded
184
221
  history.
185
222
 
186
223
  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.
224
+ :class:`HistoryTracker` to create a GIF that shows the progression of
225
+ the scheduling process.
189
226
 
190
227
  The GIF creation process involves:
228
+
191
229
  - Using the history of scheduled operations to generate frames for
192
230
  each step of the schedule.
193
231
  - Creating a GIF from these frames.
194
232
  - Optionally, removing the frames after the GIF is created.
195
233
 
196
234
  The configuration for the GIF creation can be customized through the
197
- `gif_config` attribute.
235
+ ``gif_config`` attribute.
198
236
  """
199
- create_gif(
237
+ create_gantt_chart_gif(
200
238
  instance=self.history_observer.dispatcher.instance,
201
239
  schedule_history=self.history_observer.history,
202
- plot_function=self.plot_function,
240
+ plot_function=self.partial_gantt_chart_plotter,
203
241
  **self.gif_config
204
242
  )
205
243
 
@@ -208,12 +246,12 @@ class GanttChartCreator:
208
246
  history.
209
247
 
210
248
  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.
249
+ :class:`HistoryTracker` to create a video that shows the progression
250
+ of the scheduling process.
213
251
  """
214
252
  create_gantt_chart_video(
215
253
  instance=self.history_observer.dispatcher.instance,
216
254
  schedule_history=self.history_observer.history,
217
- plot_function=self.plot_function,
255
+ plot_function=self.partial_gantt_chart_plotter,
218
256
  **self.video_config
219
257
  )
@@ -3,8 +3,7 @@
3
3
  import os
4
4
  import pathlib
5
5
  import shutil
6
- from collections.abc import Callable
7
- from typing import Sequence
6
+ from typing import Sequence, Protocol
8
7
 
9
8
  import imageio
10
9
  import matplotlib.pyplot as plt
@@ -23,19 +22,53 @@ from job_shop_lib.dispatching import (
23
22
  HistoryObserver,
24
23
  )
25
24
  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
-
25
+ from job_shop_lib.visualization._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
+ """
33
42
 
34
- def plot_gantt_chart_wrapper(
43
+ def __call__(
44
+ self,
45
+ schedule: Schedule,
46
+ makespan: int | None = None,
47
+ available_operations: list[Operation] | None = None,
48
+ current_time: int | None = 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(
35
68
  title: str | None = None,
36
69
  cmap: str = "viridis",
37
70
  show_available_operations: bool = False,
38
- ) -> PlotFunction:
71
+ ) -> PartialGanttChartPlotter:
39
72
  """Returns a function that plots a Gantt chart for an unfinished schedule.
40
73
 
41
74
  Args:
@@ -47,12 +80,13 @@ def plot_gantt_chart_wrapper(
47
80
  Returns:
48
81
  A function that plots a Gantt chart for a schedule. The function takes
49
82
  the following arguments:
83
+
50
84
  - schedule: The schedule to plot.
51
85
  - makespan: The makespan of the schedule.
52
86
  - available_operations: A list of available operations. If None,
53
- the available operations are not shown.
87
+ the available operations are not shown.
54
88
  - current_time: The current time in the schedule. If provided, a
55
- red vertical line is plotted at this time.
89
+ red vertical line is plotted at this time.
56
90
 
57
91
  """
58
92
 
@@ -97,33 +131,30 @@ def plot_gantt_chart_wrapper(
97
131
  # Most of the arguments are optional with default values. There is no way to
98
132
  # reduce the number of arguments without losing functionality.
99
133
  # pylint: disable=too-many-arguments
100
- def create_gif(
101
- gif_path: str | None,
134
+ def create_gantt_chart_gif(
102
135
  instance: JobShopInstance,
136
+ gif_path: str | None = None,
103
137
  solver: DispatchingRuleSolver | None = None,
104
- plot_function: PlotFunction | None = None,
138
+ plot_function: PartialGanttChartPlotter | None = None,
105
139
  fps: int = 1,
106
140
  remove_frames: bool = True,
107
141
  frames_dir: str | None = None,
108
142
  plot_current_time: bool = True,
109
143
  schedule_history: Sequence[ScheduledOperation] | None = None,
110
144
  ) -> 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.
145
+ """Creates a GIF of the schedule being built.
114
146
 
115
147
  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
148
  instance:
121
149
  The instance of the job shop problem to be scheduled.
150
+ gif_path:
151
+ The path to save the GIF file. It should end with ".gif". If not
152
+ provided, the name of the instance is used.
122
153
  solver:
123
154
  The dispatching rule solver to use. If not provided, the history
124
155
  of scheduled operations should be provided.
125
156
  plot_function:
126
- A function that plots a Gantt chart for a schedule. It
157
+ A :class:`PlotFunction` that plots a Gantt chart for a schedule. It
127
158
  should take a `Schedule` object and the makespan of the schedule as
128
159
  input and return a `Figure` object. If not provided, a default
129
160
  function is used.
@@ -133,7 +164,7 @@ def create_gif(
133
164
  Whether to remove the frames after creating the GIF.
134
165
  frames_dir:
135
166
  The directory to save the frames in. If not provided,
136
- `gif_path.replace(".gif", "") + "_frames"` is used.
167
+ ``gif_path.replace(".gif", "") + "_frames"`` is used.
137
168
  plot_current_time:
138
169
  Whether to plot a vertical line at the current time.
139
170
  schedule_history:
@@ -144,7 +175,7 @@ def create_gif(
144
175
  gif_path = f"{instance.name}_gantt_chart.gif"
145
176
 
146
177
  if plot_function is None:
147
- plot_function = plot_gantt_chart_wrapper()
178
+ plot_function = get_partial_gantt_chart_plotter()
148
179
 
149
180
  if frames_dir is None:
150
181
  # Use the name of the GIF file as the directory name
@@ -173,14 +204,14 @@ def create_gantt_chart_video(
173
204
  instance: JobShopInstance,
174
205
  video_path: str | None = None,
175
206
  solver: DispatchingRuleSolver | None = None,
176
- plot_function: PlotFunction | None = None,
207
+ plot_function: PartialGanttChartPlotter | None = None,
177
208
  fps: int = 1,
178
209
  remove_frames: bool = True,
179
210
  frames_dir: str | None = None,
180
211
  plot_current_time: bool = True,
181
212
  schedule_history: Sequence[ScheduledOperation] | None = None,
182
213
  ) -> None:
183
- """Creates a GIF of the schedule being built by the given solver.
214
+ """Creates a video of the schedule being built.
184
215
 
185
216
  Args:
186
217
  instance:
@@ -192,16 +223,16 @@ def create_gantt_chart_video(
192
223
  of scheduled operations should be provided.
193
224
  plot_function:
194
225
  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.
226
+ should take a :class:`Schedule` object and the makespan of the
227
+ schedule as input and return a ``Figure`` object. If not provided,
228
+ a default function is used.
198
229
  fps:
199
- The number of frames per second in the GIF.
230
+ The number of frames per second in the video.
200
231
  remove_frames:
201
- Whether to remove the frames after creating the GIF.
232
+ Whether to remove the frames after creating the video.
202
233
  frames_dir:
203
234
  The directory to save the frames in. If not provided,
204
- `name_without_the_extension` + "_frames"` is used.
235
+ ``name_without_the_extension + "_frames"`` is used.
205
236
  plot_current_time:
206
237
  Whether to plot a vertical line at the current time.
207
238
  schedule_history:
@@ -212,7 +243,7 @@ def create_gantt_chart_video(
212
243
  video_path = f"{instance.name}_gantt_chart.mp4"
213
244
 
214
245
  if plot_function is None:
215
- plot_function = plot_gantt_chart_wrapper()
246
+ plot_function = get_partial_gantt_chart_plotter()
216
247
 
217
248
  if frames_dir is None:
218
249
  extension = video_path.split(".")[-1]
@@ -238,7 +269,7 @@ def create_gantt_chart_frames(
238
269
  frames_dir: str,
239
270
  instance: JobShopInstance,
240
271
  solver: DispatchingRuleSolver | None,
241
- plot_function: PlotFunction,
272
+ plot_function: PartialGanttChartPlotter,
242
273
  plot_current_time: bool = True,
243
274
  schedule_history: Sequence[ScheduledOperation] | None = None,
244
275
  ) -> None:
@@ -295,7 +326,7 @@ def create_gantt_chart_frames(
295
326
  fig = plot_function(
296
327
  dispatcher.schedule,
297
328
  makespan,
298
- dispatcher.ready_operations(),
329
+ dispatcher.available_operations(),
299
330
  current_time,
300
331
  )
301
332
  _save_frame(fig, frames_dir, i)