morikomorou’s blog

自分が学んだことなどの備忘録的なやつ

【python】有限要素法(FEM)をゼロから実装する3_Bマトリクス導出


はじめに

仕事で部署移動したこともあり、忙しくて更新の間がかなり空いてしまいました…
前回は有限要素法に関する関係式を整理まで行ったので今回は実際に実装してみます。
対象として扱うのは4角形1次要素の平面応力状態における静弾性解析です。




実装の流れ

まずは実装の流れを整理してみます。
先回の記事にて関係式を整理しました。

最終的にやりたいことは以下の方程式を解くことです
 
\boldsymbol{f} = \boldsymbol{K} \, \boldsymbol{d}

ここで \boldsymbol{f}は全節点の荷重をまとめたベクトル、 \boldsymbol{K}は全体剛性マトリクス、 \boldsymbol{d}は全節点の変位をまとめたベクトルです。

このためには要素ごとに要素剛性マトリクス \boldsymbol{K}_{e}を算出する必要があります。
要素剛性マトリクス \boldsymbol{K}_{e}は以下の式で表せます。
 
{\displaystyle
\boldsymbol{K}_{e} = h \sum_{i=1}^{2} \sum_{j=1}^{2} w_{i}w_{j}\boldsymbol{B}^{T}(\xi_{i}, \eta_{i})\boldsymbol{DB}(\xi_{i}, \eta_{i})\mathrm{det}\boldsymbol{J}d\xi d\eta \
}

したがって以下のステップで実装していきます。

  1. Dマトリクス \boldsymbol{D}の算出
  2. Bマトリクス \boldsymbol{B}の算出
  3. 要素剛性マトリクス \boldsymbol{K}_{e}の算出
  4. 全体剛性マトリクス \boldsymbol{K}の算出
  5. 連立方程式を解き、全節点の変位 \boldsymbol{d}を求める

初めから順番にやっていきます。今回はBマトリクス算出まで行います。

実装

以前の記事で作成したコードと、クラス要件をベースに実装を進めます。

以前の記事では基本的な計算はすべてSolverクラスが実行するようなことが書かれてたのでそっちに実行するようにします。

まずは過去記事のおさらいからです。過去記事ではメッシュの生成までやってました。全コード載せときます。

import numpy as np
import matplotlib.pyplot as plt
from matplotlib import patches


class Node:
    """
    Nodeのインデックス、座標および境界条件をまとめておくクラス(2次元用)
    
    Attributes:
        id (int): Nodeのインデックス番号(重複不可)
        x, y (float): Nodeのx, y座標
    """
    def __init__(self, idx, x, y):
        self.id = idx
        self.x = x
        self.y = y
    
    def __str__(self):
        return 'Node {}: x {}, y {}'.format(self.id, self.x, self.y)


class Element:
    """
    4角形1次要素Elementのインデックス、要素内のNode情報および要素の厚みをまとめておくクラス
    
    Attributes:
        id (int): Elementのインデックス番号(重複不可)
        node ([int, int, int, int]): 要素内のNodeのインデックスのリスト(左下から反時計回りで指定)
        thickness (float): Elementの厚み
        xy (ndarray(size: 2, 4)): Element内のNodeのxy座標まとめ
    """
    def __init__(self, idx, node_idxs, thickness):
        self.id = idx
        self.node = node_idxs
        self.thickness = thickness
        self.xy = None

    def __str__(self):
        return 'Element {}: Nodes {}, Thickness: {}'.format(self.id, self.node, self.thickness)
    
    def get_coordination(self, nodes_dict):
        """
        要素の各節点のx,y座標を求めてself.xyに代入。

        Args:
            nodes_dict ({Node.id: Node}): Nodeの辞書
        """

        res = []
        for node_idx in self.node:
            res.append([nodes_dict[node_idx].x, nodes_dict[node_idx].y])
        self.xy = np.array(res)


class Mesh:
    """
    NodeオブジェクトとElementオブジェクトをまとめておくクラス
    
    Attributes:
        nodes ({Node.id: Node}): Nodeの辞書
        elements (list): Elementのリスト
    """

    def __init__(self, nodes_dict, elements):
        self.nodes = nodes_dict
        self.elements = elements
        self.get_element_coord()
    
    def get_element_coord(self):
        for elm in self.elements:
            elm.get_coordination(self.nodes)


def create_mesh(length, height, mesh_size, thickness):
    """
    モデルを格子点に分割し、全ての点のNodeクラスおよびそれらを使用した4角形1次要素を作成する.
    ただし、モデル形状は四角形とする。
    
    Args:
        length, height (float): モデルの長さ、高さ
        mesh_size (float): 分割したいメッシュのサイズ
        thickness (float): 要素の厚み(今回は全て一定とする)
        
    Returns:
        nodes_dict ({Node.id: Node}): 作成したNodeの辞書
        elsms (list): 作成したElementクラスをまとめたリスト
    """
    # Nodeの作成
    num_mesh_len = int(length / mesh_size)
    num_mesh_hei = int(height / mesh_size)
    if num_mesh_len == 0:
        num_mesh_len = 1
    if num_mesh_hei == 0:
        num_mesh_hei = 1
    x = np.linspace(0, length, num_mesh_len + 1)
    y = np.linspace(0, height, num_mesh_hei + 1)
    X, Y = np.meshgrid(x, y)
    X = X.ravel()
    Y = Y.ravel()

    nodes_dict = {}
    for i, coord in enumerate(zip(X, Y)):
        nodes_dict[i] = Node(i, coord[0], coord[1])
    
    # Elementの作成
    node_idx = 0
    elem_idx = 0
    elems = []
    for i in range(num_mesh_hei):
        for j in range(num_mesh_len + 1):
            if (node_idx + 1) % (num_mesh_len + 1) == 0:
                node_idx += 1
                continue
            else:
                node_idxs = [node_idx, node_idx + 1,
                         node_idx + num_mesh_len + 2, node_idx + num_mesh_len + 1]
                elems.append(Element(elem_idx, node_idxs, thickness))
                node_idx += 1
                elem_idx += 1
    return nodes_dict, elems


def plot_mesh(mesh):
    fig, ax = plt.subplots()
    for elem in mesh.elements:
        patch = patches.Polygon(xy=elem.xy, ec='black')
        ax.add_patch(patch)
        text_xy = np.mean(elem.xy, axis=0)
        ax.text(text_xy[0], text_xy[1], elem.id, fontsize=12, va='center', ha='center')
    for node in mesh.nodes.values():
        ax.scatter(node.x, node.y, fc='black', s=100)
        ax.text(node.x, node.y, node.id, fontsize=8, color='white', va='center', ha='center')
    ax.autoscale()
    ax.set_aspect('equal', 'box')
    plt.show()


# モデル、要素サイズ定義
MESH_SIZE = 5.
LENGTH = 10.
HEIGHT = 10.
THICKNESS = 1.

nodes_dict, elems = create_mesh(LENGTH, HEIGHT, MESH_SIZE, THICKNESS)
mesh = Mesh(0, nodes_dict, elems)
for node in mesh.nodes.values():
    print(node)
for elem in mesh.elements:
    print(elem)

plot_mesh(mesh)




Dマトリクス \boldsymbol{D}の算出

Dマトリクスは各要素ごとに以下の式で決まってます


\boldsymbol{D} = \dfrac{E}{(1-\nu)(1+\nu)}\begin{bmatrix}
1 & \nu & 0 \\ \nu & 1 & 0 \\ 0 & 0 & \frac{1-\nu}{2} 
\end{bmatrix}

 Eはヤング率、 \nuはポアソン比です。
基本的には材料特性で決まるものなので、まずは材料クラスを作成してヤング率、ポアソン比を定義しておきます

class Material:
    """
    材料特性をまとめておくクラス
    
    Attributes:
        E (float): ヤング率
        nu (float): ポアソン比
    """

    def __init__(self, E, nu):
        self.E = E
        self.nu = nu

新たにソルバークラスを作成して、メッシュクラス及び上記の材料クラスを参照できるようにしておき、Dマトリクスもここで算出します

class Solver:
    """
    有限要素法の計算を行うクラス
    
    Attributes:
        mesh (Mesh): Meshクラス
        material (Material): Materialクラス
        D (array 3x3): Dマトリクス
    """

    def __init__(self, mesh, material):
        self.mesh = mesh
        self.material = material
        self.D = self.calc_D()

    def calc_D(self):
        """
        Dマトリクスを算出。

        Returns:
            D (array 3x3): Dマトリクス
        """
        D = np.array([[1, self.material.nu, 0],
                           [self.material.nu, 1, 0],
                           [0, 0, (1 - self.material.nu) / 2]]) \
                 * self.material.E / (1 - self.material.nu ** 2)
        return D

ちゃんとできてるか確認しておきましょう

MESH_SIZE = 5.
LENGTH = 20.
HEIGHT = 10.
THICKNESS = 1.
E = 208000 # ヤング率
NU = 0.27 # ポアソン比

nodes_dict, elems = create_mesh(LENGTH, HEIGHT, MESH_SIZE, THICKNESS)
material = Material(E, NU)
mesh = Mesh(0, nodes_dict, elems)
solver = Solver(mesh, material)
print(solver.D)
[[224355.51720419  60575.98964513      0.        ]
 [ 60575.98964513 224355.51720419      0.        ]
 [     0.              0.          81889.76377953]]

まあちゃんとできてそうです。




Bマトリクス \boldsymbol{B}の算出

続いてBマトリクスを求めていきましょう。Bマトリクスは以下の2式から求められます。


\boldsymbol{B} = \begin{bmatrix}
 \dfrac{\partial N_{1}}{\partial x} & 0 &  \dfrac{\partial N_{2}}{\partial x} & 0 &  \dfrac{\partial N_{3}}{\partial x} & 0 &  \dfrac{\partial N_{4}}{\partial x} & 0 \\
 0 & \dfrac{\partial N_{1}}{\partial y} & 0 & \dfrac{\partial N_{2}}{\partial y} & 0 & \dfrac{\partial N_{3}}{\partial y} & 0 & \dfrac{\partial N_{4}}{\partial y} \\
\dfrac{\partial N_{1}}{\partial x} & \dfrac{\partial N_{1}}{\partial y} & \dfrac{\partial N_{2}}{\partial x} & \dfrac{\partial N_{2}}{\partial y} & \dfrac{\partial N_{3}}{\partial x} & \dfrac{\partial N_{3}}{\partial y} & \dfrac{\partial N_{4}}{\partial x} & \dfrac{\partial N_{4}}{\partial y} \\
\end{bmatrix}


 \begin{bmatrix}
\dfrac{\partial N_{i}}{\partial x} \\
\dfrac{\partial N_{i}}{\partial y} \\
\end{bmatrix} =  \begin{bmatrix}
\dfrac{\partial x}{\partial \xi} & \dfrac{\partial y}{\partial \xi}\\
\dfrac{\partial x}{\partial \eta} & \dfrac{\partial y}{\partial \eta}\\
\end{bmatrix}^{-1}  \begin{bmatrix}
\dfrac{\partial N_{i}}{\partial \xi} \\
\dfrac{\partial N_{i}}{\partial \eta} \\
\end{bmatrix} = \boldsymbol{J}^{-1} \begin{bmatrix}
\dfrac{\partial N_{i}}{\partial \xi} \\
\dfrac{\partial N_{i}}{\partial \eta} \\
\end{bmatrix}

まずヤコビアン \boldsymbol{J}以外の部分を整理すると以下のようになります

 \dfrac{\partial N_{1}}{\partial \xi} = \dfrac{1}{4}(-1 + \eta)
 \dfrac{\partial N_{2}}{\partial \xi} = \dfrac{1}{4}(1 - \eta)
 \dfrac{\partial N_{3}}{\partial \xi} = \dfrac{1}{4}(1 + \eta)
 \dfrac{\partial N_{4}}{\partial \xi} = \dfrac{1}{4}(-1 - \eta)

 \dfrac{\partial N_{1}}{\partial \eta} = \dfrac{1}{4}(-1 + \xi)
 \dfrac{\partial N_{2}}{\partial \eta} = \dfrac{1}{4}(-1 - \xi)
 \dfrac{\partial N_{3}}{\partial \eta} = \dfrac{1}{4}(1 + \xi)
 \dfrac{\partial N_{4}}{\partial \eta} = \dfrac{1}{4}(1 - \xi)

ヤコビアン \boldsymbol{J}の各成分は以下のように整理できます。

 \begin{bmatrix}\dfrac{\partial x}{\partial \xi} \\
\dfrac{\partial y}{\partial \xi} \\
\end{bmatrix} = \begin{bmatrix}
 \dfrac{\partial N_{1}}{\partial \xi} & 0 & \dfrac{\partial N_{2}}{\partial \xi} & 0 & \dfrac{\partial N_{3}}{\partial \xi} & 0 & \dfrac{\partial N_{4}}{\partial \xi} & 0 \\
 0 & \dfrac{\partial N_{1}}{\partial \xi} & 0 & \dfrac{\partial N_{2}}{\partial \xi} & 0 & \dfrac{\partial N_{3}}{\partial \xi} & 0 & \dfrac{\partial N_{4}}{\partial \xi} \\
\end{bmatrix} \begin{bmatrix}
  x_{1} \\ y_{1} \\ x_{2} \\ y_{2} \\ x_{3} \\ y_{3} \\ x_{4} \\ y_{4} \\
\end{bmatrix}

 \begin{bmatrix}\dfrac{\partial x}{\partial \eta} \\
\dfrac{\partial y}{\partial \eta} \\
\end{bmatrix} = \begin{bmatrix}
 \dfrac{\partial N_{1}}{\partial \eta} & 0 & \dfrac{\partial N_{2}}{\partial \eta} & 0 & \dfrac{\partial N_{3}}{\partial \eta} & 0 & \dfrac{\partial N_{4}}{\partial \eta} & 0 \\
 0 & \dfrac{\partial N_{1}}{\partial \eta} & 0 & \dfrac{\partial N_{2}}{\partial \eta} & 0 & \dfrac{\partial N_{3}}{\partial \eta} & 0 & \dfrac{\partial N_{4}}{\partial \eta} \\
\end{bmatrix} \begin{bmatrix}
  x_{1} \\ y_{1} \\ x_{2} \\ y_{2} \\ x_{3} \\ y_{3} \\ x_{4} \\ y_{4} \\
\end{bmatrix}

ここで (x_{i}, y_{i})は要素を構成する各ノードの (x, y)座標です。これを見るにBマトリクスは (\xi, \eta)の関数となります。

 (\xi, \eta)に何を入れるのかはさておきソルバークラスにBマトリクス計算用の関数を作成しておきます。

class Solver:
    """
    有限要素法の計算を行うクラス
    
    Attributes:
        mesh (Mesh): Meshクラス
        material (Material): Materialクラス
        D (array 3x3): Dマトリクス
    """

    def __init__(self, mesh, material):
        self.mesh = mesh
        self.material = material
        self.D = self.calc_D()

    def run(self):
        self.f, self.bc_dist_dict = self.read_boundary_cond()
        self.K = self.calc_K()
        self.d = self.solve()

    def calc_D(self):
        """
        Dマトリクスを算出。

        Returns:
            D (array 3x3): Dマトリクス
        """
        D = np.array([[1, self.material.nu, 0],
                           [self.material.nu, 1, 0],
                           [0, 0, (1 - self.material.nu) / 2]]) \
                 * self.material.E / (1 - self.material.nu ** 2)
        return D
    
    def calc_B(self, elem, xi, eta):
        """
        Bマトリクス及びヤコビアンを算出。

        Args:
            elem (Element): 対象のElementクラス
            xi, eta (float): Bマトリクス算出用の座標

        Returns:
            B (array 3x8): Bマトリクス
            J (array 2x2): ヤコビアン
        """
        dndxi = np.array([- 1 + eta, 1 - eta, 1 + eta, - 1 - eta]) / 4
        dndeta = np.array([- 1 + xi, - 1 - xi, 1 + xi, 1 - xi]) / 4
        x = elem.xy[:, 0]
        y = elem.xy[:, 1]
        dxdxi = np.dot(dndxi, x)
        dydxi = np.dot(dndxi, y)
        dxdeta = np.dot(dndeta, x)
        dydeta = np.dot(dndeta, y)
        J = np.array([[dxdxi, dydxi], [dxdeta, dydeta]])
        B = np.zeros((3, 8))
        for i in range(4):
            Bi = np.dot(np.linalg.inv(J), np.array([dndxi[i], dndeta[i]]).T)
            B[0, 2 * i] = Bi[0]
            B[2, 2 * i + 1] = Bi[0]
            B[2, 2 * i] = Bi[1]
            B[1, 2 * i + 1] = Bi[1]
        return B, J

ちゃんと計算できているか確認しておきましょう。

MESH_SIZE = 5.
LENGTH = 20.
HEIGHT = 10.
THICKNESS = 1.
E = 208000 # ヤング率
NU = 0.27 # ポアソン比

nodes_dict, elems = create_mesh(LENGTH, HEIGHT, MESH_SIZE, THICKNESS)
material = Material(E, NU)
mesh = Mesh(0, nodes_dict, elems, material)
solver = Solver(mesh, material)
B, J = solver.calc_B(solver.mesh.elements[0], -1, -1) # xi, etaはとりあえず-1で算出
print(B)
print(J)
[[-0.2  0.   0.2  0.   0.   0.   0.   0. ]
 [ 0.  -0.2  0.   0.   0.   0.   0.   0.2]
 [-0.2 -0.2  0.   0.2  0.   0.   0.2  0. ]]
[[2.5 0. ]
 [0.  2.5]]

まぁよさそうですね。




終わりに

少々長くなったので今回はここまでとします。
残りのステップは次回やっていきましょう。

参考文献