python使って複素積分を可視化・イメージ化して理解してみるチャレンジ

# 概要
複素積分をpython使ってベクトルで表してみました。
イメージの一つとして誰かの参考になれば幸いです。

matplotlibの練習にもなります。クリック検出とか、リアルタイム更新とかします。

## イメージ
### 1. 複素関数をベクトル場で表現してみる
複素積分を可視化する前に、積分する対象である複素関数を可視化できないとキツイので、やってみます。
今回は以下の手順でベクトル場にしてみました。(`f(z)=1/z`で考えてます。)
1. z平面、w平面を用意　（横軸=実数軸、縦軸=虚数軸です。）
1. z平面のベクトルの先端にw平面のベクトルをくっつける
1. 上記を繰り返してベクトルをたくさん作る。 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647496/ef19d15d-f96f-0c3d-c76e-ff3ee3f6f9b6.png)
こんな感じでpythonでもベクトル場を作っていきます。
ただ、ベクトルの大きさを表すことが出来ないので、色で表すことにしています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647496/6f9e04f9-c0c9-ecc8-f274-29becf2b518f.png)

---
### 2. 複素積分をベクトル表現にしてみる

経路を`C:|z|=1`の単一閉曲線に沿う複素関数f(z)=1/z の複素積分
```math
\int_C\frac{1}{z}dz = 2\pi i
```
をやってみると下のような感じになります。
![画像1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647496/91904dfa-70f3-63f4-6004-686c21df1e81.png)
それぞれの矢印は以下の項目を表してます。
![fukuso_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647496/66d760c1-c2cc-c7ec-abcf-5f111187b81e.png)
左は　　　ベクトル場と経路Ｃを表すグラフで、
真ん中は　その時のf(z),dzベクトルとその積のグラフ
右は　　　これまで計算してきた積のベクトルとその和のグラフ


結果は、
`sum f(z)*dz = -1.22 + 6.13 i`
なので、まあ2πiと言えなくもないかなと。
もうちょっと円の精度良くすれば値は近づきます。

---
**コーシーの積分定理**はこんな感じ。
f(z)が単一閉曲線C及びその内部で正則なとき、
```math
\int_Cf(z)dz = 0
```
でやってみると、
![プレゼンテーション1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647496/26c0fa69-fccb-3aae-d98c-5705f4bf5134.gif)

って感じで、割とゼロに近づいてく様子が分かるんじゃないかと思います。
値は`sum f(z)*dz = 0.08 + 0.02 i`でした。

### 注意：
製作者は複素積分についてほぼ理解していません。
学術的に正しいかは分かりません。
てかグリーンの定理から既にわかりません。教えてえrい人。

# 環境
* windows10
* python 3.7.5

# 全体スクリプト
描画スクリプトの全容です。サイト[1)2)](#参考リンク)を参考にしました。
コピペして実行して、グラフ上でマウスのホイールを押したら経路Cを描けると思います。

```python:complex_plot.py
import numpy as np
import matplotlib.pyplot as plt

LX, LY=2,2 #描画範囲
gridwidth=0.01 #矢印を描く密度

# 画面準備(1*3個)
fig, ax = plt.subplots(1, 3)
fig.set_size_inches(20, 7) #画像サイズ

# タイトルを付ける
ax[0].set_title('C & f(z)')
ax[1].set_title('f(z) & dz & f(z)*dz')
ax[2].set_title('f(z)*dz & ∫f(z)dz')
# 格子を加える
ax[0].grid()
ax[1].grid()
ax[2].grid()
# 範囲指定
ax[0].set_xlim([-LX,LX])
ax[0].set_ylim([-LY,LY])
ax[1].set_xlim([-1,1])
ax[1].set_ylim([-1,1])
ax[2].set_xlim([-1,1])
ax[2].set_ylim([-1,1])


# 被積分複素関数の定義
def f(Z):
    # return Z
    # return np.sin(Z)
    return 1/Z
    # return np.sin(Z)/(Z**2+1)
#零点(特異点)は手打ちする
Xzero, Yzero = 0, 0

"ベクトル場作成"
X, Y= np.meshgrid(np.arange(-LX, LX, gridwidth), np.arange(-LY, LY,gridwidth))
Z = (X + Y * 1j)
W = f(Z)
U = W.real
V = W.imag

#正規化したかったら以下もつける
R = np.sqrt(U**2 + V**2)
U = U / R
V = V / R

ax[0].plot(Xzero,Yzero,'o',color='black')
strm = ax[0].streamplot(X, Y, U, V, color=R, linewidth=2, cmap='summer')
plt.colorbar(strm.lines)

"クリックイベント"
C_start = []
C_end = []
dz = []
save_fz_dz = []
sum_fz_dz = 0+0j
def draw_C_vec(event):
    global C_start
    global C_end
    global dz
    global save_fz_dz
    global sum_fz_dz
    
    if event.button == 2: # ホイールクリックなら
        # 始点取得
        if C_start == []: 
            C_start = [event.xdata, event.ydata]
            ax[0].plot(C_start[0], C_start[1],
                       "o", color="blue")
            # 更新
            fig.canvas.draw()
            print("get_start")

        # 終点取得＋処理
        else:
            C_end = [event.xdata, event.ydata]
            # dzベクトル取得
            dz = (C_end[0]-C_start[0]) + (C_end[1]-C_start[1]) * 1j

            # 要素の計算 (z_i, f(z), f(z)*dz)
            z = C_start[0]+C_start[1]*1j
            f_z = f(z)
            fz_dz = f_z * dz

            " 以下グラフ描画 "
            # 経路C(dz_i)をプロット
            ax[0].arrow(C_start[0],C_start[1],
                        dz.real, dz.imag,
                        width=0.01,color="blue")
            # クリック点プロット
            ax[0].plot(C_end[0], C_end[1],
                        "o", color="blue")
            # f(z)ベクトル
            ax[0].arrow(C_start[0],C_start[1],
                        f_z.real,  f_z.imag,
                        width=0.01,color="cyan")

            # 描画し直し
            ax[1].clear()
            ax[2].clear()
            # タイトルを付ける
            ax[1].set_title('f(z) & dz & f(z)*dz')
            ax[2].set_title('f(z)*dz & ∫f(z)dz')
            # 格子を加える
            ax[1].grid()
            ax[2].grid()
            # 範囲指定
            ax[1].set_xlim([-1,1])
            ax[1].set_ylim([-1,1])
            ax[2].set_xlim([-1,1])
            ax[2].set_ylim([-1,1])

            # f(z)ベクトル
            ax[1].arrow(0,0,
                        f_z.real, f_z.imag,
                        width=0.01,color="cyan")
            # dzベクトル
            ax[1].arrow(0,0,
                        dz.real, dz.imag,
                        width=0.01,color="blue")
            # f(z)*dzベクトル
            ax[1].arrow(0,0,
                        fz_dz.real, fz_dz.imag,
                        width=0.01,color = "red")
            
            # 積分値グラフ
            sum_fz_dz += fz_dz
            save_fz_dz.append(fz_dz)

            # これまでのf(z)*dzを描画
            for one_fz_dz in save_fz_dz:
                ax[2].arrow(0,0,
                            one_fz_dz.real, one_fz_dz.imag,
                            width=0.01,color = "red")
            # 積分値を描画
            ax[2].arrow(0,0,
                        sum_fz_dz.real, sum_fz_dz.imag,
                        width=0.01,color = "gold")
            # 3桁まで表示
            print("sum f(z)*dz = {z.real:.2f} + {z.imag:.2f} i".format(z=sum_fz_dz))
            
            # グラフ更新
            fig.canvas.draw()
            # 値の更新
            C_start = C_end.copy()
            
# クリックに反応するよう設定
cid = fig.canvas.mpl_connect('button_press_event', draw_C_vec)

# 描写開始
plt.draw()
plt.show()

```

# 説明
## データ・グラフの用意
<details><summary><font color="Blue">対応スクリプト（クリックすると開きます）</font></summary>

```python:グラフ準備
import numpy as np
import matplotlib.pyplot as plt

LX, LY=2,2 #描画範囲
gridwidth=0.01 #矢印を描く密度

# 画面準備(1*3個)
fig, ax = plt.subplots(1, 3)
fig.set_size_inches(20, 7) #画像サイズ

# タイトルを付ける
ax[0].set_title('C & f(z)')
ax[1].set_title('f(z) & dz & f(z)*dz')
ax[2].set_title('f(z)*dz & ∫f(z)dz')
# 格子を加える
ax[0].grid()
ax[1].grid()
ax[2].grid()
# 範囲指定
ax[0].set_xlim([-LX,LX])
ax[0].set_ylim([-LY,LY])
ax[1].set_xlim([-1,1])
ax[1].set_ylim([-1,1])
ax[2].set_xlim([-1,1])
ax[2].set_ylim([-1,1])
```
</details>

色々グラフの準備をします。

## 被積分関数の用意
```python:被積分関数
# 被積分複素関数の定義
def f(Z):
    # return Z
    # return np.sin(Z)
    return 1/Z
    # return np.sin(Z)/(Z**2+1)
#零点(特異点)は手打ちする
Xzero, Yzero = 0, 0
```
関数に入れるだけにしました。
ただ、ゼロ点だけは手打ちのままです。

## ベクトル場の作成
<details><summary><font color="Blue">対応スクリプト（クリックすると開きます）</font></summary>


```python:ベクトル場作成
"ベクトル場作成"
X, Y= np.meshgrid(np.arange(-LX, LX, gridwidth), np.arange(-LY, LY,gridwidth))
Z = (X + Y * 1j)
W = f(Z)
U = W.real
V = W.imag

#正規化したかったら以下もつける
R = np.sqrt(U**2 + V**2)
U = U / R
V = V / R

ax[0].plot(Xzero,Yzero,'o',color='black')
strm = ax[0].streamplot(X, Y, U, V, color=R, linewidth=2, cmap='summer')
plt.colorbar(strm.lines)
```

</details>

ここでは、まず格子上の各座標をX,Yで作って、Zに複素数に変換して格納してます。
そのZを先ほどの関数で変換してWにします。
そのあとUとかVとか、streamplot()関数の引数を用意していく感じです。

## クリック反応

<details><summary><font color="Blue">対応スクリプト（クリックすると開きます）</font></summary>

```python:クリック反応
"クリックイベント"
C_start = []
C_end = []
dz = []
save_fz_dz = []
sum_fz_dz = 0+0j
def draw_C_vec(event):
    global C_start
    global C_end
    global dz
    global save_fz_dz
    global sum_fz_dz
    
    if event.button == 2: # ホイールクリックなら
        # 始点取得
        if C_start == []: 
            C_start = [event.xdata, event.ydata]
            ax[0].plot(C_start[0], C_start[1],
                       "o", color="blue")
            # 更新
            fig.canvas.draw()
            print("get_start")

        # 終点取得＋処理
        else:
            C_end = [event.xdata, event.ydata]
            # dzベクトル取得
            dz = (C_end[0]-C_start[0]) + (C_end[1]-C_start[1]) * 1j

            # 要素の計算 (z_i, f(z), f(z)*dz)
            z = C_start[0]+C_start[1]*1j
            f_z = f(z)
            fz_dz = f_z * dz

            " 以下グラフ描画 "
            # 経路C(dz_i)をプロット
            ax[0].arrow(C_start[0],C_start[1],
                        dz.real, dz.imag,
                        width=0.01,color="blue")
            # クリック点プロット
            ax[0].plot(C_end[0], C_end[1],
                        "o", color="blue")
            # f(z)ベクトル
            ax[0].arrow(C_start[0],C_start[1],
                        f_z.real,  f_z.imag,
                        width=0.01,color="cyan")

            # 描画し直し
            ax[1].clear()
            ax[2].clear()
            # タイトルを付ける
            ax[1].set_title('f(z) & dz & f(z)*dz')
            ax[2].set_title('f(z)*dz & ∫f(z)dz')
            # 格子を加える
            ax[1].grid()
            ax[2].grid()
            # 範囲指定
            ax[1].set_xlim([-1,1])
            ax[1].set_ylim([-1,1])
            ax[2].set_xlim([-1,1])
            ax[2].set_ylim([-1,1])

            # f(z)ベクトル
            ax[1].arrow(0,0,
                        f_z.real, f_z.imag,
                        width=0.01,color="cyan")
            # dzベクトル
            ax[1].arrow(0,0,
                        dz.real, dz.imag,
                        width=0.01,color="blue")
            # f(z)*dzベクトル
            ax[1].arrow(0,0,
                        fz_dz.real, fz_dz.imag,
                        width=0.01,color = "red")
            
            # 積分値グラフ
            sum_fz_dz += fz_dz
            save_fz_dz.append(fz_dz)

            # これまでのf(z)*dzを描画
            for one_fz_dz in save_fz_dz:
                ax[2].arrow(0,0,
                            one_fz_dz.real, one_fz_dz.imag,
                            width=0.01,color = "red")
            # 積分値を描画
            ax[2].arrow(0,0,
                        sum_fz_dz.real, sum_fz_dz.imag,
                        width=0.01,color = "gold")
            # 3桁まで表示
            print("sum f(z)*dz = {z.real:.2f} + {z.imag:.2f} i".format(z=sum_fz_dz))
            
            # グラフ更新
            fig.canvas.draw()
            # 値の更新
            C_start = C_end.copy()
            
# クリックに反応するよう設定
cid = fig.canvas.mpl_connect('button_press_event', draw_C_vec)
```

</details>

最後の行を用意すれば、クリック時に関数が呼べるので、その関数に機能を入れていきます。
ベクトルの値を保存したり、足し算したりと色々してるのでごちゃごちゃになってます。お許しください！博士！

ホイールをクリックすることで、`event.xdata`,`event.ydata`でカーソル位置から始点or終点を取得して、そこからベクトルを書いていきます。

ベクトル書く部分は、
1. 左グラフに　経路、点、f(z)　を描く
1. 真ん中グラフに　f(z)、dz、f(z)*dzのベクトル　を描く
1. 右グラフに　積分結果と、これまでのf(z)*dzベクトル　を描く
という順番になってます。

クリックについては、左右のクリックは拡大縮小移動などに使うので、今回はホイールをクリックするようにしました。
左右クリックにしたい場合は、`if event.button == 2`のとこを`1`とか`3`にすると使えます。

# 図から分かる複素積分の用語・性質の考察
### 例
```math
f(z)=sin(Z)
```
<details><summary>詳細（クリックすると開きます）</summary>
pythonでは return np.sin(Z) のように定義

範囲=[-2,2]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647496/9246ecdd-576e-2854-0d25-df5f233c7a65.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647496/913e9928-84f6-c014-f6cc-57bbee5265e9.png)
範囲=[-6,6]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647496/abf141e1-d314-1516-b093-441b1710343f.png)
(0,0)の点から放射状に出てそうです。
波っぽいのが出てきました。
範囲を広げると、周期関数であることが分かりやすいですね。
</details>

---



```math
f(z)=\frac{1}{Z^n}
```

<details><summary>詳細（クリックすると開きます）</summary>
pythonでは return 1/Z**2 のように定義

範囲=[-2,2]
左からn=1,2,3
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647496/867499d1-ae80-6367-704e-1e4c94b9853d.png)
これを見ると、-n乗のZという関数は、
* 特異点から、2*n本の出ていく部分と入ってくる部分がある。
* 単一閉曲線で積分しようとすると、水色のf(z)ベクトルがn周する
などが言えそうです。
[文献1)](https://monaqua.netlify.app/Speak_20210613_sundaymath.pdf)によると、位数は回転数を表しているのだそう。
となると、
位数m = 分母の(z-a)をm乗してる = 積分時のf(z)がm周回転
ってコト？！

</details>



---



```math
f(z)=\frac{e^{iZ}}{Z^2+1}
```
<details><summary>詳細（クリックすると開きます）</summary>
pythonでは return 1/Z**2 のように定義

僕の持ってる教科書に出てきたやつです。
範囲=[-2,2]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647496/6d89f316-f083-0049-8215-889ec849ae8b.png)
ここから教科書ではcosx/(x^2+1)の実積分を導いてるんですが、うーん。わからぬ。
</details>

---

### 正則かどうか
定義としては、「**コーシー・リーマンの関係式**が成り立つか？」です。
```math
\frac{\partial u}{\partial x}=\frac{\partial v}{\partial y}\\
\frac{\partial v}{\partial x}=-\frac{\partial u}{\partial y}
```

グラフから直感的なことを言うなら、
f(z)=∞となる場所があるかないか（矢印の出発点・終着点があるかないか）
* 出発点 = f(z) → +∞
* 終着点 = f(z) → -∞
という事になりそうです。

#### 数学的・物理的背景
[この動画を参考にしています。](https://www.youtube.com/watch?v=EyBDtUtyshk&t=2164s)（https://www.youtube.com/watch?v=EyBDtUtyshk&t=2164s）
グラフを見てお分かりかもしれませんが、電磁気学の電場や磁場、流体力学の流れ場などの流束に関係してきます。
この動画では、**コーシー・リーマンの関係式**とは、複素共役ベクトル`(u-iv)`を考えた時、
**発散が無い**
```math
\nabla \cdot w^* = 0\\
\nabla \cdot\left(\begin{matrix} 
u \\ 
-v
\end{matrix} 
\right)
= \frac{\partial u}{\partial x} - \frac{\partial v}{\partial y} = 0\\
∴\frac{\partial u}{\partial x}=\frac{\partial v}{\partial y}
```

**回転が無い**
```math
\nabla \times w^* = 0\\
\nabla \times \left(\begin{matrix} 
u \\ 
-v
\end{matrix} 
\right)
= \frac{\partial v}{\partial x} - ( -\frac{\partial u}{\partial y}) = 0\\
∴\frac{\partial v}{\partial x}=-\frac{\partial u}{\partial y}
```
という事をあらわあしています。
また、動画では、複素共役を扱っていますが、複素共役を用いると、
* 上記の関係式が成り立つ=正則となる
* 流束として考えることが出来るようになる
というメリットがあるため、複素共役を取っているんだと思います。



### 正則ならば積分は経路によらない
* 正則ならば、f(z)ベクトル場の矢印の方向が大体一緒なので、経路がちょっと変わっても、結果は一緒となる？
* f(z)ベクトルが変わっても、それと対応して経路C(dz)の向きも変わるから一緒になる。
* 非正則な点を横切ると値が∞に吹っ飛ぶので一緒も何もなくなる。

### 留数・留数定理
グラフ上では何を意味してるのかわかりませんでした。誰か教えてください。

### 孤立特異点の種類（除去可能な特異点・位数nの極・真性特異点）
除去可能な特異点はグラフ上で特異点っぽくならない
位数については下のやつ。
真性特異点はグラフ上でも特異点

### 位数

水色のf(z)ベクトルを見ていくと、水色ベクトルの回転数と位数が一致してそう
[参考はこちら<sup>1)<sup>](https://monaqua.netlify.app/Speak_20210613_sundaymath.pdf)

[動画](https://www.youtube.com/watch?v=EyBDtUtyshk&t=2164s)では、ベクトルが出てくる点をsource, 入ってくる点をsinkと呼び、それを合成する数が位数であるとも考えられる…？

# まとめ
複素積分の視覚化にチャレンジしました。
ここまでやっても複素関数論分からないんですがどうしたらいいでしょうか…
ご閲覧ありがとうございました。ご意見・ご指摘等、よろしくお願いいたします。


# 参考サイト
1)[複素関数を見てみよう](https://monaqua.netlify.app/Speak_20210613_sundaymath.pdf)
https://monaqua.netlify.app/Speak_20210613_sundaymath.pdf
2)[Matplotlibの使い方｜Pythonによる可視化入門 #3](https://lib-arts.hatenablog.com/entry/Python_visualize3)
https://lib-arts.hatenablog.com/entry/Python_visualize3
3)[Complex integration, Cauchy and residue theorems | Essence of Complex Analysis #6](https://www.youtube.com/watch?v=EyBDtUtyshk&t=2164s)
https://www.youtube.com/watch?v=EyBDtUtyshk&t=2164s
めちゃわかりやすいです。サイト製作者様にこの場をお借りして感謝申し上げます。