parallel-matplotlib-animation 0.1.2__tar.gz → 0.1.4__tar.gz
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.
- parallel_matplotlib_animation-0.1.4/.github/workflows/docs.yml +40 -0
- parallel_matplotlib_animation-0.1.4/.github/workflows/publish.yml +42 -0
- parallel_matplotlib_animation-0.1.4/.github/workflows/tests.yml +33 -0
- {parallel_matplotlib_animation-0.1.2 → parallel_matplotlib_animation-0.1.4}/.gitignore +1 -1
- parallel_matplotlib_animation-0.1.2/README.md → parallel_matplotlib_animation-0.1.4/PKG-INFO +38 -12
- parallel_matplotlib_animation-0.1.2/PKG-INFO → parallel_matplotlib_animation-0.1.4/README.md +16 -27
- parallel_matplotlib_animation-0.1.4/docs/api/animator.md +10 -0
- parallel_matplotlib_animation-0.1.4/docs/benchmark.md +41 -0
- parallel_matplotlib_animation-0.1.4/docs/developer.md +49 -0
- parallel_matplotlib_animation-0.1.4/docs/embed_example_videos.py +40 -0
- parallel_matplotlib_animation-0.1.4/docs/examples.md +83 -0
- parallel_matplotlib_animation-0.1.4/docs/gen_benchmark.py +23 -0
- parallel_matplotlib_animation-0.1.4/docs/index.md +76 -0
- parallel_matplotlib_animation-0.1.4/docs/installation.md +43 -0
- parallel_matplotlib_animation-0.1.4/docs/usage.md +86 -0
- {parallel_matplotlib_animation-0.1.2/src/parallel_animate → parallel_matplotlib_animation-0.1.4}/examples/multi_panel_animation.py +1 -1
- {parallel_matplotlib_animation-0.1.2/src/parallel_animate → parallel_matplotlib_animation-0.1.4}/examples/nondeterministic_video_loader.py +1 -1
- parallel_matplotlib_animation-0.1.4/examples/run_all_except_benchmarks.sh +47 -0
- parallel_matplotlib_animation-0.1.4/examples/scaling_test.py +234 -0
- {parallel_matplotlib_animation-0.1.2/src/parallel_animate → parallel_matplotlib_animation-0.1.4}/examples/simple_wave_animation.py +1 -1
- {parallel_matplotlib_animation-0.1.2/src/parallel_animate → parallel_matplotlib_animation-0.1.4}/examples/very_complex_animation.py +2 -2
- parallel_matplotlib_animation-0.1.4/mkdocs.yml +66 -0
- parallel_matplotlib_animation-0.1.4/pyproject.toml +52 -0
- {parallel_matplotlib_animation-0.1.2 → parallel_matplotlib_animation-0.1.4}/src/parallel_animate/__init__.py +2 -0
- parallel_matplotlib_animation-0.1.4/src/parallel_animate/animator.py +587 -0
- {parallel_matplotlib_animation-0.1.2 → parallel_matplotlib_animation-0.1.4}/tests/test_animator.py +112 -0
- parallel_matplotlib_animation-0.1.4/tests/test_examples.py +78 -0
- parallel_matplotlib_animation-0.1.4/tests/test_pvio_options.py +216 -0
- parallel_matplotlib_animation-0.1.4/uv.lock +2190 -0
- parallel_matplotlib_animation-0.1.2/.claude/settings.local.json +0 -12
- parallel_matplotlib_animation-0.1.2/assets/multi_panel_animation.gif +0 -0
- parallel_matplotlib_animation-0.1.2/assets/nondeterministic_video_loader.gif +0 -0
- parallel_matplotlib_animation-0.1.2/assets/scaling_graph.png +0 -0
- parallel_matplotlib_animation-0.1.2/assets/simple_wave_animation.gif +0 -0
- parallel_matplotlib_animation-0.1.2/assets/very_complex_animation.gif +0 -0
- parallel_matplotlib_animation-0.1.2/pyproject.toml +0 -26
- parallel_matplotlib_animation-0.1.2/src/parallel_animate/animator.py +0 -441
- parallel_matplotlib_animation-0.1.2/src/parallel_animate/examples/scaling_test.py +0 -96
- parallel_matplotlib_animation-0.1.2/tests/__init__.py +0 -0
- parallel_matplotlib_animation-0.1.2/tests/test_examples.py +0 -47
- parallel_matplotlib_animation-0.1.2/uv.lock +0 -785
- {parallel_matplotlib_animation-0.1.2 → parallel_matplotlib_animation-0.1.4}/LICENSE +0 -0
- {parallel_matplotlib_animation-0.1.2 → parallel_matplotlib_animation-0.1.4}/dev/mp4_to_gif.sh +0 -0
- {parallel_matplotlib_animation-0.1.2 → parallel_matplotlib_animation-0.1.4}/src/parallel_animate/util.py +0 -0
- {parallel_matplotlib_animation-0.1.2/src/parallel_animate/examples → parallel_matplotlib_animation-0.1.4/tests}/__init__.py +0 -0
- {parallel_matplotlib_animation-0.1.2 → parallel_matplotlib_animation-0.1.4}/tests/test_util.py +0 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: Deploy Docs
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
release:
|
|
7
|
+
types: [published]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
deploy:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
with:
|
|
19
|
+
fetch-depth: 0
|
|
20
|
+
|
|
21
|
+
- name: Install FFmpeg
|
|
22
|
+
run: |
|
|
23
|
+
sudo apt-get update
|
|
24
|
+
sudo apt-get install -y ffmpeg
|
|
25
|
+
|
|
26
|
+
- name: Set up uv
|
|
27
|
+
uses: astral-sh/setup-uv@v6
|
|
28
|
+
with:
|
|
29
|
+
enable-cache: true
|
|
30
|
+
|
|
31
|
+
- name: Install dependencies
|
|
32
|
+
run: uv sync --frozen --extra dev
|
|
33
|
+
|
|
34
|
+
- name: Render example videos
|
|
35
|
+
# The gallery MP4s are git-ignored, so render them here. The
|
|
36
|
+
# embed_example_videos.py MkDocs hook copies them into the built site.
|
|
37
|
+
run: uv run bash examples/run_all_except_benchmarks.sh
|
|
38
|
+
|
|
39
|
+
- name: Deploy docs to GitHub Pages
|
|
40
|
+
run: uv run mkdocs gh-deploy --force --clean
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
name: Build distribution
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
|
|
14
|
+
- name: Install uv
|
|
15
|
+
uses: astral-sh/setup-uv@v6
|
|
16
|
+
|
|
17
|
+
- name: Build package
|
|
18
|
+
run: uv build
|
|
19
|
+
|
|
20
|
+
- name: Upload dist artifacts
|
|
21
|
+
uses: actions/upload-artifact@v4
|
|
22
|
+
with:
|
|
23
|
+
name: dist
|
|
24
|
+
path: dist/
|
|
25
|
+
|
|
26
|
+
publish:
|
|
27
|
+
name: Publish to PyPI
|
|
28
|
+
needs: build
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
environment: pypi-nely
|
|
31
|
+
permissions:
|
|
32
|
+
id-token: write # required for OIDC trusted publisher
|
|
33
|
+
|
|
34
|
+
steps:
|
|
35
|
+
- name: Download dist artifacts
|
|
36
|
+
uses: actions/download-artifact@v4
|
|
37
|
+
with:
|
|
38
|
+
name: dist
|
|
39
|
+
path: dist/
|
|
40
|
+
|
|
41
|
+
- name: Publish to PyPI
|
|
42
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: ["main", "dev-*"]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: ["main"]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Install FFmpeg
|
|
21
|
+
run: sudo apt-get update && sudo apt-get install -y ffmpeg
|
|
22
|
+
|
|
23
|
+
- name: Set up uv
|
|
24
|
+
uses: astral-sh/setup-uv@v6
|
|
25
|
+
with:
|
|
26
|
+
enable-cache: true
|
|
27
|
+
python-version: ${{ matrix.python-version }}
|
|
28
|
+
|
|
29
|
+
- name: Install dependencies
|
|
30
|
+
run: uv sync --frozen --extra dev
|
|
31
|
+
|
|
32
|
+
- name: Run tests
|
|
33
|
+
run: uv run pytest tests
|
parallel_matplotlib_animation-0.1.2/README.md → parallel_matplotlib_animation-0.1.4/PKG-INFO
RENAMED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: parallel-matplotlib-animation
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: Animate matplotlib figures into videos, in parallel but with efficient caching
|
|
5
|
+
Project-URL: Repository, https://github.com/nely-epfl/parallel-matplotlib-animation
|
|
6
|
+
Author-email: Sibo Wang-Chen <sibo.wang@epfl.ch>
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: <3.15,>=3.10
|
|
9
|
+
Requires-Dist: matplotlib>=3.10.0
|
|
10
|
+
Requires-Dist: numpy<3,>=2
|
|
11
|
+
Requires-Dist: parallel-video-io==0.1.9
|
|
12
|
+
Requires-Dist: tqdm>=4.67
|
|
13
|
+
Provides-Extra: benchmark
|
|
14
|
+
Requires-Dist: pandas<3.0,>=2.0; extra == 'benchmark'
|
|
15
|
+
Requires-Dist: plotly<7.0,>=6.8; extra == 'benchmark'
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: mkdocs-material<10.0,>=9.5; extra == 'dev'
|
|
18
|
+
Requires-Dist: mkdocstrings[python]>=0.29; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest<10.0,>=9.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: ruff>=0.15; extra == 'dev'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
1
23
|
# parallel-matplotlib-animation
|
|
2
24
|
|
|
3
25
|
Create matplotlib animations rendered to video in parallel, with efficient resources reuse.
|
|
@@ -21,7 +43,7 @@ Renders matplotlib animations by:
|
|
|
21
43
|
1. Creating a bunch of worker processes, and creating matplotlib resources (plt.Figure, plt.Axes, artists, etc.) once per worker
|
|
22
44
|
2. Distributing frames across workers via a dynamic queue
|
|
23
45
|
3. Rendering the assigned frames from each worker, but updating the data only (without redrawing the whole plot from scratch)
|
|
24
|
-
4. Encoding frames to video with
|
|
46
|
+
4. Encoding frames to video with [parallel-video-io](https://github.com/sibocw/parallel-video-io) (FFmpeg under the hood, with automatic GPU/NVENC acceleration when available)
|
|
25
47
|
|
|
26
48
|
**Key design: Figure reuse.** In each worker process, `setup()` runs once to create the figure, then `update()` modifies it repeatedly. This brings the best of:
|
|
27
49
|
- Serial processing: avoids the overhead of recreating complex layouts for every frame
|
|
@@ -37,7 +59,7 @@ from parallel_animate import Animator
|
|
|
37
59
|
# Step 1: Create a child class of parallel_animate.Animator
|
|
38
60
|
class WaveAnimation(Animator):
|
|
39
61
|
|
|
40
|
-
# Step 2: Define how the plot should be
|
|
62
|
+
# Step 2: Define how the plot should be set up
|
|
41
63
|
def setup(self):
|
|
42
64
|
fig, ax = plt.subplots()
|
|
43
65
|
self.x = np.linspace(0, 4 * np.pi, 200)
|
|
@@ -58,12 +80,12 @@ class WaveAnimation(Animator):
|
|
|
58
80
|
# Step 4: Define a list of input parameters, one for each frame
|
|
59
81
|
params = [{"phase": 2 * np.pi * i / 60} for i in range(60)]
|
|
60
82
|
|
|
61
|
-
# Step 5: Make video in parallel
|
|
83
|
+
# Step 5: Make the video in parallel
|
|
62
84
|
anim = WaveAnimation()
|
|
63
85
|
anim.make_video("wave.mp4", param_by_frame=params, fps=30, num_workers=4)
|
|
64
86
|
```
|
|
65
87
|
|
|
66
|
-
<
|
|
88
|
+
<video src="https://sibocw.github.io/parallel-matplotlib-animation/output/simple_wave_animation.mp4" width="480" autoplay loop muted playsinline controls></video>
|
|
67
89
|
|
|
68
90
|
## Usage
|
|
69
91
|
This library has a single class: `parallel_animate.Animator`. To make an animation, you must create your own class inheriting from it and define the following methods:
|
|
@@ -79,37 +101,41 @@ Once you have defined your animator class, there is a single method that you nee
|
|
|
79
101
|
- `fps` (int): Frame rate of the output video
|
|
80
102
|
- `n_frames` (int or None): Number of frames to render. If None, use the length of `param_by_frame`. If param_by_frame does not have `__len__` implemented and `n_frames` is None, the progress bar won't show completion percentage.
|
|
81
103
|
- `num_workers` (int): Number of worker processes to be spawned. If -1, use all CPU cores. If -2, use all but one CPU cores, etc. If 1, no child process is created and the video is made in the main process itself. Default is -1.
|
|
82
|
-
-
|
|
104
|
+
- `video_mode` (str): Encoder selection passed to parallel-video-io: `"auto"` (default) uses the GPU encoder (FFmpeg/NVENC) when a CUDA device is available and falls back to CPU (libx264) otherwise; `"gpu"` forces NVENC and `"cpu"` forces libx264. Output is always an H.264 MP4.
|
|
105
|
+
- `video_quality` (int or None), `video_preset` (str or None), `video_extra_ffmpeg_params` (list of str or None): Optional encoding-quality controls forwarded to parallel-video-io. Leave as `None` to use its sensible defaults.
|
|
106
|
+
- See the docstring for `parallel_animate.animator` directly for the remaining less commonly used, optional parameters. These control logging, figure reuse, prefetching, etc.
|
|
107
|
+
|
|
108
|
+
> **Note:** parallel-video-io is currently Linux-only.
|
|
83
109
|
|
|
84
110
|
### Special case: frame params arriving out-of-order in `param_by_frame`
|
|
85
111
|
In some cases, frames in `param_by_frame` might be out of order. We can handle these scenarios by populating `param_by_frame` with a special `parallel_animate.IndexedFrameParams` dataclass, which specifies the frame index that overrides the ordering in `param_by_frame`. This can be useful when, for example, the animator needs to draw frames that are decoded from a video, and the dataloader for that video might return frames in nondeterministic order because it's parallelized.
|
|
86
112
|
|
|
87
|
-
See `
|
|
113
|
+
See `examples/nondeterministic_video_loader.py` for details.
|
|
88
114
|
|
|
89
115
|
## Examples
|
|
90
116
|
|
|
91
|
-
See [`
|
|
117
|
+
See [`examples/`](https://github.com/sibocw/parallel-matplotlib-animation/blob/main/examples/). Run all of them (except the benchmark) with `./examples/run_all.sh`.
|
|
92
118
|
|
|
93
119
|
`simple_wave_animation.py`: The example above
|
|
94
120
|
|
|
95
121
|
`multi_panel_animation.py`: 5 subplots with different plot types
|
|
96
122
|
|
|
97
|
-
<
|
|
123
|
+
<video src="https://sibocw.github.io/parallel-matplotlib-animation/output/multi_panel_animation.mp4" width="480" autoplay loop muted playsinline controls></video>
|
|
98
124
|
|
|
99
125
|
`very_complex_animation.py`: 14 subplots with GridSpec layout
|
|
100
126
|
|
|
101
|
-
<
|
|
127
|
+
<video src="https://sibocw.github.io/parallel-matplotlib-animation/output/very_complex_animation.mp4" width="480" autoplay loop muted playsinline controls></video>
|
|
102
128
|
|
|
103
129
|
`nondeterministic_video_loader.py`: handling frames that arrive out of order
|
|
104
130
|
|
|
105
|
-
<
|
|
131
|
+
<video src="https://sibocw.github.io/parallel-matplotlib-animation/output/nondeterministic_video_loader.mp4" width="240" autoplay loop muted playsinline controls></video>
|
|
106
132
|
|
|
107
133
|
|
|
108
134
|
## Performance test
|
|
109
135
|
|
|
110
|
-
A [strong scaling test](https://hpc-wiki.info/hpc/Scaling_tests#Strong_Scaling) is implemented in `
|
|
136
|
+
A [strong scaling test](https://hpc-wiki.info/hpc/Scaling_tests#Strong_Scaling) is implemented in `examples/scaling_test.py`. Here's the result on my 8-core (16-thread) Intel Core i9-11900K Processor:
|
|
111
137
|
|
|
112
|
-
|
|
138
|
+
See the [interactive scaling figure](https://sibocw.github.io/parallel-matplotlib-animation/benchmark/) on the documentation site.
|
|
113
139
|
|
|
114
140
|
The left-most blue dot indicates serial processing with resources reuse. The black line indicates ideal scaling (zero overhead) if all frames are rendered completely independently in parallel (as is the case in all parallel matplotlib animation libraries I found). Blue dots at 1+ workers are what's implemented in this library.
|
|
115
141
|
|
parallel_matplotlib_animation-0.1.2/PKG-INFO → parallel_matplotlib_animation-0.1.4/README.md
RENAMED
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: parallel-matplotlib-animation
|
|
3
|
-
Version: 0.1.2
|
|
4
|
-
Summary: Animate matplotlib figures into videos, in parallel but with efficient caching
|
|
5
|
-
Project-URL: Repository, https://github.com/sibocw/parallel-matplotlib-animation
|
|
6
|
-
Author-email: Sibo Wang-Chen <sibo.wang@epfl.ch>
|
|
7
|
-
License-File: LICENSE
|
|
8
|
-
Requires-Python: >=3.10
|
|
9
|
-
Requires-Dist: av>=14.0
|
|
10
|
-
Requires-Dist: matplotlib>=3.10.0
|
|
11
|
-
Requires-Dist: numpy<3,>=2
|
|
12
|
-
Requires-Dist: pillow>=11.0
|
|
13
|
-
Requires-Dist: tqdm>=4.67
|
|
14
|
-
Description-Content-Type: text/markdown
|
|
15
|
-
|
|
16
1
|
# parallel-matplotlib-animation
|
|
17
2
|
|
|
18
3
|
Create matplotlib animations rendered to video in parallel, with efficient resources reuse.
|
|
@@ -36,7 +21,7 @@ Renders matplotlib animations by:
|
|
|
36
21
|
1. Creating a bunch of worker processes, and creating matplotlib resources (plt.Figure, plt.Axes, artists, etc.) once per worker
|
|
37
22
|
2. Distributing frames across workers via a dynamic queue
|
|
38
23
|
3. Rendering the assigned frames from each worker, but updating the data only (without redrawing the whole plot from scratch)
|
|
39
|
-
4. Encoding frames to video with
|
|
24
|
+
4. Encoding frames to video with [parallel-video-io](https://github.com/sibocw/parallel-video-io) (FFmpeg under the hood, with automatic GPU/NVENC acceleration when available)
|
|
40
25
|
|
|
41
26
|
**Key design: Figure reuse.** In each worker process, `setup()` runs once to create the figure, then `update()` modifies it repeatedly. This brings the best of:
|
|
42
27
|
- Serial processing: avoids the overhead of recreating complex layouts for every frame
|
|
@@ -52,7 +37,7 @@ from parallel_animate import Animator
|
|
|
52
37
|
# Step 1: Create a child class of parallel_animate.Animator
|
|
53
38
|
class WaveAnimation(Animator):
|
|
54
39
|
|
|
55
|
-
# Step 2: Define how the plot should be
|
|
40
|
+
# Step 2: Define how the plot should be set up
|
|
56
41
|
def setup(self):
|
|
57
42
|
fig, ax = plt.subplots()
|
|
58
43
|
self.x = np.linspace(0, 4 * np.pi, 200)
|
|
@@ -73,12 +58,12 @@ class WaveAnimation(Animator):
|
|
|
73
58
|
# Step 4: Define a list of input parameters, one for each frame
|
|
74
59
|
params = [{"phase": 2 * np.pi * i / 60} for i in range(60)]
|
|
75
60
|
|
|
76
|
-
# Step 5: Make video in parallel
|
|
61
|
+
# Step 5: Make the video in parallel
|
|
77
62
|
anim = WaveAnimation()
|
|
78
63
|
anim.make_video("wave.mp4", param_by_frame=params, fps=30, num_workers=4)
|
|
79
64
|
```
|
|
80
65
|
|
|
81
|
-
<
|
|
66
|
+
<video src="https://sibocw.github.io/parallel-matplotlib-animation/output/simple_wave_animation.mp4" width="480" autoplay loop muted playsinline controls></video>
|
|
82
67
|
|
|
83
68
|
## Usage
|
|
84
69
|
This library has a single class: `parallel_animate.Animator`. To make an animation, you must create your own class inheriting from it and define the following methods:
|
|
@@ -94,37 +79,41 @@ Once you have defined your animator class, there is a single method that you nee
|
|
|
94
79
|
- `fps` (int): Frame rate of the output video
|
|
95
80
|
- `n_frames` (int or None): Number of frames to render. If None, use the length of `param_by_frame`. If param_by_frame does not have `__len__` implemented and `n_frames` is None, the progress bar won't show completion percentage.
|
|
96
81
|
- `num_workers` (int): Number of worker processes to be spawned. If -1, use all CPU cores. If -2, use all but one CPU cores, etc. If 1, no child process is created and the video is made in the main process itself. Default is -1.
|
|
97
|
-
-
|
|
82
|
+
- `video_mode` (str): Encoder selection passed to parallel-video-io: `"auto"` (default) uses the GPU encoder (FFmpeg/NVENC) when a CUDA device is available and falls back to CPU (libx264) otherwise; `"gpu"` forces NVENC and `"cpu"` forces libx264. Output is always an H.264 MP4.
|
|
83
|
+
- `video_quality` (int or None), `video_preset` (str or None), `video_extra_ffmpeg_params` (list of str or None): Optional encoding-quality controls forwarded to parallel-video-io. Leave as `None` to use its sensible defaults.
|
|
84
|
+
- See the docstring for `parallel_animate.animator` directly for the remaining less commonly used, optional parameters. These control logging, figure reuse, prefetching, etc.
|
|
85
|
+
|
|
86
|
+
> **Note:** parallel-video-io is currently Linux-only.
|
|
98
87
|
|
|
99
88
|
### Special case: frame params arriving out-of-order in `param_by_frame`
|
|
100
89
|
In some cases, frames in `param_by_frame` might be out of order. We can handle these scenarios by populating `param_by_frame` with a special `parallel_animate.IndexedFrameParams` dataclass, which specifies the frame index that overrides the ordering in `param_by_frame`. This can be useful when, for example, the animator needs to draw frames that are decoded from a video, and the dataloader for that video might return frames in nondeterministic order because it's parallelized.
|
|
101
90
|
|
|
102
|
-
See `
|
|
91
|
+
See `examples/nondeterministic_video_loader.py` for details.
|
|
103
92
|
|
|
104
93
|
## Examples
|
|
105
94
|
|
|
106
|
-
See [`
|
|
95
|
+
See [`examples/`](https://github.com/sibocw/parallel-matplotlib-animation/blob/main/examples/). Run all of them (except the benchmark) with `./examples/run_all.sh`.
|
|
107
96
|
|
|
108
97
|
`simple_wave_animation.py`: The example above
|
|
109
98
|
|
|
110
99
|
`multi_panel_animation.py`: 5 subplots with different plot types
|
|
111
100
|
|
|
112
|
-
<
|
|
101
|
+
<video src="https://sibocw.github.io/parallel-matplotlib-animation/output/multi_panel_animation.mp4" width="480" autoplay loop muted playsinline controls></video>
|
|
113
102
|
|
|
114
103
|
`very_complex_animation.py`: 14 subplots with GridSpec layout
|
|
115
104
|
|
|
116
|
-
<
|
|
105
|
+
<video src="https://sibocw.github.io/parallel-matplotlib-animation/output/very_complex_animation.mp4" width="480" autoplay loop muted playsinline controls></video>
|
|
117
106
|
|
|
118
107
|
`nondeterministic_video_loader.py`: handling frames that arrive out of order
|
|
119
108
|
|
|
120
|
-
<
|
|
109
|
+
<video src="https://sibocw.github.io/parallel-matplotlib-animation/output/nondeterministic_video_loader.mp4" width="240" autoplay loop muted playsinline controls></video>
|
|
121
110
|
|
|
122
111
|
|
|
123
112
|
## Performance test
|
|
124
113
|
|
|
125
|
-
A [strong scaling test](https://hpc-wiki.info/hpc/Scaling_tests#Strong_Scaling) is implemented in `
|
|
114
|
+
A [strong scaling test](https://hpc-wiki.info/hpc/Scaling_tests#Strong_Scaling) is implemented in `examples/scaling_test.py`. Here's the result on my 8-core (16-thread) Intel Core i9-11900K Processor:
|
|
126
115
|
|
|
127
|
-
|
|
116
|
+
See the [interactive scaling figure](https://sibocw.github.io/parallel-matplotlib-animation/benchmark/) on the documentation site.
|
|
128
117
|
|
|
129
118
|
The left-most blue dot indicates serial processing with resources reuse. The black line indicates ideal scaling (zero overhead) if all frames are rendered completely independently in parallel (as is the case in all parallel matplotlib animation libraries I found). Blue dots at 1+ workers are what's implemented in this library.
|
|
130
119
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# `parallel_animate` — API Reference
|
|
2
|
+
|
|
3
|
+
The public API consists of the `Animator` base class and the
|
|
4
|
+
`IndexedFrameParams` helper, both importable directly from `parallel_animate`.
|
|
5
|
+
|
|
6
|
+
::: parallel_animate.animator
|
|
7
|
+
options:
|
|
8
|
+
members:
|
|
9
|
+
- Animator
|
|
10
|
+
- IndexedFrameParams
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Benchmark
|
|
2
|
+
|
|
3
|
+
A [strong-scaling test](https://hpc-wiki.info/hpc/Scaling_tests#Strong_Scaling)
|
|
4
|
+
measures how render-to-video throughput scales with the number of worker
|
|
5
|
+
processes. It sweeps:
|
|
6
|
+
|
|
7
|
+
- **worker count** — 1, 2, 4, 8, 16;
|
|
8
|
+
- **figure reuse** — with vs without (`reuse_figure_object`); and
|
|
9
|
+
- **encoding backend** — parallel-video-io's CPU (libx264) and GPU (NVENC)
|
|
10
|
+
encoders. The GPU backend is benchmarked only when a CUDA device is present.
|
|
11
|
+
|
|
12
|
+
The animation rendered is
|
|
13
|
+
[`very_complex_animation.py`](examples.md#very_complex_animationpy), a 14-subplot
|
|
14
|
+
`GridSpec` figure that is expensive to build — exactly the case where the
|
|
15
|
+
setup-once / update-many design pays off.
|
|
16
|
+
|
|
17
|
+
## How to run
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install -e '.[benchmark]'
|
|
21
|
+
python examples/scaling_test.py # full sweep
|
|
22
|
+
python examples/scaling_test.py --quick # small, fast smoke run
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Results are written to `examples/output/scaling_test/` (`results.csv`,
|
|
26
|
+
`results.json`, and the interactive figure embedded below as
|
|
27
|
+
`scaling_graph.html`).
|
|
28
|
+
|
|
29
|
+
## Result
|
|
30
|
+
|
|
31
|
+
Speedup is normalised to the serial (one worker), no-reuse, CPU-encode baseline,
|
|
32
|
+
so the curves capture both the parallel speedup and the extra gains from figure
|
|
33
|
+
reuse and GPU encoding. The dashed black line is ideal (zero-overhead) linear
|
|
34
|
+
scaling.
|
|
35
|
+
|
|
36
|
+
--8<-- "examples/output/scaling_test/scaling_graph.html"
|
|
37
|
+
|
|
38
|
+
The left-most dark-blue point is serial processing with figure reuse. Points at
|
|
39
|
+
2+ workers show the parallel speedup; reuse curves (darker) sit above the
|
|
40
|
+
no-reuse curves (lighter) because the expensive layout is built once per worker
|
|
41
|
+
instead of once per frame.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Developer Info
|
|
2
|
+
|
|
3
|
+
Clone the repository and install the development extra:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
git clone https://github.com/sibocw/parallel-matplotlib-animation.git
|
|
7
|
+
cd parallel-matplotlib-animation
|
|
8
|
+
pip install -e '.[dev]'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The `dev` extra installs everything needed for testing, formatting, and building
|
|
12
|
+
the documentation site.
|
|
13
|
+
|
|
14
|
+
## Testing
|
|
15
|
+
|
|
16
|
+
The test suite uses pytest. Run it from the repository root:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pytest
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
A few tests write small MP4 files via parallel-video-io / FFmpeg, so make sure
|
|
23
|
+
`ffmpeg` is on your `$PATH`.
|
|
24
|
+
|
|
25
|
+
## Documentation
|
|
26
|
+
|
|
27
|
+
The docs are built with [MkDocs](https://www.mkdocs.org/) and the Material theme,
|
|
28
|
+
with API pages generated from docstrings by
|
|
29
|
+
[mkdocstrings](https://mkdocstrings.github.io/).
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
mkdocs serve # live-preview at http://127.0.0.1:8000
|
|
33
|
+
mkdocs build # build the static site into site/
|
|
34
|
+
mkdocs gh-deploy # build and push to the gh-pages branch
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The [benchmark page](benchmark.md) embeds
|
|
38
|
+
`examples/output/scaling_test/scaling_graph.html`, produced
|
|
39
|
+
by `python examples/scaling_test.py`. If that file is missing at build time, a
|
|
40
|
+
placeholder is substituted by the `docs/gen_benchmark.py` hook so the build still
|
|
41
|
+
succeeds.
|
|
42
|
+
|
|
43
|
+
## Code style
|
|
44
|
+
|
|
45
|
+
- Follow the [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html).
|
|
46
|
+
- Use ASCII-only characters in `.py` docstrings and comments.
|
|
47
|
+
- Format with [ruff](https://docs.astral.sh/ruff/).
|
|
48
|
+
- On a new GitHub release, the package is published to PyPI and the docs are
|
|
49
|
+
deployed to GitHub Pages automatically.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""MkDocs hook: publish the gallery example videos into the built site.
|
|
2
|
+
|
|
3
|
+
The example scripts render their animations to ``examples/output/`` (git-ignored,
|
|
4
|
+
so the MP4s are not committed to ``main``/``dev`` branches). The docs embed them
|
|
5
|
+
via ``<video>`` tags pointing at ``output/<name>.mp4``. This hook copies the
|
|
6
|
+
gallery videos straight from ``examples/output/`` into the built site so they are
|
|
7
|
+
published to the ``gh-pages`` branch -- and only there -- by ``mkdocs gh-deploy``.
|
|
8
|
+
|
|
9
|
+
Only the named gallery videos are copied (not, e.g., the large benchmark
|
|
10
|
+
artifacts that ``scaling_test.py`` writes alongside them). Missing videos are
|
|
11
|
+
skipped with a warning so the build still succeeds on a fresh checkout where the
|
|
12
|
+
examples have not been rendered yet.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
import shutil
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
log = logging.getLogger("mkdocs.hooks.embed_example_videos")
|
|
22
|
+
|
|
23
|
+
_SOURCE_DIR = Path("examples/output")
|
|
24
|
+
_GALLERY_VIDEOS = (
|
|
25
|
+
"simple_wave_animation.mp4",
|
|
26
|
+
"multi_panel_animation.mp4",
|
|
27
|
+
"very_complex_animation.mp4",
|
|
28
|
+
"nondeterministic_video_loader.mp4",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def on_post_build(config): # noqa: ARG001
|
|
33
|
+
dest_dir = Path(config["site_dir"]) / "output"
|
|
34
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
for name in _GALLERY_VIDEOS:
|
|
36
|
+
src = _SOURCE_DIR / name
|
|
37
|
+
if src.exists():
|
|
38
|
+
shutil.copy2(src, dest_dir / name)
|
|
39
|
+
else:
|
|
40
|
+
log.warning("Example video missing, not published: %s", src)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
Runnable example scripts live in [`examples/`](https://github.com/sibocw/parallel-matplotlib-animation/tree/main/examples).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## `simple_wave_animation.py`
|
|
8
|
+
|
|
9
|
+
A single oscillating cosine wave — the [quick example](index.md#quick-example).
|
|
10
|
+
|
|
11
|
+
<div style="text-align:center;margin:1em 0;">
|
|
12
|
+
<a href="https://github.com/NeLy-EPFL/parallel-matplotlib-animation/blob/main/examples/simple_wave_animation.py" target="_blank" rel="noopener"
|
|
13
|
+
style="display:inline-block;padding:0.25em 0.9em;font-size:0.8em;font-weight:400;background:var(--md-primary-fg-color);color:var(--md-primary-bg-color);border-radius:6px;text-decoration:none;">
|
|
14
|
+
See source code ↗</a>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div style="text-align:center;margin:1em 0;">
|
|
18
|
+
<video src="../output/simple_wave_animation.mp4" autoplay loop muted playsinline controls
|
|
19
|
+
style="width:100%;border-radius:6px;">
|
|
20
|
+
Your browser does not support the video tag.
|
|
21
|
+
</video>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## `multi_panel_animation.py`
|
|
27
|
+
|
|
28
|
+
Five subplots with different plot types updating together.
|
|
29
|
+
|
|
30
|
+
<div style="text-align:center;margin:1em 0;">
|
|
31
|
+
<a href="https://github.com/NeLy-EPFL/parallel-matplotlib-animation/blob/main/examples/http://multi_panel_animation.py" target="_blank" rel="noopener"
|
|
32
|
+
style="display:inline-block;padding:0.25em 0.9em;font-size:0.8em;font-weight:400;background:var(--md-primary-fg-color);color:var(--md-primary-bg-color);border-radius:6px;text-decoration:none;">
|
|
33
|
+
See source code ↗</a>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div style="text-align:center;margin:1em 0;">
|
|
37
|
+
<video src="../output/multi_panel_animation.mp4" autoplay loop muted playsinline controls
|
|
38
|
+
style="width:100%;border-radius:6px;">
|
|
39
|
+
Your browser does not support the video tag.
|
|
40
|
+
</video>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## `very_complex_animation.py`
|
|
46
|
+
|
|
47
|
+
A 14-subplot `GridSpec` layout — the figure used by the
|
|
48
|
+
[benchmark](benchmark.md). It showcases how much the setup-once / update-many
|
|
49
|
+
design saves when the layout is expensive to build.
|
|
50
|
+
|
|
51
|
+
<div style="text-align:center;margin:1em 0;">
|
|
52
|
+
<a href="https://github.com/NeLy-EPFL/parallel-matplotlib-animation/blob/main/examples/very_complex_animation.py" target="_blank" rel="noopener"
|
|
53
|
+
style="display:inline-block;padding:0.25em 0.9em;font-size:0.8em;font-weight:400;background:var(--md-primary-fg-color);color:var(--md-primary-bg-color);border-radius:6px;text-decoration:none;">
|
|
54
|
+
See source code ↗</a>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div style="text-align:center;margin:1em 0;">
|
|
58
|
+
<video src="../output/very_complex_animation.mp4" autoplay loop muted playsinline controls
|
|
59
|
+
style="width:100%;border-radius:6px;">
|
|
60
|
+
Your browser does not support the video tag.
|
|
61
|
+
</video>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## `nondeterministic_video_loader.py`
|
|
67
|
+
|
|
68
|
+
Handling frames whose parameters arrive out of order, using
|
|
69
|
+
[`IndexedFrameParams`](api/animator.md). Useful when frames are decoded by a
|
|
70
|
+
parallel dataloader that returns them non-deterministically.
|
|
71
|
+
|
|
72
|
+
<div style="text-align:center;margin:1em 0;">
|
|
73
|
+
<a href="https://github.com/NeLy-EPFL/parallel-matplotlib-animation/blob/main/examples/nondeterministic_video_loader.py" target="_blank" rel="noopener"
|
|
74
|
+
style="display:inline-block;padding:0.25em 0.9em;font-size:0.8em;font-weight:400;background:var(--md-primary-fg-color);color:var(--md-primary-bg-color);border-radius:6px;text-decoration:none;">
|
|
75
|
+
See source code ↗</a>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div style="text-align:center;margin:1em 0;">
|
|
79
|
+
<video src="../output/nondeterministic_video_loader.mp4" autoplay loop muted playsinline controls
|
|
80
|
+
style="width:100%;border-radius:6px;">
|
|
81
|
+
Your browser does not support the video tag.
|
|
82
|
+
</video>
|
|
83
|
+
</div>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""MkDocs hook: ensure the benchmark figure exists before the docs build.
|
|
2
|
+
|
|
3
|
+
The strong-scaling figure is generated by ``examples/scaling_test.py`` and
|
|
4
|
+
written to ``examples/output/scaling_test/scaling_graph.html``, then embedded
|
|
5
|
+
into ``benchmark.md`` via
|
|
6
|
+
pymdownx.snippets. When the benchmark has not been run (e.g. on a fresh CI
|
|
7
|
+
checkout) the file is missing and the build would fail, so this hook writes a
|
|
8
|
+
placeholder in its place.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
_FIGURE = Path("examples/output/scaling_test/scaling_graph.html")
|
|
14
|
+
_PLACEHOLDER = (
|
|
15
|
+
"<p><em>Run <code>python examples/scaling_test.py</code> to generate this "
|
|
16
|
+
"figure.</em></p>\n"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def on_pre_build(config): # noqa: ARG001
|
|
21
|
+
if not _FIGURE.exists():
|
|
22
|
+
_FIGURE.parent.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
_FIGURE.write_text(_PLACEHOLDER)
|