Pythonで数式・グラフのアニメを描ける「Manim」

## はじめに
この記事は、はんなりPythonアドベントカレンダーの7日目の記事です。

海外で有名な数学を紹介するYouTubeチャンネル「[3Blue1Brown(YouTube)](https://www.youtube.com/channel/UCYO_jab_esuFRV4b17AJtAw)」に使われているアニメーションツールを見つけました。

名前を、「**Manim**」(Mathematical Animation Engineの略)といいます。

日本での知名度はまだ低いですが、開発は活発に行われていて、MITライセンスなのでこれから有名になりそうです。

このようなアニメーションをPythonで簡単に作れます。

![](https://i.imgur.com/b9BESBN.gif)


## インストール
今回は、3Blue1Brownに使われているバージョンを含む [github.com/3b1b/manim](https://github.com/3b1b/manim) ではなく、メンテナンスが活発なコミュニティーエディション [github.com/ManimCommunity/manim](https://github.com/ManimCommunity/manim/) をインストールします。

### 環境構築

ManimはクロスプラットフォームなのでMac、GNU/Linux、Windowsにインストールできます。

**インストールの詳細**はこちら！↓
[**Installation**](https://docs.manim.community/en/v0.1.1/installation.html)

WSL(Ubuntu 18.04)の場合、

まずは、更新。

```
sudo apt update
sudo apt upgrade
```

`pycairo`をインストールします。
`pycairo`は、2Dのベクターイメージを描画するライブラリ`cairo`のPythonラッパーです。

```
sudo apt install libcairo2-dev
```

`ffmpeg`は、動画と音声を記録・変換・再生するライブラリです。

```
sudo apt install ffmpeg
```

`Latex`をインストール

```
sudo apt install texlive texlive-latex-extra texlive-fonts-extra \
texlive-latex-recommended texlive-science texlive-fonts-extra tipa
```

`python3-pip`がない場合、

```
sudo apt install python3-pip
```

```
pip3 install manim
```

## 動かしてみる
適当なプロジェクトディレクトリを作り、そのなかで作業します。

```
mkdir project
cd project
```

試しに`scene.py`というファイルを作って、以下のプログラムを書き込みます。

**Manimに含まれるモジュール、関数、および変数についての詳細**はこちら↓
[**Reference**](https://docs.manim.community/en/v0.1.1/reference.html)


```python

from manim import *

class HelloWorld(Scene):
    def construct(self):
        #######Code#######
        # テキストを定義
        first_text = Text("Hello, World!")

        # テキストを表示
        self.wait(1)
        self.play(Write(first_text))
        self.wait(1)
        # テキスト２つ分アップ
        self.play(ApplyMethod(first_text.shift,2*UP))
        self.wait(1)

        math_text=MathTex(
            "\\frac{d}{dx}f(x)g(x)=","f(x)\\frac{d}{dx}g(x)","+",
            "g(x)\\frac{d}{dx}f(x)"
        )
        # 数式を表示
        self.play(Write(math_text))

        framebox1 = SurroundingRectangle(math_text[1], buff = .1)
        framebox2 = SurroundingRectangle(math_text[3], buff = .1)
        
        self.play(
            ShowCreation(framebox1),
        )
        self.wait()
        # manim.animation.transform
        self.play(
            ReplacementTransform(framebox1,framebox2),
        )
        self.wait()
```
`scene.py`の`HelloWorld`クラス名を指定、低品質レンダリング、`gif`形式で保存します。

```
manim scene.py HelloWorld -ql -i
```

```
[12/15/20 13:21:22] INFO     Text now uses Pango for rendering. In case of          text_mobject.py:719
                             problems, the old implementation is available as
                             CairoText.
[12/15/20 13:21:24] INFO     Animation 0 : Partial movie file written in {'/mn scene_file_writer.py:393
                             t/c/Users/tonoy/Dev/manim/project/media/videos/sc
                             ene/480p15/partial_movie_files/HelloWorld/3070002
                             022_3972803965_223132457.mp4'}
...
```

![](https://i.imgur.com/hAgqpUa.gif)


コマンドの構成
![](https://i.imgur.com/FbavS1k.png)
https://github.com/ManimCommunity/manim/#command-line-arguments より引用

**コマンドの詳細について**はこちら↓
[**A list of all CLI flags**](https://docs.manim.community/en/latest/tutorials/configuration.html#a-list-of-all-cli-flags)

`-ql`：低品質レンダリング、480p 15fps
`-qe`：高品質レンダリング、1080p 60fps
`-qk`：4Kレンダリング、4K 60fps
`-p`：シーンがレンダリングされたら動画を再生するようにmanimに指示する
`-s`：シーンの最後のフレームを出力する。静止画もとりたい場合に使います。
`-i`：gif形式で保存
`-a`：複数の(`Scene`)をまとめてレンダリングできる。

## 遊んでみる
### 2Dグラフ

活性化関数をアニメーションしてみます。

```python

from manim import *
import math

class Graphing(GraphScene):
    CONFIG = {
        "x_min": -4,
        "x_max": 4,
        "y_min": -2,
        "y_max": 2,
        "graph_origin": ORIGIN,
        "function_color": WHITE,
        "axes_color": BLUE
    }

    def construct(self):
        #Make graph
        self.setup_axes(animate=True)
        func_graph = self.get_graph(lambda x: 1 / (1 + np.exp(-x)), color=WHITE)
        graph_title = Tex("sigmoid function")
        graph_title.scale(1.5)
        graph_title.to_corner(UP + LEFT)

        func_graph_2 = self.get_graph(lambda x: np.tanh(x), color=GREEN)
        graph_title_2 = Tex("tanh function")
        graph_title_2.scale(1.5)
        graph_title_2.to_corner(UP + LEFT)

        func_graph_3 = self.get_graph(lambda x: np.maximum(0, x), color=YELLOW)
        graph_title_3 = Tex("ReLU function")
        graph_title_3.scale(1.5)
        graph_title_3.to_corner(UP + LEFT)
        

        #Display graph
        self.play(ShowCreation(func_graph))
        self.add(graph_title)
        self.wait(1)
        self.play(FadeOut(graph_title))
        self.play(ShowCreation(func_graph_2))
        self.add(graph_title_2)
        self.wait(1)
        self.play(FadeOut(graph_title_2))
        self.play(ShowCreation(func_graph_3))
        self.add(graph_title_3)
        self.wait(2)
```

![](https://i.imgur.com/qkNLe6M.gif)

### 3Dグラフ

ニューラルネットワークの学習でよくでる偏微分をグラフにしてみます。

3Dなのでそれなりに時間かかります。

```python

from manim import *

class ThreeDSurface(ParametricSurface):

    def __init__(self, **kwargs):
        kwargs = {
        "u_min": -1.5,
        "u_max": 1.5,
        "v_min": -1.5,
        "v_max": 1.5,
        "checkerboard_colors": [GREEN, BLUE],
        "checkerboard_opacity": 0.5
        }
        ParametricSurface.__init__(self, self.func, **kwargs)

    def func(self, x, y):
        return np.array([x,y,x**2 + y**2])


class Test(ThreeDScene):

    def construct(self):
        self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)

        axes = ThreeDAxes()
        self.add(axes)

        surface = ThreeDSurface()
        self.play(ShowCreation(surface))

        d = Dot(np.array([0,0,0]), color = YELLOW)
        self.play(ShowCreation(d))


        self.wait()
        self.move_camera(phi=45 * DEGREES, theta=30 * DEGREES)
        self.begin_ambient_camera_rotation()
        self.wait(9)
```

![](https://i.imgur.com/g4aEi7o.gif)

## 最後に
`matplotlib`では難しいアニメーションがこんなに簡単にできるとは思いませんでした。これからどんどん使っていこうと思います。