有限要素法のためのPython入門

紹介する機能について

PythonにはNumpy/Matplotlib/Scipy/Getfem++などのパッケージがあり、それらを組み合わせることにより容易に有限要素法のスクリプトを組むことができます。それぞれのパッケージに関しては各Wikipediaの内容を確認してください。

Numpy (https://ja.wikipedia.org/wiki/NumPy)

Matplotlib(https://ja.wikipedia.org/wiki/Matplotlib)

Scipy (https://ja.wikipedia.org/wiki/SciPy)

Getfem++ (https://ja.wikipedia.org/wiki/Getfem%2B%2B)

それぞれのパッケージはPythonでは以下のように呼び出します。


In [ ]:
import numpy as np

これで、これらのパッケージはそれぞれ、np/sp/gf/pltとしてインポートされました。 各パッケージの機能は.の後にタブキーを打つことで実行可能な関数などが表示されます。


In [ ]:
np.

In [ ]:
np?

Numpyの学習 〜 ベクトル操作の基本 〜

本節ではNumpyのチュートリアル

https://docs.scipy.org/doc/numpy-dev/user/quickstart.html

の内容を解説します。

配列の作成

numpyを使用する利点としては多次元配列の操作が容易になることがあげられます。 numpyの配列のクラスはndarrayと呼ばれています。ndarrayの主な属性には主に以下のものがあります。

ndarray.ndim

配列の次元

ndarray.shape

配列の形状。n行m列の行列については、 形状 (M、N)となります 。

ndarray.size

配列の要素数の合計。

ndarray.dtype

配列の要素のタイプ。numpy.int32、numpy.int16、およびnumpy.float64などがあります。

ndarray.itemsize

配列の各要素のサイズ(バイト単位)。

ndarray.data

配列の実際の要素を含むバッファー。 インデックスで配列の要素にアクセスするため、この属性を使用する必要はありません。

実際に配列を定義してみます。例えば、ベクトル$b = \left\{ \begin{array}{c}6\\7\\8\end{array}\right\}$と$A=\left[\begin{array}{ccc} 1 & 2 & 0\\ 2 & 5 & 6\\ 0 & 6 & 9 \end{array}\right]$の行列を定義したい場合には次のように定義します。


In [ ]:
b = np.array([6, 7, 8])
A = np.array([[1, 2, 0],[2, 5, 6],[0, 6, 9]])
# bを出力します
print "b ="
print b
# Aを出力します
print "A ="
print A

np.arrayについて詳細を確認したい場合は次のようにコマンドを入力します。


In [ ]:
np.array?

このコマンドを入力するとコマンドの呼び出し方法・目的・パラメータの説明が出てきます。 詳細説明から分かるようにarrayには先ほど指定したobject以外にも引数があります。この引数のなかで"引数="となっているものはデフォルト引数と呼ばれ指定されない場合は説明に書かれた値が使用されます。

デフォルト引数を変更したい場合には以下のように指定します。


In [ ]:
A = np.array([[1, 2, 0],[2, 5, 6],[0, 6, 9]], dtype=complex)
print A

dtypeは詳細説明にもあるとおりarrayのデータ型を指定する引数です。これを"dtype=complex"としているため今回は複素数の行列が作成されています。

図の出力

ipoython notebookで2次元の図を出力する場合にはmatplotlibが使われます。

https://ja.wikipedia.org/wiki/Matplotlib

以下のようにモジュールを呼び出します。2行目の"%matplotlib inline"はmatplotlibの図をiPython Notebook内で描写したグラフを展開するためのものです。


In [ ]:
from matplotlib import pylab as plt
%matplotlib inline

In [ ]:
a = np.array([1.0, 2.0])
b = np.array([3.0, 4.0])
plt.plot(a, b)

線形代数

Numpyの線形代数に関する関数はnp.linalg以下にあります。


In [ ]:
np.linalg?

例えば、先ほど作成した行列$A$の逆行列を計算したい場合はinv関数を使用します。


In [ ]:
np.linalg.inv?

In [ ]:
AA = np.linalg.inv(A)
print AA

Aに対して行列積(np.dot)を行うと以下のように単位行列が作成されます。これにより正しく逆行列が計算されることがわかります。


In [ ]:
np.dot?

In [ ]:
np.dot(A,AA)

また、逆行列だけでなく固有値解析の実行も可能です。


In [ ]:
a, V = np.linalg.eig(A)
# 固有値を出力します
print "固有値="
print a
# 固有値ベクトル
print "固有ベクトル="
print V
plt.plot(V[:,0])
plt.plot(V[:,1])

ここまでのまとめ

  • Numpyの中心となっているのは配列を扱うためのarrayオブジェクトである。
  • arrayオブジェクトに対しては各種関数が準備されており、簡単に操作できる。
  • arrayはMatplotlibを使用して図化が可能である。

Scipyの学習〜疎行列による線形代数〜

ScipyはNumPyを基盤にした科学計算ライブラリです。本節ではScipyのチュートリアル

http://docs.scipy.org/doc/scipy/reference/tutorial/

の内容のうち、疎行列による線形代数関係のライブラリの説明をします。


In [ ]:
import numpy as np
import scipy as sp
from scipy import sparse

In [ ]:
sparse?

Scipyで疎行列を操作する際にはNumpyで使用していたarrayオブジェクトとは別のオブジェクトを使用します。疎行列に使用されているオブジェクトは複数あり、相互に変換可能ですが今回はcoo_matrixオブジェクトを紹介します。


In [ ]:
# A sparse matrix in COOrdinate format
sparse.coo_matrix?

マニュアルにある通り疎行列に代入するための値とそのインデックスを準備します。


In [ ]:
# 行のインデックス
I = np.array([0,0,1,1,1,2,2])
# 列のインデックス
J = np.array([0,1,0,1,2,1,2])
# 値
V = np.array([1,2,2,5,6,6,9])
A = sparse.coo_matrix((V,(I,J)),shape=(3,3))
print A

以上のように各インデックスに値の入った疎行列が作成されます。このようにオブジェクトを作成した際には必ずなんのオブジェクトであるかを確認しておきましょう。


In [ ]:
A?

また、これらの疎行列はファイルへの保存と読み込みが可能です。その際にはScipyのioモジュールを使用します。


In [ ]:
from scipy import io

In [ ]:
io?

ここではMatrix Market形式(http://math.nist.gov/MatrixMarket/formats.html) で書き出しを行ってみます。


In [ ]:
io.mmwrite("A.mtx", A)

ファイルの中身を出力してみるとMatrix Market形式で正しく出力されていることがわかります。Matrix Market形式は国際的に使われている疎行列のファイル形式です。


In [ ]:
%cat A.mtx

また、既存のファイルを読み込んで新しく行列を作成することも可能です。


In [ ]:
B = io.mmread("A.mtx")

以下のようにしてファイルから行列が読み込めていることを確認できます。


In [ ]:
print B

疎行列の線形代数

以上では、Pythonによる密行列の線形代数までを紹介してきました。しかしながら、有限要素法計算の際にはメモリ節約のため疎行列計算が行われます。その際にはscipyのsparseパッケージとその下のlinalgパッケージを使用します。


In [ ]:
import scipy as sp
from scipy.sparse import linalg

In [ ]:
linalg?

密行列と同じ操作を疎行列の線形代数パッケージで実行します。


In [ ]:
A  = A.tocsc()
AA = linalg.inv(A)
print AA

密行列の場合は行列積が"*"になりますので注意してください。先ほどと同じように単位行列が逆行列との積で得られます。


In [ ]:
print A*AA

また、固有値解析も少し複雑ですが以下のように計算できます。


In [ ]:
X = sp.rand(A.shape[0], 3)
linalg.lobpcg(A,X)

密行列の場合と同じ計算結果になることが確認できます。 それぞれの関数の説明は以下で確認してください。


In [ ]:
sp.rand?

In [ ]:
linalg.lobpcg?

ここまでのまとめ

  • 疎行列の計算にはScipyのsparseモジュールが便利である。
  • Scipyのioモジュールを使用すると、各種形式で行列の入出力が可能である。

Getfem++の学習


In [ ]:
import getfem as gf
import numpy as np
from scipy import io

本節ではメッシュ作成とそれを離散化した行列を作成するパッケージGetfem++について説明します。必要となるオブジェクトは以下の図に示すものになります。

Femオブジェクト

有限要素で何を使用するかを指定する。(ラグランジュ要素Pk、Qkなど)

Meshオブジェクト

メッシュの指定(point:節点とconvex:要素などから作られる)

Integオブジェクト

積分方法を指定する。

MeshFemオブジェクト

メッシュの要素に対してFemを設定したオブジェクト、自由度数などの情報も設定する。

MeshImオブジェクト

メッシュの要素に対して積分方法を指定したオブジェクト

Assemblyオブジェクト

離散化した行列組み立てのためのオブジェクト。MeshFemとMeshImを基に作成する。

Modelオブジェクト

モデル全体の情報を設定するオブジェクト。今回は未使用。

マニュアルからわかるように、規則的なメッシュを作成したり、Gmshのメッシュファイルなどからインポートすることも可能です。ただ、今回はメッシュオブジェクトに慣れることを目的としますので、まずは1次元の空のMeshオブジェクトを作成します。


In [ ]:
gf.Mesh?

In [ ]:
m = gf.Mesh('empty', 1)

ご覧のようにこの時点ではメッシュは空です。このメッシュに節点と要素を追加していきます。


In [ ]:
print m

要素を追加していきます。(add_convexメソッド)


In [ ]:
m.add_convex?

GeoTransが必要であることが分かりましたので、GeoTransのマニュアルも確認します。


In [ ]:
gf.GeoTrans?

座標 x = 0.0からx = 2.0の間に平行6面体要素を追加します。


In [ ]:
m.add_convex?

In [ ]:
m.add_convex(gf.GeoTrans('GT_QK(1,1)'),[[0,2]])

In [ ]:
m.add_convex(gf.GeoTrans('GT_QK(1,1)'),[[2,4]])

メッシュmに2つの要素が追加されました。


In [ ]:
print m

ちなみにこのMeshオブジェクトはVTKファイルにExportできます。 1次元のメッシュでは寂しいので、2次元の方眼メッシュを作成しましょう。


In [ ]:
m2 = gf.Mesh('cartesian', np.array([0.0, 1.0, 2.0]), np.array([0.0, 1.0, 2.0]))

In [ ]:
m2.export_to_vtk?

In [ ]:
m2.export_to_vtk('m2.vtk','ascii')

これをParaviewで確認すると4要素の方眼メッシュが作成されていることが分かります。

次に、先ほどの1次元のメッシュを使用して剛性マトリックスと質量マトリックスを作成してみます。


In [ ]:
gf.asm_linear_elasticity?

In [ ]:
gf.MeshFem?

変位用のMeshFemとデータ用のMeshFemを作成します。MeshFemはMesh上にFem要素を割り当てるためのオブジェクトです。


In [ ]:
mfu = gf.MeshFem(m,1) # 変位用MeshFem
mfd = gf.MeshFem(m,1) # データ用MeshFem

今回は1次元のLagrange要素$Q_1$を使用します。


In [ ]:
gf.Fem?

In [ ]:
fem = gf.Fem('FEM_QK(1,1)')

作成したFem要素をMeshFemに設定します。


In [ ]:
mfu.set_fem(fem)
mfd.set_fem(fem)

次は、積分要素を設定します。


In [ ]:
gf.Integ?

今回は1次のガウス積分を使用します。


In [ ]:
im = gf.Integ('IM_GAUSS1D(1)')

In [ ]:
mim = gf.MeshIm(m, im)

In [ ]:
gf.asm_linear_elasticity?

In [ ]:
gf.asm_mass_matrix?

In [ ]:
# ラメの弾性定数
Lambda = np.array([1.0, 1.0, 1.0])
Mu = np.array([1.0, 1.0, 1.0])
# 剛性行列の作成
K = gf.asm_linear_elasticity(mim, mfu, mfd, Lambda, Mu)
print K.full() # 剛性行列の出力
# 質量行列の作成
M = gf.asm_mass_matrix(mim, mfu)
print M.full() # 質量行列の出力

以上により作成した剛性マトリックスと質量マトリックスはSpmatというGetfem++独自の行列オブジェクトで、先ほど紹介したMatrixMarketのフォーマットに出力するメソッドも備えています。

また、自分で微分方程式を定義して計算することも可能です。その際にはgf.asm_volumicを使用します。


In [ ]:
gf.asm_volumic?

線形等方弾性問題の微分方程式は次のように表せます。

$\nabla\cdot\left[\left\{ \nabla\boldsymbol{U}+\left(\nabla\boldsymbol{U}\right)^{T}\right\}\mu + tr\left(\nabla\boldsymbol{U}\right)\boldsymbol{I}\lambda\right]=\boldsymbol{F}$

Getfem++ではテンソルを使用した表現で以下のように微分方程式を定義できます。


In [ ]:
K = gf.asm_volumic("lambda=data$1(#2);"+ \
                   "mu=data$2(#2);"+ \
                   "t=comp(vGrad(#1).vGrad(#1).Base(#2));"+ \
                   "M(#1,#1)+= sym(t(:,i,j,:,i,j,k).mu(k)+ t(:,j,i,:,i,j,k).mu(k)+ t(:,i,i,:,j,j,k).lambda(k))", 
                   mim, mfu, mfd, np.array([1.0, 1.0, 1.0]), np.array([1.0, 1.0, 1.0]))

当然ながら結果は同じになります。以下のようにすると"小さい"行列であれば先ほどのNumpyのarrayオブジェクトが出力されます。


In [ ]:
K.full()

もちろん、構造用のNavierの式の離散化だけではありません。laplacianやhelmholtzなどの関数があります。 つまり、1つのメッシュでさまざまな微分方程式の離散化が可能ということです。


In [ ]:
gf.asm_laplacian?

In [ ]:
gf.asm_helmholtz?

さて、この得られた行列はScipyの疎行列というわけではなく、SpmatというGetfem++(Gmm++)独自の形式です。


In [ ]:
K?

Matrix-Marketフォーマットで出力してみます。


In [ ]:
K.save('mm', "K.mtx")
M.save('mm', "M.mtx")

出力されたファイルはノートブックと同じディレクトリに保存されています。


In [ ]:
K = io.mmread("K.mtx")
M = io.mmread("M.mtx")

In [ ]:
print "K = "
print K
print "M = "
print M

このようにScipyの疎行列に容易に変換できます。そのため、Scipyを使用した静解析や固有値解析を行うことにより有限要素法の解析が可能となります。 さて1次元メッシュで次のような階がnp.arrayオブジェクトで得られたとします。


In [ ]:
outou = np.array([1.0, 2.0, 3.0])
print outou

これをベクトル情報付きのVTKファイルとして出力します。その際にはGetfem++のSliceオブジェクトを使用します。


In [ ]:
gf.Slice?

スライスオブジェクトを作成する際にはメッシュをスライスすることで断面などの表示か可能です。 しかし、今回は特に断面を見る必要もないので何もしません。


In [ ]:
sl = gf.Slice(('none',), m, 1)

スライスオブジェクトにはVTKにMeshFemオブジェクトと応答値であるarrayオブジェクトを引数としてVTKに出力する機能があります。


In [ ]:
sl.export_to_vtk?

In [ ]:
sl.export_to_vtk('m_results.vtk', 'ascii', mfu, outou, 'jiyuunanamae')

ここまでのまとめ

  • メッシュはGetfem++のMeshオブジェクトにより作成することができる。このメッシュはVTKファイルに出力しParaviewで確認することが可能である。
  • Meshオブジェクトに有限要素オブジェクトと積分法オブジェクトを割り当てることにより、それぞれMeshFemオブジェクトとMeshImオブジェクトを作成することができる。
  • MeshFemオブジェクトとMeshImオブジェクトから有限要素法により離散化した行列を得ることができる。この行列はSpmatオブジェクトであるが、ファイル出力をしてScipyで読み込むことで各種操作が可能である。

ギャラリー

今回、未紹介のもので出力が面白かったものをいくつか掲載しました。

電磁ポテンシャル


In [ ]:
"""
Demonstrates computation of gradient with matplotlib.tri.CubicTriInterpolator.
"""
from matplotlib.tri import Triangulation, UniformTriRefiner,\
    CubicTriInterpolator
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import numpy as np
import math
%matplotlib inline


#-----------------------------------------------------------------------------
# Electrical potential of a dipole
#-----------------------------------------------------------------------------
def dipole_potential(x, y):
    """ The electric dipole potential V """
    r_sq = x**2 + y**2
    theta = np.arctan2(y, x)
    z = np.cos(theta)/r_sq
    return (np.max(z) - z) / (np.max(z) - np.min(z))


#-----------------------------------------------------------------------------
# Creating a Triangulation
#-----------------------------------------------------------------------------
# First create the x and y coordinates of the points.
n_angles = 30
n_radii = 10
min_radius = 0.2
radii = np.linspace(min_radius, 0.95, n_radii)

angles = np.linspace(0, 2*math.pi, n_angles, endpoint=False)
angles = np.repeat(angles[..., np.newaxis], n_radii, axis=1)
angles[:, 1::2] += math.pi/n_angles

x = (radii*np.cos(angles)).flatten()
y = (radii*np.sin(angles)).flatten()
V = dipole_potential(x, y)

# Create the Triangulation; no triangles specified so Delaunay triangulation
# created.
triang = Triangulation(x, y)

# Mask off unwanted triangles.
xmid = x[triang.triangles].mean(axis=1)
ymid = y[triang.triangles].mean(axis=1)
mask = np.where(xmid*xmid + ymid*ymid < min_radius*min_radius, 1, 0)
triang.set_mask(mask)

#-----------------------------------------------------------------------------
# Refine data - interpolates the electrical potential V
#-----------------------------------------------------------------------------
refiner = UniformTriRefiner(triang)
tri_refi, z_test_refi = refiner.refine_field(V, subdiv=3)

#-----------------------------------------------------------------------------
# Computes the electrical field (Ex, Ey) as gradient of electrical potential
#-----------------------------------------------------------------------------
tci = CubicTriInterpolator(triang, -V)
# Gradient requested here at the mesh nodes but could be anywhere else:
(Ex, Ey) = tci.gradient(triang.x, triang.y)
E_norm = np.sqrt(Ex**2 + Ey**2)

#-----------------------------------------------------------------------------
# Plot the triangulation, the potential iso-contours and the vector field
#-----------------------------------------------------------------------------
plt.figure()
plt.gca().set_aspect('equal')
plt.triplot(triang, color='0.8')

levels = np.arange(0., 1., 0.01)
cmap = cm.get_cmap(name='hot', lut=None)
plt.tricontour(tri_refi, z_test_refi, levels=levels, cmap=cmap,
               linewidths=[2.0, 1.0, 1.0, 1.0])
# Plots direction of the electrical vector field
plt.quiver(triang.x, triang.y, Ex/E_norm, Ey/E_norm,
           units='xy', scale=10., zorder=3, color='blue',
           width=0.007, headwidth=3., headlength=4.)

plt.title('Gradient plot: an electrical dipole')
plt.show()

アニメーション出力


In [ ]:
from tempfile import NamedTemporaryFile
from IPython.display import HTML

VIDEO_TAG = """<video controls>
 <source src="data:video/x-m4v;base64,{0}" type="video/mp4">
 Your browser does not support the video tag.
</video>"""

def anim_to_html(anim):
    if not hasattr(anim, '_encoded_video'):
        with NamedTemporaryFile(suffix='.mp4') as f:
            anim.save(f.name, fps=20, extra_args=['-vcodec', 'libx264'])
            video = open(f.name, "rb").read()
        anim._encoded_video = video.encode("base64")
    
    return VIDEO_TAG.format(anim._encoded_video)

def display_animation(anim):
    plt.close(anim._fig)
    return HTML(anim_to_html(anim))

In [ ]:
import pylab as plt
import numpy as np
from matplotlib import animation

# First set up the figure, the axis, and the plot element we want to animate
fig = plt.figure()
ax = plt.axes(xlim=(0, 2), ylim=(-2, 2))
line, = ax.plot([], [], lw=2)

# initialization function: plot the background of each frame
def init():
    line.set_data([], [])
    return line,

# animation function.  This is called sequentially
def animate(i):
    x = np.linspace(0, 2, 1000)
    y = np.sin(2 * np.pi * (x - 0.01 * i))
    line.set_data(x, y)
    return line,

# call the animator.  blit=True means only re-draw the parts that have changed.
anim = animation.FuncAnimation(fig, animate, init_func=init,
                               frames=100, interval=20, blit=True)

# call our new function to display the animation
display_animation(anim)

おすすめの本

Python入門書としては、以下の書籍がわかりやすいです。Amazonで購入できます。 また、IPythonやNumpy/Scipyを勉強するには以下の書籍がおすすめです。 AmazonのKindleで購入が可能となっています。


In [ ]: