有限要素法の2次元アイソパラメトリック要素の形状関数による座標変換をPythonでアニメーションにした


# はじめに

有限要素法を学んでいるのですが、アイソパラメトリック要素というものが出てきて、自然座標系から物理座標へ変換する必要が出てきます。
しかし、勉強をしていて変換がとても分かりにくいという問題がありました。
そこで、変換をPythonのmatplotlibというライブラリのFuncAnimationを使用してアニメーションを作成しました。


![animation3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/54c9dae0-311f-1466-55af-40389181469e.gif)

![rasen.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/133c7cb1-59aa-b419-5b5b-0c8d279dbed0.gif)



# 座標の定義


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/3b945488-baa1-d4a2-3570-ea33defaf595.png)


今回、座標系として、
$[-1,1]\times[-1,1]$で定義された**自然座標**$(\xi, \eta)$、
任意範囲で定義された**物理座標**$(x, y)$
の2つの直交座標系を考えます。

それぞれの座標系には4節点からなる四角形があり、それぞれのの座標における節点は対応しています。
節点は自然座標における(-1,-1)の点から反時計回りに[1,2,3,4]と番号を付けます。

また、以後出てくる形状関数$N_i$についても、同じ番号の節点と対応したものとなります。




# 形状関数による座標変換


今回の目的は、$[-1,1]\times[-1,1]$で定義された**自然座標**の点を、任意範囲の**物理座標**の四角形に座標変換（写像）することです。
その際、必ず満たさなければいけないことは、有限要素法では節点に座標が設定されているため、**自然座標**と**物理座標**の節点座標が**1対1対応**していることです。



ここでパッと思いついたのが、ある節点で1、その他の3節点では0になるような関数です。
さらに、節点は-1か+1をとりますから、以下のような形状関数$N_i$を**思いつきます。**


```math
\begin{align}
N_1(\xi, \eta)=\frac{1}{4}(1-\xi)(1-\eta) \\
N_2(\xi, \eta)=\frac{1}{4}(1+\xi)(1-\eta) \\
N_3(\xi, \eta)=\frac{1}{4}(1+\xi)(1+\eta) \\
N_4(\xi, \eta)=\frac{1}{4}(1-\xi)(1+\eta)
\end{align}
```

それぞれ、特定の自然座標の節点でしか1になりません。



さらに、これと物理座標の節点座標を組み合わせることで、以下のように、自然座標から物理座標に座標変換（写像）することができます。

```math
\begin{align}
x(\xi, \eta)=&N_1(\xi, \eta)\cdot x_1+N_2(\xi, \eta)\cdot x_2+N_3(\xi, \eta)\cdot x_3+N_4(\xi, \eta)\cdot x_4 \\
y(\xi, \eta)=&N_1(\xi, \eta)\cdot y_1+N_2(\xi, \eta)\cdot y_2+N_3(\xi, \eta)\cdot y_3+N_4(\xi, \eta)\cdot y_4
\end{align}
```

```math
\begin{equation}
\boldsymbol{\{x\}}=
\begin{Bmatrix} x(\xi, \eta) \\ y(\xi, \eta) \end{Bmatrix}
=\begin{bmatrix}
N_1(\xi, \eta) & N_2(\xi, \eta) & N_3(\xi, \eta) & N_4(\xi, \eta)    \\
\end{bmatrix}
\begin{Bmatrix}
x_1 & y_1 \\ 
x_2 & y_2 \\ 
x_3 & y_3 \\
x_4 & y_4
\end{Bmatrix}\\
=\begin{bmatrix}
N_1 & 0 & N_2 & 0 & N_3 & 0 & N_4 & 0    \\
0 & N_1 & 0 & N_2 & 0 & N_3 & 0 & N_4    \\
\end{bmatrix}
\begin{Bmatrix}
x_1 \\ y_1 \\ 
x_2 \\ y_2 \\ 
x_3 \\ y_3 \\
x_4 \\ y_4
\end{Bmatrix}\\
\end{equation}
```


このように式変形から導出したのではなく、形状関数が偶然発見されたようなものを、**セレンディピティ要素**と言います。


https://www.fem-vandv.net/c32.html

https://www.jsme.or.jp/jsme-medwiki/01:1007051#:~:text=serendipity%20element&text=%E6%9C%89%E9%99%90%E8%A6%81%E7%B4%A0%E6%B3%95%E8%A7%A3%E6%9E%90%E3%81%A7,%E3%81%8B%E3%82%89%E3%82%BB%E3%83%AC%E3%83%B3%E3%83%87%E3%82%A3%E3%83%94%E3%83%86%E3%82%A3%E8%A6%81%E7%B4%A0%E3%81%A8%E5%91%BC%E3%81%B0%E3%82%8C%E3%82%8B%EF%BC%8E



よく$\xi$と$\eta$の1次関数だから以下のような式から始まる行列の変形によって形状関数を導出しているものを見かけますが、こちらよりも**セレンディピティ要素**だからという説明のほうが個人的にはしっくりときます。


```math
\begin{align}
x = 
\begin{bmatrix}
1 & \xi & \eta & \xi \eta
\end{bmatrix}
\begin{bmatrix}
c1\\c2\\c3\\c4
\end{bmatrix}
\end{align}
```







# 形状関数のまとめたかきかた


形状関数を書くときに、毎回以下のように4つ書くのは大変です。

```math
\begin{align}
N_1(\xi, \eta)=\frac{1}{4}(1-\xi)(1-\eta) \\
N_2(\xi, \eta)=\frac{1}{4}(1+\xi)(1-\eta) \\
N_3(\xi, \eta)=\frac{1}{4}(1+\xi)(1+\eta) \\
N_4(\xi, \eta)=\frac{1}{4}(1-\xi)(1+\eta)
\end{align}
```

ある自然座標における節点$i$における座標を$(\xi_i, \eta_i)$とすれば、

```math
\begin{align}
N_i(\xi, \eta)=\frac{1}{4}(1+\xi_i\xi)(1+\eta_i\eta) \\
\end{align}
```

と書くことができます。






# 形状関数の性質

![rasen.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/133c7cb1-59aa-b419-5b5b-0c8d279dbed0.gif)


## 節点の性質
形状関数では、その定義の仕方から節点では変換前後で対応があります。

## 辺上での性質
節点間の辺上であれば、自然座標での辺のある比での内分点が、物理座標での辺のある比での内分点に対応します。

例として、節点1と節点2の間の辺について考えましょう。
節点1と節点2では、$\eta=-1$であるため、代入すると、以下の式になります。



```math
\begin{align}
x(\xi, \eta=-1) &=N_1(\xi, \eta=-1)\cdot x_1+N_2(\xi, \eta=-1)\cdot x_2+N_3(\xi, \eta=-1)\cdot x_3+N_4(\xi, \eta=-1)\cdot x_4 \\
&=\frac{1}{2}(1-\xi)x_1 +  \frac{1}{2}(1+\xi)x_2 \\
&= \frac{1}{2}(x_1 +x_2) + \frac{1}{2}(x_2 - x_1)\xi
\end{align}
```

$\frac{1}{2}(x_1 +x_2) $が辺の中点、$\frac{1}{2}(x_2 - x_1)$が辺の長さの半分であるため、辺上での内分点が対応するということがわかります。

この式が表す他の特徴は、辺上の値は両端の節点の影響しか受けないということです。
すなわち、有限要素法において辺は、半分同じ半分違う節点から構成された2つの要素と共有されますが、それぞれの要素において同じ節点から辺上の値が計算されるため、要素間で連続し、下図のような非連続な状態にはならないのです。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/c428e3e8-d23f-f2b7-1fae-4e97b6ceefd5.png)
>画像引用元　https://www.fem-vandv.net/c29.html

そして、このような連続性を満たした要素を**適合要素**と言います。

https://www.jsme.or.jp/jsme-medwiki/01:1008708#:~:text=%E6%9C%89%E9%99%90%E8%A6%81%E7%B4%A0%E6%B3%95%E8%A7%A3%E6%9E%90%E3%81%A7,%E3%82%92%E6%BA%80%E3%81%9F%E3%81%97%E3%81%A6%E3%81%84%E3%82%8B%E3%82%82%E3%81%AE%EF%BC%8E

https://www.fem-vandv.net/c29.html

## 四角形内部の性質

四角形内部については、節点の値と形状関数によって値が計算されます。
そのため内部での値は形状関数に従うと**仮定して**計算されたもので、現実世界での値とは異なります。
そして、内部においては4節点すべての値から影響を受けます。









# プログラム

やっと形状関数の説明が終わり、ちらほら出てきたgifを作成するプログラムの説明をします。
今回は、`matplotlib.animation`にある`FuncAnimation`を使用して作成しました。

今回の描写プログラムは以下のようになっています。
入力する自然座標での値`ij_pair`と、それを`isox(xi, eta, x1, x2, x3, x4)`関数で変換した物理座標での値をアニメーション化しています。
変換前の四角形の節点座標は`node_xi`、`node_eta`に格納され、
変換後の四角形の節点座標は`node_x`、`node_y`に格納されます。


```python
import matplotlib.pyplot as plt
import matplotlib.patches as pat
from matplotlib.animation import FuncAnimation
import numpy as np


#各形状関数
def N1(xi, eta):
    n = (1-xi)*(1-eta)/4
    return n

def N2(xi, eta):
    n = (1+xi)*(1-eta)/4
    return n

def N3(xi, eta):
    n = (1+xi)*(1+eta)/4
    return n

def N4(xi, eta):
    n = (1-xi)*(1+eta)/4
    return n

#形状関数による自然座標から物理座標への変換
def isox(xi, eta, x1, x2, x3, x4):
    x  = x1*N1(xi, eta)
    x += x2*N2(xi, eta)
    x += x3*N3(xi, eta)
    x += x4*N4(xi, eta)    
    return x




def plot_transformation(ij_pair):
    
    #自然座標の四角形（左下から）
    node_xi =[-1,1,1,-1]
    node_eta =[-1,-1,1,1]

    #物理座標の四角形（左下から）
    node_x = [0,6,5,1]
    node_y = [0,0,4,6]
    
    
    #コメントアウトで前の点が残る
    #ax[0].cla()
    #ax[1].cla()
    
    #自然座標
    ax[0].grid(b=True)
    p_xieta = pat.Polygon(xy = list(zip(node_xi,node_eta)), fc = "gainsboro", ec = "black")
    ax[0].add_patch(p_xieta)
    ax[0].set_xlabel("ξ", size = 14, fontname="MS Gothic", weight = "light")
    ax[0].set_ylabel("η", size = 14, fontname="MS Gothic", weight = "light")
    #ax[0].set_xlim(-2,2)
    #ax[0].set_ylim(-2, 2)
    ax[0].set_aspect('equal', adjustable='box')
    ax[0].set_xticks([-1,0,1])
    ax[0].set_yticks([-1,0,1])
    
    
    #物理座標
    p_xy = pat.Polygon(xy = list(zip(node_x,node_y)), fc = "gainsboro", ec = "black")
    ax[1].add_patch(p_xy)
    ax[1].set_xlabel("x", size = 14, weight = "light")
    ax[1].set_ylabel("y", size = 14, weight = "light")
    #ax[1].set_xlim(-1, 8)
    #ax[1].set_ylim(-1, 8)
    ax[1].set_aspect('equal', adjustable='box')

    #自然座標から物理座標への変換
    x = isox(*ij_pair, *node_x)
    y = isox(*ij_pair, *node_y)
    

    
    #ax[0].plot(*ij_pair, marker='.',color="black")
    #ax[1].plot(x,y,marker='.',color="black")
    
    ax[0].plot(*ij_pair, marker='.')
    ax[1].plot(x,y,marker='.')
    



#入力する自然座標の作成
ij_pair = []
for i in np.arange(-1, 1.2, 0.2)[::-1]:
    for j in np.arange(-1, 1.2, 0.2)[::-1]:        
        ij_pair.append([i,j])


fig, ax = plt.subplots(1,2, dpi=100)
plt.subplots_adjust(wspace=0.4, hspace=0.6)
anim = FuncAnimation(fig, plot_transformation, frames=ij_pair, interval=100)


anim.save("animation3.gif", writer="pillow")
plt.close()
```

上記のプログラムで出来上がるgifがこちらです。
![animation3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/54c9dae0-311f-1466-55af-40389181469e.gif)


## ほかの点を消す

現在のプログラムでは、プロットしたすべての点が描写されています。
これを、1点のみにするには、以下の部分のコメントアウトを削除します。
これは、前フレームの描写をクリアする関数です。

```python
    #コメントアウトで前の点が残る
    #ax[0].cla()
    #ax[1].cla()
```

するとこうなります。

![animation3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/6ef96997-6796-3d5c-bdd3-d87b13fdee84.gif)



## 点の色を変える

点の色が今はカラフルですが、例えば黒一色にしたいときは、これを、

```python
    ax[0].plot(*ij_pair, marker='.')
    ax[1].plot(x,y,marker='.')
```

このようにして色を指定してください。

```python
    ax[0].plot(*ij_pair, marker='.',color="black")
    ax[1].plot(x,y,marker='.',color="black")
```

![animation3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/2e9c84f9-d426-b461-25c0-ba9d03e9870a.gif)



色一覧は以下のサイトにあります。

https://pythondatascience.plavox.info/matplotlib/%E8%89%B2%E3%81%AE%E5%90%8D%E5%89%8D



## 変換後の四角形の形を変える
変換後の四角形の座標を表す`node_x`、`node_y`をいじります。
以下のように座標を変えた結果です。


```.py
    #物理座標の四角形（左下から）
    node_x = [0,4,5,3]
    node_y = [0,1,4,6]
```


![animation3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/12faab32-ddf8-9872-b9fb-b707512c3a95.gif)
![rasen.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/5bd6139a-d3a4-c2d9-6623-b54cef27c68e.gif)






# ほかの入力

色々な入力とその変換を追ってみましょう。
プログラムは、後半のみ入れ替えています。


## 四角らせん

以下のサイトを参考に作成

https://katoh4u.hatenablog.com/entry/2018/03/24/201333



![rasen.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/133c7cb1-59aa-b419-5b5b-0c8d279dbed0.gif)




```後半.py
import itertools

LOOP = 3
WIDTH = (2*LOOP) + 1

E = (1, 0)
N = (0, -1)
W = (-1, 0)
S = (0, 1)
DIRECTION = itertools.cycle((E, N, W, S))

x = LOOP
y = LOOP
step = 1    # 進んだ距離
corner = 1  # まがり角の位置

# 二次元リストを初期化
spiral = []
for i in range(WIDTH):
    spiral.append([0 for j in range(WIDTH)])

for i in range(WIDTH * WIDTH):
    # まがり角に到達したら方向転換
    if step >= corner:
        step = 1
        direction = next(DIRECTION)
        dx, dy = direction

        # X方向に進むとき、まがり角が遠くなる
        if direction == E or direction == W:
            corner += 1
            
    spiral[y][x] = i
    step += 1
    x += dx
    y += dy



spiral = np.array(spiral)
ij_pair = []
max_index = LOOP*2
    
for i in range(WIDTH*WIDTH):
    print(np.where(spiral == i)[0][0], np.where(spiral == i)[1][0])
    ij_pair.append([np.where(spiral == i)[0][0], np.where(spiral == i)[1][0]])
    
max_index = LOOP*2
ij_pair = np.array(ij_pair)
ij_pair = (ij_pair/max_index -0.5)*2
ij_pair = ij_pair[::-1]

fig, ax = plt.subplots(1,2, dpi=100)
plt.subplots_adjust(wspace=0.4, hspace=0.6)
anim = FuncAnimation(fig, plot_transformation, frames=ij_pair, interval=100)


anim.save("rasen.gif", writer="pillow")
plt.close()
```






## 対数らせん


![rasen.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/d431add0-7708-064c-4790-c864746d1cab.gif)


```後半.py
ij_pair = []
for theta in range(47):
    theta /= 2
    i = 0.1*np.exp(theta/10)*np.cos(theta)
    j = 0.1*np.exp(theta/10)*np.sin(theta)
    ij_pair.append([i,j])



fig, ax = plt.subplots(1,2, dpi=100)
plt.subplots_adjust(wspace=0.4, hspace=0.6)
anim = FuncAnimation(fig, plot_transformation, frames=ij_pair, interval=100)


anim.save("rasen.gif", writer="pillow")
plt.close()
```





# さいごに

いかがだったでしょうか。
有限要素法のわからないところを追っていたら記事が一本かけてしまいました。
他にも有限要素法の肝になってくるガウス・ルジャンドル求積については以下の記事も書いていますので、読んでみてください。


https://qiita.com/yuki_2020/items/1ba48481b930fc3998fc



# plt参考サイト

https://qiita.com/MuAuan/items/acee29099a98c5cfb020



https://www.yutaka-note.com/entry/matplotlib_artist_anim

https://pystyle.info/matplotlib-animation/

https://sabopy.com/py/matplotlib-animation-16/

https://qiita.com/osanshouo/items/3c66781f41884694838b




# 形状関数参考サイト

http://www.ms1.mctv.ne.jp/sifoen.project/FEM/FEM-PDF/shape%20function%20&%20Jacobi%20matrix.pdf

https://www.fem-vandv.net/c29.html

https://www.fem-vandv.net/c32.html

https://moridesignoffice.com/FEM-sum-4.html

http://www.ms1.mctv.ne.jp/sifoen.project/FEM/FEM-PDF/shape%20function%20&%20Jacobi%20matrix.pdf

http://www.multi.k.u-tokyo.ac.jp/lectures/H18_HPC/materials/FEM_2D.PDF

https://www.jstage.jst.go.jp/article/jime/49/2/49_239/_pdf

https://moridesignoffice.com/FEM-sum-3.html

https://qiita.com/damyarou/items/320bad2052bb5fccd75f#%E5%86%85%E6%8C%BF%E9%96%A2%E6%95%B0

https://monoist.itmedia.co.jp/mn/articles/0909/02/news100_2.html



https://www.jsme.or.jp/jsme-medwiki/01:1008708#:~:text=%E6%9C%89%E9%99%90%E8%A6%81%E7%B4%A0%E6%B3%95%E8%A7%A3%E6%9E%90%E3%81%A7,%E3%82%92%E6%BA%80%E3%81%9F%E3%81%97%E3%81%A6%E3%81%84%E3%82%8B%E3%82%82%E3%81%AE%EF%BC%8E

https://www.fem-vandv.net/c31.html


# 有限要素法関連の記事

https://qiita.com/yuki_2020/items/87ce1e97772876f1b157

https://qiita.com/yuki_2020/items/1ba48481b930fc3998fc

https://qiita.com/yuki_2020/items/9dcfd88c8909300cc9a1

https://qiita.com/yuki_2020/items/1b603561bb3ff779ad58

https://qiita.com/yuki_2020/items/4a741014be1721e6e968