Google Colabで始めるFEniCSx入門:Poisson方程式,誤差評価,log-logプロット

Introduction

有限要素法の数値計算を学ぶとき,最初の題材としてPoisson方程式はとても扱いやすいです.関数空間を作り,線形偏微分方程式を解くという基本の流れを身に付けることが出来ます.また,Google Colab では FEM on Colab が FEniCSxの導入方法を案内しており,Colab上で比較的素直に実験を始めることができます.このページでは,Google Colabで実際にノートブックを作成し,FEniCSxを導入し,Poisson方程式を解き,絶対誤差・相対誤差・log-logプロットまで確認する過程をまとめます.

今回の目標は 5 つです.

1.Google ColabでFEniCSxを動かす手順を,実際の操作の流れとして確認すること.
2.Poisson方程式をP1有限要素で解くコードを理解すること.
3.絶対誤差,相対誤差,log-logプロットの意味を,実験結果と結びつけて読むこと.
4.メッシュの切り方と誤差の測り方が,結果の読み方にどのように効くかを確認すること.
5.2次元のメッシュ図と3次元の近似解を可視化して,計算結果を形として確認すること.

対象とするPoisson問題は \[ -\Delta u = f \quad \text{in } \Omega := (0,1)^2,\quad u=0 \quad \text{on } \partial\Omega \] です.厳密解として \[ u(x,y)=\sin(\pi x)\sin(\pi y) \] を用い, \[ f=2\pi^2 u \] を右辺として与えます.この設定により,相対誤差 \[ \displaystyle \frac{|u-u_h|_{H^1}}{|u|_{H^1}},\quad \frac{\|u-u_h\|_{L^2}}{\|u\|_{L^2}} \] を確かめます.

Google Colabでの実行手順

まずは Google Colab をブラウザで開きます.新しいノートブックを作成し,タイトルをたとえば fenicsx_poisson_error_demo.ipynb のように付けます.

Google Colabでは,特別な設定をしなくても標準のCPUランタイムで実行できます.このページの例ではGPUは不要です.セルを上から順に実行していけば動くようにしてあります.初回だけ導入セルの実行に少し時間がかかることがありますが,その後は普段のPythonノートブックと同じ感覚で進められます.

セル1: FEniCSxの導入

最初のセルは,ColabにFEniCSx が入っているかを確認し,入っていなければ導入するためのものです.

# すでに dolfinx が入っているかを試します
try:
    import dolfinx

# 入っていなければ,Colab 用のインストールスクリプトを実行します
except ImportError:
    !wget "https://fem-on-colab.github.io/releases/fenicsx-install-release-real.sh" -O "/tmp/fenicsx-install.sh" \
        && bash "/tmp/fenicsx-install.sh"

# 最後に,ちゃんと import できることを確認します
import dolfinx
print("dolfinx is ready.")

このセルを実行して最後に dolfinx is ready. と出れば,準備完了です.ここは数学的な本体ではなく,あくまで環境構築の部分です.

セル 2:Poisson方程式を解いて誤差を測る

次が本体セルです.MPIはFEniCSxの設計上必要ですが,今回は COMM に押し込めて,有限要素法の本体が見える形にしています.また,現行のDOLFINxでは LinearProblempetsc_options_prefix を与える必要があるので,その点も反映しています.また,次の2点を取り入れています.第一に,対角線を交互に入れて,左上と右下の角に生じる「死んだ要素」を避けています.第二に,誤差評価では補間関数ではなく厳密解そのもののUFL表現を使い,真の\(H^1\)誤差を測っています.

# NumPy を使います
import numpy as np

# グラフ描画に Matplotlib を使います
import matplotlib.pyplot as plt

# 弱形式を書くために UFL を使います
import ufl

# FEniCSx は MPI ベースなので communicator を読み込みます
from mpi4py import MPI

# メッシュと有限要素空間のための機能を読み込みます
from dolfinx import mesh, fem

# 線形問題をまとめて解くためのクラスを読み込みます
from dolfinx.fem.petsc import LinearProblem

# 今回は 1 プロセスとして,communicator を 1 つだけ名前で持ちます
COMM = MPI.COMM_WORLD


# 厳密解 u(x,y)=sin(pi x)sin(pi y) を Python 関数として定義します
# これは境界値の補間に使います
def exact_solution(x):
    return np.sin(np.pi * x[0]) * np.sin(np.pi * x[1])


# N を受け取って,N×N 分割メッシュ上で計算し,誤差を返す関数を作ります
def solve_and_measure(N):

    # 対角線を交互に入れて,角の「死んだ要素」を避けます
    domain = mesh.create_unit_square(
        COMM,
        N,
        N,
        diagonal=mesh.DiagonalType.left_right,
    )

    # 1 次 Lagrange 要素による有限要素空間 V_h を作ります
    V = fem.functionspace(domain, ("Lagrange", 1))

    # 空間座標 x=(x_0,x_1) を UFL の記号として定義します
    x = ufl.SpatialCoordinate(domain)

    # 厳密解を UFL の式として定義します
    # 誤差評価ではこちらを使って「真の誤差」を測ります
    u_exact_expr = ufl.sin(np.pi * x[0]) * ufl.sin(np.pi * x[1])

    # 右辺 f = -Δu を UFL で定義します
    f = 2 * np.pi**2 * ufl.sin(np.pi * x[0]) * ufl.sin(np.pi * x[1])

    # 境界条件で使うために,有限要素空間上の関数 u_D を作ります
    u_D = fem.Function(V)

    # 厳密解を節点に補間して,Dirichlet 境界値として使える形にします
    u_D.interpolate(exact_solution)

    # 空間次元は 2 なので,境界は 1 次元の面です
    fdim = domain.topology.dim - 1

    # 境界上の全ての facet を見つけます
    boundary_facets = mesh.locate_entities_boundary(
        domain, fdim, lambda x: np.full(x.shape[1], True, dtype=bool)
    )

    # 境界上の自由度を取り出します
    boundary_dofs = fem.locate_dofs_topological(V, fdim, boundary_facets)

    # Dirichlet 境界条件 u=u_D を作ります
    bc = fem.dirichletbc(u_D, boundary_dofs)

    # TrialFunction は未知関数 u_h を表す記号です
    u = ufl.TrialFunction(V)

    # TestFunction は試験関数 v を表す記号です
    v = ufl.TestFunction(V)

    # 双線形形式 a(u,v)=∫ grad(u)・grad(v) dx を定義します
    a = ufl.inner(ufl.grad(u), ufl.grad(v)) * ufl.dx

    # 線形形式 L(v)=∫ f v dx を定義します
    L = f * v * ufl.dx

    # 線形問題 a(u_h,v)=L(v) を解く準備をします
    problem = LinearProblem(
        a,
        L,
        bcs=[bc],
        petsc_options_prefix=f"poisson_{N}_",
        petsc_options={
            "ksp_type": "preonly",
            "pc_type": "lu",
            "ksp_error_if_not_converged": True,
        },
    )

    # 実際に線形方程式を解いて数値解 u_h を得ます
    u_h = problem.solve()

    # L2 絶対誤差 ||u-u_h||_{L2} を計算します
    abs_L2 = np.sqrt(
        fem.assemble_scalar(
            fem.form((u_h - u_exact_expr) ** 2 * ufl.dx)
        )
    )

    # H1 半ノルム絶対誤差 |u-u_h|_{H1} を計算します
    # ここでは補間関数ではなく,厳密解そのものの勾配と比較しています
    abs_H1 = np.sqrt(
        fem.assemble_scalar(
            fem.form(
                ufl.inner(
                    ufl.grad(u_h - u_exact_expr),
                    ufl.grad(u_h - u_exact_expr),
                ) * ufl.dx
            )
        )
    )

    # 厳密解の L2 ノルム ||u||_{L2} を計算します
    norm_L2 = np.sqrt(
        fem.assemble_scalar(
            fem.form(u_exact_expr ** 2 * ufl.dx)
        )
    )

    # 厳密解の H1 半ノルム |u|_{H1} を計算します
    seminorm_H1 = np.sqrt(
        fem.assemble_scalar(
            fem.form(
                ufl.inner(ufl.grad(u_exact_expr), ufl.grad(u_exact_expr)) * ufl.dx
            )
        )
    )

    # 相対 L2 誤差を計算します
    rel_L2 = abs_L2 / norm_L2

    # 相対 H1 半ノルム誤差を計算します
    rel_H1 = abs_H1 / seminorm_H1

    # 一様メッシュでは代表的なメッシュ幅として h=1/N とみなせます
    h = 1.0 / N

    # 後で表やグラフに使うため,必要な値をまとめて返します
    return h, abs_L2, rel_L2, abs_H1, rel_H1


# 誤差を見るために,粗いメッシュから細かいメッシュまで並べます
Ns = [8, 16, 32, 64, 128]

# h を入れる空リストを用意します
hs = []

# L2 の絶対誤差を入れる空リストを用意します
abs_L2s = []

# L2 の相対誤差を入れる空リストを用意します
rel_L2s = []

# H1 半ノルムの絶対誤差を入れる空リストを用意します
abs_H1s = []

# H1 半ノルムの相対誤差を入れる空リストを用意します
rel_H1s = []

# 各メッシュサイズで solve_and_measure を実行します
for N in Ns:
    h, abs_L2, rel_L2, abs_H1, rel_H1 = solve_and_measure(N)
    hs.append(h)
    abs_L2s.append(abs_L2)
    rel_L2s.append(rel_L2)
    abs_H1s.append(abs_H1)
    rel_H1s.append(rel_H1)

# あとで計算しやすいように NumPy 配列に変えます
hs = np.array(hs)
abs_L2s = np.array(abs_L2s)
rel_L2s = np.array(rel_L2s)
abs_H1s = np.array(abs_H1s)
rel_H1s = np.array(rel_H1s)


# 連続する 2 点の log の差から実験的収束率を計算する関数を作ります
def rates(errors, hs):
    return np.log(errors[1:] / errors[:-1]) / np.log(hs[1:] / hs[:-1])


# 絶対誤差と相対誤差の収束率を表示します
print("L2 absolute rates  =", rates(abs_L2s, hs))
print("L2 relative rates  =", rates(rel_L2s, hs))
print("H1 absolute rates  =", rates(abs_H1s, hs))
print("H1 relative rates  =", rates(rel_H1s, hs))

# L2 誤差の log-log プロットを描きます
plt.figure(figsize=(6, 5))
plt.loglog(hs, abs_L2s, "o-", label="absolute L2 error")
plt.loglog(hs, rel_L2s, "s--", label="relative L2 error")
plt.xlabel("h")
plt.ylabel("error")
plt.title("L2 errors on a log-log plot")
plt.grid(True, which="both")
plt.legend()
plt.show()

# H1 半ノルム誤差の log-log プロットを描きます
plt.figure(figsize=(6, 5))
plt.loglog(hs, abs_H1s, "o-", label="absolute H1 seminorm error")
plt.loglog(hs, rel_H1s, "s--", label="relative H1 seminorm error")
plt.xlabel("h")
plt.ylabel("error")
plt.title("H1 seminorm errors on a log-log plot")
plt.grid(True, which="both")
plt.legend()
plt.show()

セル2の1行ごとの解説

以下では,本体セルを上から順に解説します.かなり長いです.

  1. import numpy as np
    数値計算用ライブラリ NumPy を読み込みます.
  2. import matplotlib.pyplot as plt
    グラフ描画用の Matplotlib を読み込みます.
  3. import ufl
    弱形式や厳密解の式を記述するために UFL を読み込みます.
  4. from mpi4py import MPI
    FEniCSx が communicator を必要とするので MPI を読み込みます.今回は 1 プロセス用です.
  5. from dolfinx import mesh, fem
    メッシュ生成,有限要素空間,有限要素関数のための機能を読み込みます.
  6. from dolfinx.fem.petsc import LinearProblem
    線形変分問題をまとめて解くクラスを読み込みます.
  7. COMM = MPI.COMM_WORLD
    MPI の詳細を前面に出しすぎないため,communicator を COMM にまとめています.
  8. def exact_solution(x):
    境界値の補間に使う Python 関数を定義します.
  9. return np.sin(np.pi * x[0]) * np.sin(np.pi * x[1])
    厳密解 \(u(x,y)=\sin(\pi x)\sin(\pi y)\) を返します.
  10. def solve_and_measure(N):
    格子分割数 \(N\) を受け取り,その格子で解いて誤差を返す関数です.
  11. domain = mesh.create_unit_square(..., diagonal=mesh.DiagonalType.left_right)
    ここが前版からの重要な修正です.対角線を交互に入れることで,角の 3 頂点すべてが Dirichlet 境界上にある三角形を避けます.
  12. V = fem.functionspace(domain, ("Lagrange", 1))
    1 次 Lagrange 要素空間 \(V_h\) を定義します.
  13. x = ufl.SpatialCoordinate(domain)
    空間座標を UFL の記号として用意します.
  14. u_exact_expr = ...
    厳密解を UFL の式として定義します.誤差評価ではこちらを使うのが重要です.
  15. f = 2 * np.pi**2 * ...
    右辺 \(f=-\Delta u\) を厳密解から作っています.
  16. u_D = fem.Function(V)
    Dirichlet 境界条件を入れるための有限要素関数を作ります.
  17. u_D.interpolate(exact_solution)
    厳密解を節点に補間し,境界値として使います.
  18. fdim = domain.topology.dim - 1
    2 次元領域なので,境界 facet は 1 次元です.
  19. boundary_facets = mesh.locate_entities_boundary(...)
    全境界を取り出します.
  20. boundary_dofs = fem.locate_dofs_topological(...)
    境界に載っている自由度番号を取り出します.
  21. bc = fem.dirichletbc(u_D, boundary_dofs)
    Dirichlet 条件 \(u=u_D\) を作ります.
  22. u = ufl.TrialFunction(V)
    未知関数 \(u_h\) に対応する記号です.
  23. v = ufl.TestFunction(V)
    試験関数 \(v\) を定義します.
  24. a = ufl.inner(ufl.grad(u), ufl.grad(v)) * ufl.dx
    双線形形式 \(a(u,v)=\int_\Omega \nabla u\cdot \nabla v\,dx\) を定義します.
  25. L = f * v * ufl.dx
    線形形式 \(L(v)=\int_\Omega fv\,dx\) を定義します.
  26. problem = LinearProblem(...)
    線形変分問題を組み立てる準備です.
  27. petsc_options_prefix=f"poisson_{N}_"
    現行 DOLFINx では prefix を必須で与えます.格子ごとに一意になるように \(N\) を入れています.
  28. "ksp_type": "preonly"
    Krylov 反復を行わず,前処理側の解法をそのまま使います.
  29. "pc_type": "lu"
    見通しを優先し,LU による直接法を使います.
  30. "ksp_error_if_not_converged": True
    ソルバが収束しなかったときにその場で気づけるようにします.
  31. u_h = problem.solve()
    離散方程式を解いて数値解 \(u_h\) を得ます.
  32. abs_L2 = ... (u_h - u_exact_expr) ...
    ここも前版からの重要な修正です.補間関数ではなく,厳密解そのものとの \(L^2\) 差を測っています.
  33. abs_H1 = ... grad(u_h - u_exact_expr) ...
    こちらが本稿の中心です.以前のように補間関数との差を取ると supercloseness を見てしまうので,真の \(H^1\) 誤差を測る形に直しています.
  34. norm_L2 = ... u_exact_expr ** 2 ...
    相対 \(L^2\) 誤差の分母となる厳密解の \(L^2\) ノルムです.
  35. seminorm_H1 = ... grad(u_exact_expr) ...
    相対 \(H^1\) 半ノルム誤差の分母を計算しています.
  36. rel_L2 = abs_L2 / norm_L2
    絶対 \(L^2\) 誤差を厳密解の大きさで割って,相対誤差を作ります.
  37. rel_H1 = abs_H1 / seminorm_H1
    同様に,相対 \(H^1\) 半ノルム誤差を作ります.
  38. h = 1.0 / N
    一様格子なので,代表的メッシュ幅を \(h=1/N\) とみなします.
  39. return h, abs_L2, rel_L2, abs_H1, rel_H1
    後で表やグラフに使う値をまとめて返します.
  40. Ns = [8, 16, 32, 64, 128]
    粗い格子から細かい格子までの分割数を並べます.
  41. hs = [], abs_L2s = [], ...
    メッシュ幅と誤差列を保存する空リストを用意します.
  42. for N in Ns:
    各格子で解いて誤差を集めます.
  43. hs.append(h) など
    各格子で得られた値を順番に保存します.
  44. hs = np.array(hs) など
    後で計算しやすいように NumPy 配列へ変換します.
  45. def rates(errors, hs):
    実験的収束率を計算する関数を定義します.
  46. return np.log(errors[1:] / errors[:-1]) / np.log(hs[1:] / hs[:-1])
    連続する 2 点の log の差から収束率を求めます.
  47. print("L2 absolute rates =", ...)
    \(L^2\) の収束率を表示します.
  48. print("H1 absolute rates =", ...)
    \(H^1\) の収束率を表示します.ここが 1 に近ければ標準的理論と整合的です.
  49. plt.loglog(...)
    両軸を対数目盛にしたグラフを描きます.
  50. plt.grid(True, which="both")
    対数目盛でも読みやすいように補助線を付けます.
  51. plt.show()
    最後に図を表示します.

セル 3:2次元メッシュ図と3次元近似解の可視化

誤差や収束率だけでなく,実際にどのような三角形分割で計算しているか,また数値解がどのような形をしているかも見せると理解が深まります.そこで,最後に可視化専用のセルを追加します.このセルはセル2のあとに実行してください.

以下のコードでは,まず表示用の分割数 N_vis を選び,そのメッシュ上でPoisson問題を解き直します.そのうえで,2次元のメッシュ図,2次元の等高線図,3次元の近似解曲面図を順に表示します.

# 3D 描画を使うために読み込みます
from mpl_toolkits.mplot3d import Axes3D  # noqa: F401

# 可視化専用の分割数です
# 12〜24 程度にすると見やすいです
N_vis = 16

# -------------------------
# 1. 可視化用の問題をもう一度解きます
# -------------------------
domain_vis = mesh.create_unit_square(
    COMM,
    N_vis,
    N_vis,
    diagonal=mesh.DiagonalType.left_right,
)

V_vis = fem.functionspace(domain_vis, ("Lagrange", 1))

x_vis = ufl.SpatialCoordinate(domain_vis)
f_vis = 2 * np.pi**2 * ufl.sin(np.pi * x_vis[0]) * ufl.sin(np.pi * x_vis[1])

u_D_vis = fem.Function(V_vis)
u_D_vis.interpolate(exact_solution_numpy)

fdim_vis = domain_vis.topology.dim - 1
boundary_facets_vis = mesh.locate_entities_boundary(
    domain_vis,
    fdim_vis,
    lambda x: np.full(x.shape[1], True, dtype=bool),
)
boundary_dofs_vis = fem.locate_dofs_topological(V_vis, fdim_vis, boundary_facets_vis)
bc_vis = fem.dirichletbc(u_D_vis, boundary_dofs_vis)

u_vis = ufl.TrialFunction(V_vis)
v_vis = ufl.TestFunction(V_vis)

a_vis = ufl.inner(ufl.grad(u_vis), ufl.grad(v_vis)) * ufl.dx
L_vis = f_vis * v_vis * ufl.dx

problem_vis = LinearProblem(
    a_vis,
    L_vis,
    bcs=[bc_vis],
    petsc_options_prefix=f"visual_{N_vis}_",
    petsc_options={
        "ksp_type": "preonly",
        "pc_type": "lu",
        "ksp_error_if_not_converged": True,
    },
)

u_h_vis = problem_vis.solve()

# -------------------------
# 2. 2 次元のメッシュ図を描きます
# -------------------------
domain_vis.topology.create_connectivity(domain_vis.topology.dim, 0)
cells_vis = domain_vis.topology.connectivity(domain_vis.topology.dim, 0).array.reshape(-1, 3)
points_vis = domain_vis.geometry.x[:, :2]

plt.figure(figsize=(6, 6))
plt.triplot(points_vis[:, 0], points_vis[:, 1], cells_vis, linewidth=0.8)
plt.xlabel("x")
plt.ylabel("y")
plt.title(f"2D mesh (N = {N_vis})")
plt.gca().set_aspect("equal")
plt.show()

# -------------------------
# 3. 2 次元の等高線図を描きます
# -------------------------
num_cells_vis = domain_vis.topology.index_map(domain_vis.topology.dim).size_local
triangles_vis = np.vstack([V_vis.dofmap.cell_dofs(c) for c in range(num_cells_vis)])

# ここでは reshape せず,自由度座標をそのまま受け取ります
dof_coords_vis = V_vis.tabulate_dof_coordinates()

# 2 次元表示なので x, y の 2 成分だけを使います
xy_vis = dof_coords_vis[:, :2]

# 自由度に対応する数値解の値を取り出します
values_vis = u_h_vis.x.array.real

plt.figure(figsize=(6, 5))
plt.tricontourf(
    xy_vis[:, 0],
    xy_vis[:, 1],
    triangles_vis,
    values_vis,
    levels=20,
)
plt.colorbar(label="u_h")
plt.xlabel("x")
plt.ylabel("y")
plt.title(f"2D contour of FEM solution (N = {N_vis})")
plt.gca().set_aspect("equal")
plt.show()

# -------------------------
# 4. 近似解の 3 次元曲面図を描きます
# -------------------------
fig = plt.figure(figsize=(8, 6))
ax = fig.add_subplot(111, projection="3d")

ax.plot_trisurf(
    xy_vis[:, 0],
    xy_vis[:, 1],
    values_vis,
    triangles=triangles_vis,
    linewidth=0.2,
    antialiased=True,
)

ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_zlabel("u_h")
ax.set_title(f"3D surface of FEM solution (N = {N_vis})")
plt.show()

このセルを実行すると,まず三角形分割そのものが見えます.次に,近似解の等高線図から,中心付近で値が大きく,境界で0になっている様子が確認できます.最後の3次元図では, \[ u_h(x,y)\approx \sin(\pi x)\sin(\pi y) \] の山型の形が視覚的に分かります.

メッシュ図では「どの三角形で計算しているか」を確認し,等高線図では「平面上での分布」を確認し,3次元図では「解の全体形状」を確認すると自然です.とくに,境界条件が0なので,3次元図でも周囲が下がっていることが分かります.

メッシュの切り方について

一方向にそろえた対角線で単位正方形を三角形分割すると,左上と右下の角に,3頂点すべてが境界上にある三角形が生じます.P1要素かつ同次Dirichlet条件のもとでは,そのような三角形上で有限要素関数は恒等的に0になります.したがって,その要素はメッシュとしては存在していても,近似空間の観点では実質的に「死んだ要素」です.

最初の例としては,この人工的な偏りを避けた方が自然です.そこで本稿では, diagonal=mesh.DiagonalType.left_right を使って,対角線を交互に入れるメッシュにしています.

絶対誤差と相対誤差

今回のコードでは,まず絶対誤差 \[ \|u-u_h\|_{L^2}, \quad |u-u_h|_{H^1} \] を計算しています.これは,数値解が厳密解からどれだけ離れているかをそのまま測る量です.

そのうえで,相対誤差 \[ \displaystyle \frac{\|u-u_h\|_{L^2}}{\|u\|_{L^2}}, \quad \frac{|u-u_h|_{H^1}}{|u|_{H^1}} \] も計算しています.こちらは,解そのものの大きさに対してどれだけずれているかを見る量です.

これは「絶対誤差はズレそのもの,相対誤差は元の大きさに対するズレ」を意味します.今回のように厳密解を固定してメッシュだけ細かくする実験では,相対誤差は絶対誤差を定数で割っただけなので,収束次数そのものは同じになります.

それでも相対誤差を出す理由ははっきりあります.第一に,問題ごとに解の大きさが違っていても比較しやすくなることです.たとえば,ある問題では解のノルムが100で,別の問題では0.01かもしれません.このとき絶対誤差だけを並べると読みづらくなりますが,相対誤差なら「解の大きさに対して何パーセントずれているか」という見方ができます.第二に,「誤差の大きさ」と「解そのものの大きさ」を区別して考える練習になることです.有限要素法では,誤差評価はしばしばノルムの比として整理されるので,相対誤差に慣れておくことには意味があります.第三に,グラフや表で結果を示したとき,異なる設定の計算を並べやすくなることです.

要するに,今回の例では絶対誤差でも相対誤差でも収束次数は同じですが,相対誤差を載せておくと「どれくらい良く近似できているか」を直感的に読みやすくなります.

\(H^1\) が \(h^2\) に見えるときがあるのか

厳密解を同じP1空間に補間した関数との \(H^1\) 差を測る場合です.しかしそれは,真の誤差 \(|u-u_h|_{H^1}\) ではなく, \(|u_h-I_hu|_{H^1}\) を見ていることになります.

構造化されたきれいな格子では,数値解 \(u_h\) と補間解 \(I_hu\) が真の誤差よりも1次高く近づくことがあり,これを supercloseness と呼びます.したがって,見かけ上 \(H^1\) 半ノルムで \(h^2\) が出ても,それを「P1 要素が真の \(H^1\) 誤差で 2 次収束した」と読んではいけません.

この点を避けるために,本稿では誤差評価にu_exact_expr = ufl.sin(...)を使い,厳密解そのものに対する差を積分する形としました.

log-logプロットは何を見せているのか

log-logプロットを使う理由は,誤差の減り方を「目で見てすぐ分かる形」に直したいからです.有限要素法では,誤差はしばしば \[ E(h) \approx C h^r \] のようなべき乗則で減ります.しかし,通常の線形目盛では,この関係から収束次数 \(r\) を読み取るのはあまり容易ではありません.とくに,メッシュを細かくするにつれて誤差が急激に小さくなると,点が下側に詰まって見えやすく,どの程度きれいに \(h\) の何乗で減っているかが判断しにくくなります.

そこで両軸を対数目盛にすると,このべき乗則が直線に変わります.実際,両辺の対数を取ると \[ \log E(h) = \log C + r\log h \] となります.つまり,横軸を \(\log h\),縦軸を \(\log E(h)\) にして両方を対数目盛にすると,点は直線に並びます.そして,その傾きが収束次数 \(r\) です.このため,log-log プロットは「有限要素法が期待どおりの次数で収束しているか」を一目で確かめるための道具としてとても便利です.

1次Lagrange要素で滑らかな Poisson問題を解く場合には,理論上, \[ |u-u_h|_{H^1} = O(h), \quad \|u-u_h\|_{L^2} = O(h^2) \] が期待されます.したがって,出力される収束率がH1では1に近く,L2では2に近ければ,標準的理論と整合的に動いていると読めます.

このページの使い方

この例題のよいところは,偏微分方程式,弱形式,有限要素空間,境界条件,誤差評価,収束率という有限要素法の基本要素が1本のノートブックでつながることです.加えて,今回は, 「メッシュの作り方が近似空間に影響する」ことと「何を誤差と呼ぶかで観測される次数が変わる」ことも自然に話題にできます.

したがって,単なる導入実習としてだけでなく,講義の中で「正しい数値実験の読み方」を教える素材としても使いやすいと思います.

References

ページの先頭へ