異方性メッシュ族をPythonで再構成する:6つのmesh familyの節点座標・要素接点・境界辺接点表からgood/badを読む
Introduction
このページでは,Interpolation error analysis using a new geometric parameter Section 9 で与えられている異方性メッシュ族をコードに沿って解説します.
方針は単純です.まず 1 本の完成版コード (Appendix)で,節点座標対応表,要素接点対応表,境界辺接点対応表,メッシュ図,そして MinAngle / MaxAngle 型の比較表までまとめて出せるようにします.そのうえで,Section 9.1 の good family,Section 9.2 の指標,Section 9.3 の bad family を順に読み解きます.
1.このページで何を解説するのか
ここでは,記事本文は「Section 9 の意味を説明すること」に集中し,詳細な実装は完成版コード (Appendix)にまとめました.これにより,読者はまず「何を再現したいのか」を理解し,そのあとで「どの関数が何を担当しているのか」を追えるようになります.
特に Section 9 では,9.1 に (I) Standard,(II) Shishkin,(III) anisotropic mesh from [25],(IV) graded mesh が与えられ,9.2 では MinAngle と MaxAngle 型の量が導入され,9.3 では bad family として (V) Shishkin mesh,(VI) Graded mesh が挙げられています.この記事では,この流れそのものを崩さないことを優先します.注意: (V)は bad と分類していますが,古典的意味で収束はします.理論に係数が悪くなる可能性があるという意味で bad に分類しています.
2.完成版コード (Appendix)の役割
完成版コードの役割は次の6点です.
- Section 9.1 の (I)–(IV) の節点列を生成すること.
- Section 9.3 の (V),(VI) を,x 方向は staggered row,y 方向は Shishkin/graded で構成すること.
- 節点座標対応表を出すこと.
- 要素接点対応表と境界辺接点対応表を出すこと.
- メッシュ図を描き,必要なら節点番号・要素番号・境界辺番号を重ねること.
MinAngleとMaxAngle型の量を計算し,6つの family を一つの表で比較すること.
つまり,このファイル 1 本を見れば,Section 9 の mesh family を「図」と「表」と「指標」の三つの視点からまとめて確認できます.
3.Section 9.1:good family (I)–(IV)
Section 9.1 の (I)–(IV) は,まず節点列 \((x_1^i, x_2^i)\) を与え,その tensor-product grid を三角形に分割することで得られます.完成版コードでは,この部分を section9_lines(kind, N, ...) が担当しています.
- (I) Standard mesh:x 方向・y 方向ともに一様分割です.
- (II) Shishkin mesh:x 方向は一様,y 方向のみ Shishkin 型に分割します.
- (III) Anisotropic mesh from [25]:x 方向・y 方向とも cosine clustering を使います.
- (IV) Graded mesh:x 方向は一様,y 方向は \((i/N)^2\) とします.
この4つは good family 側の導入例として使いやすく,節点座標対応表と要素接点対応表を出すだけでも,異方性の入り方の違いがかなりよく見えます.
4.Section 9.2:MinAngle と MaxAngle をどう読むか
Section 9.2 では,各三角形の 3 辺長を \(|L_1| \le |L_2| \le |L_3|\) の順に並べたうえで,
\[ \mathrm{MinAngle}:=\max_{T\in\mathcal T_h}\frac{|L_3|^2}{|T|_2}, \qquad \mathrm{MaxAngle}:=\max_{T\in\mathcal T_h}\frac{|L_1||L_2|}{|T|_2} \]
を計算しています.完成版コードでは,tri_edge_lengths,tri_area,section9_indicators がこの部分を担当します.
ここで大事なのは,辺長は厳密に L1 < L2 < L3 ではなく,L1 <= L2 <= L3 と並べることです.等辺や二等辺の要素では辺長が一致することがあるからです.完成版コードでも,np.sort(...) によりこの順序を保証しています.
5.Section 9.3:bad family (V), (VI)
Section 9.3 では (V) Shishkin mesh と (VI) Graded mesh が bad family として現れます.ここで注意すべきなのは,(V),(VI) を「別の y 方向節点列」として考えるだけでは不十分で,x 方向の切り方そのものが 9.1 と異なることです.
このコードでは,odd row は通常格子,even row は半ステップずれた格子になっており,さらに odd strip/even strip ごとに三角形の貼り方を変えています.完成版コードでは,この考え方を mesh1_style_rows と build_mesh1_style_mesh に移しました.
したがって,(V),(VI) の構成は次のように読むのが自然です.
- (V):y 方向は Shishkin,x 方向は staggered row.
- (VI):y 方向は graded,x 方向は staggered row.
これにより,図だけでは分かりにくかった「どこが 9.1 の family と違うのか」が,節点列と要素接続の両方から読めるようになります.
6.A型/B型と good/bad の見方
本ページでは,メッシュ退化を二つに分けて読みます.
- A型:形状パラメータが一様有界な相似縮小型.
- B型:精密化と同時に形状パラメータが発散する graded 退化型.
A型では古典理論によって収束次数自体は既知であり,本質は誤差係数の退化依存性にあります.一方,B型では shape-regularity が壊れていても new geometric condition 側では still good になり得るかどうかが中心問題になります.Section 9 を family ごとに表と図で並べる意義は,まさにここにあります.
Section 9.3 では Mesh (V) を Bad にしています.(V) は shape-regularity を満たしますが,誤差係数が退化依存します.これにより,Bad に分類しています.しかしながら,収束次数自体は古典理論によって保証されます.
完成版コードは,この good/bad の違いを indicator_table_all という比較表にまとめます.したがって,読者は6つの family を一つの表で見たあとに,その背後にある mesh data を節点表・要素表・境界辺表へ戻って確認できます.
7.完成版コードの読み方
完成版コードは,次の順番で読むと分かりやすいです.
section9_lines:9.1 の (I)–(IV) を作る.build_tri_mesh_from_tensor_grid:tensor-product grid を三角形化する.tables_from_meshとshow_tables:三つの表を出す.section9_indicators:Section 9.2 の指標を計算する.section93_lines,mesh1_style_rows,build_mesh1_style_mesh:9.3 の (V),(VI) を作る.- 最後の demo 部分:family ごとの図や比較表を出す.
この構成にしておくと,記事本文では個々の関数を必要なときだけ参照すればよく,HTML の中に長大なコードを何度も埋め込まずに済みます.
8.実行のしかた
この整理版では,まず完成版コードを Colab または Jupyter へ貼り付けて実行し,その出力を見ながら本文を読むことを想定しています.最初の example では N=4 の小さい例で節点表・要素表・境界辺表を確認し,その後で6つの family の比較表へ進みます.
記事を読む順番としては,
- まず節点座標対応表・要素接点対応表・境界辺接点対応表を見る.
- 次にメッシュ図で節点番号・要素番号・境界辺番号の対応を確認する.
- 最後に
MinAngle/MaxAngle型の量で family 全体を比較する.
と進むのが一番分かりやすいです.
9.まとめ
完成版コードを1本に固定し,本文は Section 9 の意味を説明することに集中させました.その結果,
- Section 9.1 の good family,
- Section 9.2 の指標,
- Section 9.3 の bad family,
- A型 / B型と good / bad の見方,
- staggered rowの役割,
がそれぞれ整理しやすくなりました.今後は,この整理版を土台にして,FEniCSx や Gmsh 側の mesh data とどう対応づけるかを次段階として書いていく方針です.
Appendix 1.完成版コード全文(HTML 版)
以下に,コードの全文を掲載します.この版では,(I) から (VI) までの 6 つの family を 2×3 の図としてまとめて表示する plot_all_six_families も含めています.DELTA をコード冒頭で定義し,Shishkin 型 family では,delta を省略した呼び出しでも自動的に DELTA が使われるようにしてあります.
コード全文を開く/閉じる
# Colab / Jupyter 用の全コード(I)-(VI) 比較つき)
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from IPython.display import display
pd.set_option("display.max_rows", None)
# -------------------------
# Global configuration
# -------------------------
# Shishkin-type families (II) and (V) depend on delta.
# Change this value freely when you want to compare different transition layers.
DELTA = 1/128
def x_standard(N):
return np.linspace(0.0, 1.0, N + 1)
def x_shishkin_y(N, delta=None):
if delta is None:
delta = DELTA
tau = 2 * delta * abs(np.log(N))
tau = min(max(tau, 0.0), 1.0)
y = np.zeros(N + 1)
half = N // 2
for i in range(N + 1):
if i <= half:
y[i] = tau * i / half if half > 0 else 0.0
else:
y[i] = tau + (1 - tau) * (i - half) / (N - half)
return y
def x_cosine_cluster(N):
i = np.arange(N + 1, dtype=float)
return 0.5 * (1.0 - np.cos(i * np.pi / N))
def x_graded_square(N):
i = np.arange(N + 1, dtype=float)
return (i / N) ** 2
def section9_lines(kind, N, delta=None):
if delta is None:
delta = DELTA
if kind == "I":
x1 = x_standard(N)
x2 = x_standard(N)
elif kind == "II":
x1 = x_standard(N)
x2 = x_shishkin_y(N, delta=delta)
elif kind == "III":
x1 = x_cosine_cluster(N)
x2 = x_cosine_cluster(N)
elif kind == "IV":
x1 = x_standard(N)
x2 = x_graded_square(N)
else:
raise ValueError("kind must be one of 'I', 'II', 'III', 'IV'.")
return x1, x2
def build_tri_mesh_from_tensor_grid(x1, x2, diagonal="right", flip_corner_cells=False):
x1 = np.asarray(x1, dtype=float)
x2 = np.asarray(x2, dtype=float)
nx = len(x1) - 1
ny = len(x2) - 1
nodes = []
for j in range(ny + 1):
for i in range(nx + 1):
nodes.append([x1[i], x2[j]])
nodes = np.array(nodes, dtype=float)
def nid(i, j):
return j * (nx + 1) + i
elements = []
for j in range(ny):
for i in range(nx):
n00 = nid(i, j)
n10 = nid(i + 1, j)
n01 = nid(i, j + 1)
n11 = nid(i + 1, j + 1)
use_diagonal = diagonal
if flip_corner_cells:
is_top_left = (i == 0 and j == ny - 1)
is_bottom_right = (i == nx - 1 and j == 0)
if is_top_left or is_bottom_right:
use_diagonal = "left" if diagonal == "right" else "right"
if use_diagonal == "right":
elements.append([n00, n10, n11])
elements.append([n00, n11, n01])
elif use_diagonal == "left":
elements.append([n00, n10, n01])
elements.append([n10, n11, n01])
else:
raise ValueError("diagonal must be 'right' or 'left'.")
return nodes, np.array(elements, dtype=int)
def boundary_edges_from_elements(elements):
edge_count = {}
for tri in elements:
a, b, c = map(int, tri)
edges = [tuple(sorted((a, b))), tuple(sorted((b, c))), tuple(sorted((c, a)))]
for e in edges:
edge_count[e] = edge_count.get(e, 0) + 1
boundary_edges = np.array([e for e, c in edge_count.items() if c == 1], dtype=int)
idx = np.lexsort((boundary_edges[:, 1], boundary_edges[:, 0]))
return boundary_edges[idx]
def tables_from_mesh(nodes, elements, boundary_edges):
node_table = pd.DataFrame({
"node": np.arange(len(nodes), dtype=int),
"x": nodes[:, 0],
"y": nodes[:, 1],
})
element_table = pd.DataFrame({
"element": np.arange(len(elements), dtype=int),
"node0": elements[:, 0],
"node1": elements[:, 1],
"node2": elements[:, 2],
})
boundary_table = pd.DataFrame({
"boundary_edge": np.arange(len(boundary_edges), dtype=int),
"node0": boundary_edges[:, 0],
"node1": boundary_edges[:, 1],
})
return node_table, element_table, boundary_table
def show_tables(node_table, element_table, boundary_table):
print("=== node table ===")
display(node_table.style.format({"x": "{:16.12e}", "y": "{:16.12e}"}))
print("=== element table ===")
display(element_table)
print("=== boundary-edge table ===")
display(boundary_table)
def plot_mesh_with_labels(nodes, elements, boundary_edges=None,
show_node_ids=True, show_elem_ids=False,
show_boundary_ids=False,
title="mesh"):
plt.figure(figsize=(6, 6))
for tri in elements:
tri = list(map(int, tri))
poly = np.vstack([nodes[tri], nodes[tri[0]]])
plt.plot(poly[:, 0], poly[:, 1], color="0.45", lw=0.9)
if boundary_edges is not None:
for k, (a, b) in enumerate(boundary_edges):
seg = nodes[[a, b]]
plt.plot(seg[:, 0], seg[:, 1], color="black", lw=1.5)
if show_boundary_ids:
m = seg.mean(axis=0)
plt.text(m[0], m[1], str(k), fontsize=8, color="tab:purple")
if show_node_ids:
for k, (x, y) in enumerate(nodes):
plt.text(x, y, str(k), fontsize=8, color="tab:blue")
if show_elem_ids:
for k, tri in enumerate(elements):
pts = nodes[np.array(tri, dtype=int)]
c = pts.mean(axis=0)
plt.text(c[0], c[1], str(k), fontsize=8, color="tab:red")
plt.gca().set_aspect("equal")
plt.xlabel("x")
plt.ylabel("y")
plt.title(title)
plt.show()
def tri_edge_lengths(pts):
L = [
np.linalg.norm(pts[1] - pts[0]),
np.linalg.norm(pts[2] - pts[1]),
np.linalg.norm(pts[0] - pts[2]),
]
return np.sort(np.array(L, dtype=float)) # L1 <= L2 <= L3
def tri_area(pts):
a = pts[1] - pts[0]
b = pts[2] - pts[0]
return 0.5 * abs(a[0] * b[1] - a[1] * b[0])
def section9_indicators(nodes, elements):
min_like = 0.0
max_like = 0.0
for tri in elements:
pts = nodes[tri]
L1, L2, L3 = tri_edge_lengths(pts)
area = tri_area(pts)
min_like = max(min_like, (L3 ** 2) / area)
max_like = max(max_like, (L1 * L2) / area)
return float(min_like), float(max_like)
# --- bad family based on mesh1.h ---
def section93_lines(kind, N, delta=None):
if delta is None:
delta = DELTA
"""
Section 9.3 の (V), (VI) 用の y 方向節点列.
(V) : Shishkin mesh なので 9.1 の (II) と同じ縦方向節点列
(VI) : Graded mesh なので 9.1 の (IV) と同じ縦方向節点列
"""
if kind == "V":
return x_shishkin_y(N, delta=delta)
elif kind == "VI":
return x_graded_square(N)
else:
raise ValueError("kind must be 'V' or 'VI'.")
def mesh1_style_rows(N):
"""
mesh1.h 型の x 方向節点列を行ごとに作る.
偶数番目の行(0-based)は通常格子,奇数番目の行は半ステップずれた格子にする.
"""
x_base = np.linspace(0.0, 1.0, N + 1)
mids = 0.5 * (x_base[:-1] + x_base[1:])
rows = []
for j in range(N + 1):
if j % 2 == 0:
rows.append(x_base.copy())
else:
rows.append(np.concatenate(([x_base[0]], mids, [x_base[-1]])))
return rows
def build_mesh1_style_mesh(N, y_lines):
"""
mesh1.h の odd/even row と要素接続を Python に移した 2D 三角形メッシュ生成.
x 方向は staggered row,y 方向は外から与える.
"""
y_lines = np.asarray(y_lines, dtype=float)
if len(y_lines) != N + 1:
raise ValueError("y_lines must have length N+1")
rows_x = mesh1_style_rows(N)
nodes = []
row_starts = []
count = 0
for j in range(N + 1):
row_starts.append(count)
for x in rows_x[j]:
nodes.append([x, y_lines[j]])
count += 1
nodes = np.array(nodes, dtype=float)
def nid(row, k):
return row_starts[row] + k
elements = []
for j in range(N):
if j % 2 == 0:
# mesh1.h の odd strip に対応(0-based では j 偶数)
for i in range(N):
p1 = nid(j, i)
p2 = nid(j, i + 1)
p3 = nid(j + 1, i + 1)
elements.append([p1, p2, p3])
p1 = nid(j, i)
p2 = nid(j + 1, i + 1)
p3 = p2 - 1
elements.append([p1, p2, p3])
p1 = nid(j, N)
p2 = nid(j + 1, N + 1)
p3 = p2 - 1
elements.append([p1, p2, p3])
else:
# mesh1.h の even strip に対応(0-based では j 奇数)
p1 = nid(j, 0)
p2 = p1 + 1
p3 = nid(j + 1, 0)
elements.append([p1, p2, p3])
for i in range(N):
p1 = nid(j, i + 1)
p2 = p1 + 1
p3 = nid(j + 1, i + 1)
elements.append([p1, p2, p3])
p1 = nid(j, i + 1)
p2 = nid(j + 1, i + 1)
p3 = p2 - 1
elements.append([p1, p2, p3])
return nodes, np.array(elements, dtype=int)
def section93_reconstruction(kind, N, delta=None):
if delta is None:
delta = DELTA
"""
Section 9.3 の (V), (VI) を作る.
x 方向は mesh1.h 型,y 方向は (V) で Shishkin, (VI) で graded を使う.
"""
y_lines = section93_lines(kind, N, delta=delta)
return build_mesh1_style_mesh(N, y_lines)
def build_family_mesh(kind, N, delta=None):
if delta is None:
delta = DELTA
"""
kind = "I", ..., "VI" に対して nodes, elements, boundary_edges を返す.
"""
if kind in ["I", "II", "III", "IV"]:
x1, x2 = section9_lines(kind, N, delta=delta)
nodes, elements = build_tri_mesh_from_tensor_grid(
x1, x2, diagonal="right", flip_corner_cells=True
)
elif kind in ["V", "VI"]:
nodes, elements = section93_reconstruction(kind, N, delta=delta)
else:
raise ValueError("kind must be one of 'I', 'II', 'III', 'IV', 'V', 'VI'.")
boundary_edges = boundary_edges_from_elements(elements)
return nodes, elements, boundary_edges
def plot_family_on_ax(ax, kind, N=16, delta=None,
show_node_ids=False,
show_elem_ids=False,
show_boundary_ids=False):
"""
1 つの family を与えられた Axes に描く.
"""
if delta is None:
delta = DELTA
nodes, elements, boundary_edges = build_family_mesh(kind, N, delta=delta)
for tri in elements:
tri = list(map(int, tri))
poly = np.vstack([nodes[tri], nodes[tri[0]]])
ax.plot(poly[:, 0], poly[:, 1], color="0.45", lw=0.9)
for k, (a, b) in enumerate(boundary_edges):
seg = nodes[[a, b]]
ax.plot(seg[:, 0], seg[:, 1], color="black", lw=1.4)
if show_boundary_ids:
m = seg.mean(axis=0)
ax.text(m[0], m[1], str(k), fontsize=7, color="tab:purple")
if show_node_ids:
for k, (x, y) in enumerate(nodes):
ax.text(x, y, str(k), fontsize=7, color="tab:blue")
if show_elem_ids:
for k, tri in enumerate(elements):
pts = nodes[np.array(tri, dtype=int)]
c = pts.mean(axis=0)
ax.text(c[0], c[1], str(k), fontsize=7, color="tab:red")
name_map = {
"I": "I : Standard",
"II": "II : Shishkin",
"III": "III : Anisotropic",
"IV": "IV : Graded",
"V": "V : Shishkin (bad family)",
"VI": "VI : Graded (bad family)",
}
ax.set_aspect("equal")
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_title(f"{name_map[kind]} (N={N})")
def plot_all_six_families(N=16, delta=None,
show_node_ids=False,
show_elem_ids=False,
show_boundary_ids=False):
"""
I から VI までの 6 つの family を 2x3 に並べて描く.
"""
if delta is None:
delta = DELTA
fig, axes = plt.subplots(2, 3, figsize=(15, 9))
for ax, kind in zip(axes.ravel(), ["I", "II", "III", "IV", "V", "VI"]):
plot_family_on_ax(
ax, kind, N=N, delta=delta,
show_node_ids=show_node_ids,
show_elem_ids=show_elem_ids,
show_boundary_ids=show_boundary_ids,
)
plt.tight_layout()
plt.show()
# --- examples ---
# You can freely change DELTA above.
# Families (II) and (V) use this value automatically when delta is omitted.
kind = "IV"
N = 4
x1, x2 = section9_lines(kind, N)
nodes, elements = build_tri_mesh_from_tensor_grid(x1, x2, diagonal="right", flip_corner_cells=True)
boundary_edges = boundary_edges_from_elements(elements)
node_table, element_table, boundary_table = tables_from_mesh(nodes, elements, boundary_edges)
show_tables(node_table, element_table, boundary_table)
plot_mesh_with_labels(nodes, elements, boundary_edges, show_node_ids=True, show_elem_ids=True,
show_boundary_ids=False, title=f"family {kind}: nodes + elements")
plot_mesh_with_labels(nodes, elements, boundary_edges, show_node_ids=False, show_elem_ids=False,
show_boundary_ids=True, title=f"family {kind}: boundary-edge ids")
# full comparison (I)-(VI)
rows_all = []
for kind in ["I", "II", "III", "IV"]:
for N in [16, 32, 64, 128]:
x1, x2 = section9_lines(kind, N)
nodes_g, elements_g = build_tri_mesh_from_tensor_grid(x1, x2, diagonal="right", flip_corner_cells=True)
min_like, max_like = section9_indicators(nodes_g, elements_g)
rows_all.append({"Mesh": kind, "Type": "good family", "N": N,
"MinAngle": min_like, "MaxAngle": max_like})
for kind in ["V", "VI"]:
for N in [16, 32, 64, 128]:
nodes_b, elements_b = section93_reconstruction(kind, N)
min_like, max_like = section9_indicators(nodes_b, elements_b)
rows_all.append({"Mesh": kind, "Type": "bad-family reconstruction", "N": N,
"MinAngle": min_like, "MaxAngle": max_like})
indicator_table_all = pd.DataFrame(rows_all)
display(indicator_table_all.style.format({"MinAngle": "{:16.12e}", "MaxAngle": "{:16.12e}"}))
# -------------------------
# Demo: plot bad family reconstruction (V), (VI)
# -------------------------
fig, axes = plt.subplots(1, 2, figsize=(10, 4.5))
for ax, kind in zip(axes, ["V", "VI"]):
nodes_b, elements_b = section93_reconstruction(kind, 16)
boundary_b = boundary_edges_from_elements(elements_b)
for tri in elements_b:
tri = list(map(int, tri))
poly = np.vstack([nodes_b[tri], nodes_b[tri[0]]])
ax.plot(poly[:, 0], poly[:, 1], color="0.4", lw=0.8)
for e in boundary_b:
pts = nodes_b[list(e)]
ax.plot(pts[:, 0], pts[:, 1], color="tab:red", lw=1.2)
ax.set_aspect("equal")
ax.set_title(f"family {kind}")
ax.set_xlabel("x")
ax.set_ylabel("y")
plt.tight_layout()
plt.show()
# -------------------------
# Demo: plot all six families
# -------------------------
plot_all_six_families(N=16)
Appendix 2.完成版コード全文(HTML 版)
この版では,N,DELTA,EPSILON を global configuration としてまとめ,それ以外の箇所では数値を直書きしないように整理しています.CASE = make_case(...) を1回与えれば,1 family の結果,6 family の比較表,6 family の図を同じ設定で再利用できます.
以下に,現時点の完成版コード全文を HTML 表示用に埋め込みます.この版では,DELTA をコード冒頭で定義し,Shishkin 型 family では,delta を省略した呼び出しでも自動的に DELTA が使われるようにしてあります.
コード全文を開く/閉じる
# Colab / Jupyter 用の全コード(I)-(VI) 比較つき)
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from IPython.display import display
pd.set_option("display.max_rows", None)
# -------------------------
# Global configuration
# -------------------------
# 既定の family
KIND_DEFAULT = "IV"
# 既定の分割数
N_DEFAULT = 16
# Shishkin-type families (II) and (V) depend on DELTA.
DELTA = 1 / 128
# Graded mesh families (IV) and (VI) use the power EPSILON.
EPSILON = 2.0
def resolve_parameters(N=None, delta=None, epsilon=None):
"""
N, DELTA, EPSILON の既定値を解決して返す.
"""
if N is None:
N = N_DEFAULT
if delta is None:
delta = DELTA
if epsilon is None:
epsilon = EPSILON
return int(N), float(delta), float(epsilon)
def make_case(N=None, delta=None, epsilon=None):
"""
ひと組の設定をまとめて返す.
例:
case = make_case(N=32, delta=1/64, epsilon=3.0)
"""
N, delta, epsilon = resolve_parameters(N=N, delta=delta, epsilon=epsilon)
return {
"N": N,
"DELTA": delta,
"EPSILON": epsilon,
}
def x_standard(N=None):
N, _, _ = resolve_parameters(N=N)
return np.linspace(0.0, 1.0, N + 1)
def x_shishkin_y(N=None, delta=None):
"""
Shishkin 型の y 方向節点列を返す.
delta を省略すると global DELTA を使う.
"""
N, delta, _ = resolve_parameters(N=N, delta=delta)
tau = 2 * delta * abs(np.log(N))
tau = min(max(tau, 0.0), 1.0)
y = np.zeros(N + 1)
half = N // 2
for i in range(N + 1):
if i <= half:
y[i] = tau * i / half if half > 0 else 0.0
else:
y[i] = tau + (1 - tau) * (i - half) / (N - half)
return y
def x_cosine_cluster(N=None):
N, _, _ = resolve_parameters(N=N)
i = np.arange(N + 1, dtype=float)
return 0.5 * (1.0 - np.cos(i * np.pi / N))
def x_graded_power(N=None, epsilon=None):
"""
graded mesh の 1 次元節点列を返す.
epsilon を省略すると global EPSILON を使う.
"""
N, _, epsilon = resolve_parameters(N=N, epsilon=epsilon)
i = np.arange(N + 1, dtype=float)
return (i / N) ** epsilon
def section9_lines(kind, N=None, delta=None, epsilon=None):
"""
Section 9.1 の (I)-(IV) 用の節点列を返す.
"""
N, delta, epsilon = resolve_parameters(N=N, delta=delta, epsilon=epsilon)
if kind == "I":
x1 = x_standard(N)
x2 = x_standard(N)
elif kind == "II":
x1 = x_standard(N)
x2 = x_shishkin_y(N, delta=delta)
elif kind == "III":
x1 = x_cosine_cluster(N)
x2 = x_cosine_cluster(N)
elif kind == "IV":
x1 = x_standard(N)
x2 = x_graded_power(N, epsilon=epsilon)
else:
raise ValueError("kind must be one of 'I', 'II', 'III', 'IV'.")
return x1, x2
def build_tri_mesh_from_tensor_grid(x1, x2, diagonal="right", flip_corner_cells=False):
x1 = np.asarray(x1, dtype=float)
x2 = np.asarray(x2, dtype=float)
nx = len(x1) - 1
ny = len(x2) - 1
nodes = []
for j in range(ny + 1):
for i in range(nx + 1):
nodes.append([x1[i], x2[j]])
nodes = np.array(nodes, dtype=float)
def nid(i, j):
return j * (nx + 1) + i
elements = []
for j in range(ny):
for i in range(nx):
n00 = nid(i, j)
n10 = nid(i + 1, j)
n01 = nid(i, j + 1)
n11 = nid(i + 1, j + 1)
use_diagonal = diagonal
if flip_corner_cells:
is_top_left = (i == 0 and j == ny - 1)
is_bottom_right = (i == nx - 1 and j == 0)
if is_top_left or is_bottom_right:
use_diagonal = "left" if diagonal == "right" else "right"
if use_diagonal == "right":
elements.append([n00, n10, n11])
elements.append([n00, n11, n01])
elif use_diagonal == "left":
elements.append([n00, n10, n01])
elements.append([n10, n11, n01])
else:
raise ValueError("diagonal must be 'right' or 'left'.")
return nodes, np.array(elements, dtype=int)
def boundary_edges_from_elements(elements):
edge_count = {}
for tri in elements:
a, b, c = map(int, tri)
edges = [tuple(sorted((a, b))), tuple(sorted((b, c))), tuple(sorted((c, a)))]
for e in edges:
edge_count[e] = edge_count.get(e, 0) + 1
boundary_edges = np.array([e for e, c in edge_count.items() if c == 1], dtype=int)
idx = np.lexsort((boundary_edges[:, 1], boundary_edges[:, 0]))
return boundary_edges[idx]
def tables_from_mesh(nodes, elements, boundary_edges):
node_table = pd.DataFrame({
"node": np.arange(len(nodes), dtype=int),
"x": nodes[:, 0],
"y": nodes[:, 1],
})
element_table = pd.DataFrame({
"element": np.arange(len(elements), dtype=int),
"node0": elements[:, 0],
"node1": elements[:, 1],
"node2": elements[:, 2],
})
boundary_table = pd.DataFrame({
"boundary_edge": np.arange(len(boundary_edges), dtype=int),
"node0": boundary_edges[:, 0],
"node1": boundary_edges[:, 1],
})
return node_table, element_table, boundary_table
def show_tables(node_table, element_table, boundary_table):
print("=== node table ===")
display(node_table.style.format({"x": "{:16.12e}", "y": "{:16.12e}"}))
print("=== element table ===")
display(element_table)
print("=== boundary-edge table ===")
display(boundary_table)
def plot_mesh_with_labels(nodes, elements, boundary_edges=None,
show_node_ids=True, show_elem_ids=False,
show_boundary_ids=False,
title="mesh"):
plt.figure(figsize=(6, 6))
for tri in elements:
tri = list(map(int, tri))
poly = np.vstack([nodes[tri], nodes[tri[0]]])
plt.plot(poly[:, 0], poly[:, 1], color="0.45", lw=0.9)
if boundary_edges is not None:
for k, (a, b) in enumerate(boundary_edges):
seg = nodes[[a, b]]
plt.plot(seg[:, 0], seg[:, 1], color="black", lw=1.5)
if show_boundary_ids:
m = seg.mean(axis=0)
plt.text(m[0], m[1], str(k), fontsize=8, color="tab:purple")
if show_node_ids:
for k, (x, y) in enumerate(nodes):
plt.text(x, y, str(k), fontsize=8, color="tab:blue")
if show_elem_ids:
for k, tri in enumerate(elements):
pts = nodes[np.array(tri, dtype=int)]
c = pts.mean(axis=0)
plt.text(c[0], c[1], str(k), fontsize=8, color="tab:red")
plt.gca().set_aspect("equal")
plt.xlabel("x")
plt.ylabel("y")
plt.title(title)
plt.show()
def tri_edge_lengths(pts):
L = [
np.linalg.norm(pts[1] - pts[0]),
np.linalg.norm(pts[2] - pts[1]),
np.linalg.norm(pts[0] - pts[2]),
]
return np.sort(np.array(L, dtype=float)) # L1 <= L2 <= L3
def tri_area(pts):
a = pts[1] - pts[0]
b = pts[2] - pts[0]
return 0.5 * abs(a[0] * b[1] - a[1] * b[0])
def section9_indicators(nodes, elements):
min_like = 0.0
max_like = 0.0
for tri in elements:
pts = nodes[tri]
L1, L2, L3 = tri_edge_lengths(pts)
area = tri_area(pts)
min_like = max(min_like, (L3 ** 2) / area)
max_like = max(max_like, (L1 * L2) / area)
return float(min_like), float(max_like)
# --- bad family based on mesh1.h ---
def section93_lines(kind, N=None, delta=None, epsilon=None):
"""
Section 9.3 の (V), (VI) 用の y 方向節点列を返す.
(V) : Shishkin mesh なので 9.1 の (II) と同じ縦方向節点列
(VI) : Graded mesh なので 9.1 の (IV) と同じ縦方向節点列
"""
N, delta, epsilon = resolve_parameters(N=N, delta=delta, epsilon=epsilon)
if kind == "V":
return x_shishkin_y(N, delta=delta)
elif kind == "VI":
return x_graded_power(N, epsilon=epsilon)
else:
raise ValueError("kind must be 'V' or 'VI'.")
def mesh1_style_rows(N=None):
"""
mesh1.h 型の x 方向節点列を行ごとに作る.
偶数番目の行(0-based)は通常格子,奇数番目の行は半ステップずれた格子にする.
"""
N, _, _ = resolve_parameters(N=N)
x_base = np.linspace(0.0, 1.0, N + 1)
mids = 0.5 * (x_base[:-1] + x_base[1:])
rows = []
for j in range(N + 1):
if j % 2 == 0:
rows.append(x_base.copy())
else:
rows.append(np.concatenate(([x_base[0]], mids, [x_base[-1]])))
return rows
def build_mesh1_style_mesh(N=None, y_lines=None):
"""
mesh1.h の odd/even row と要素接続を Python に移した 2D 三角形メッシュ生成.
x 方向は staggered row,y 方向は外から与える.
"""
N, _, _ = resolve_parameters(N=N)
y_lines = np.asarray(y_lines, dtype=float)
if len(y_lines) != N + 1:
raise ValueError("y_lines must have length N+1")
rows_x = mesh1_style_rows(N)
nodes = []
row_starts = []
count = 0
for j in range(N + 1):
row_starts.append(count)
for x in rows_x[j]:
nodes.append([x, y_lines[j]])
count += 1
nodes = np.array(nodes, dtype=float)
def nid(row, k):
return row_starts[row] + k
elements = []
for j in range(N):
if j % 2 == 0:
# mesh1.h の odd strip に対応(0-based では j 偶数)
for i in range(N):
p1 = nid(j, i)
p2 = nid(j, i + 1)
p3 = nid(j + 1, i + 1)
elements.append([p1, p2, p3])
p1 = nid(j, i)
p2 = nid(j + 1, i + 1)
p3 = p2 - 1
elements.append([p1, p2, p3])
p1 = nid(j, N)
p2 = nid(j + 1, N + 1)
p3 = p2 - 1
elements.append([p1, p2, p3])
else:
# mesh1.h の even strip に対応(0-based では j 奇数)
p1 = nid(j, 0)
p2 = p1 + 1
p3 = nid(j + 1, 0)
elements.append([p1, p2, p3])
for i in range(N):
p1 = nid(j, i + 1)
p2 = p1 + 1
p3 = nid(j + 1, i + 1)
elements.append([p1, p2, p3])
p1 = nid(j, i + 1)
p2 = nid(j + 1, i + 1)
p3 = p2 - 1
elements.append([p1, p2, p3])
return nodes, np.array(elements, dtype=int)
def section93_reconstruction(kind, N=None, delta=None, epsilon=None):
"""
Section 9.3 の (V), (VI) を作る.
x 方向は mesh1.h 型,y 方向は (V) で Shishkin, (VI) で graded を使う.
"""
N, delta, epsilon = resolve_parameters(N=N, delta=delta, epsilon=epsilon)
y_lines = section93_lines(kind, N=N, delta=delta, epsilon=epsilon)
return build_mesh1_style_mesh(N=N, y_lines=y_lines)
def build_family_mesh(kind, N=None, delta=None, epsilon=None):
"""
kind = "I", ..., "VI" に対して nodes, elements, boundary_edges を返す.
"""
N, delta, epsilon = resolve_parameters(N=N, delta=delta, epsilon=epsilon)
if kind in ["I", "II", "III", "IV"]:
x1, x2 = section9_lines(kind, N=N, delta=delta, epsilon=epsilon)
nodes, elements = build_tri_mesh_from_tensor_grid(
x1, x2, diagonal="right", flip_corner_cells=True
)
elif kind in ["V", "VI"]:
nodes, elements = section93_reconstruction(kind, N=N, delta=delta, epsilon=epsilon)
else:
raise ValueError("kind must be one of 'I', 'II', 'III', 'IV', 'V', 'VI'.")
boundary_edges = boundary_edges_from_elements(elements)
return nodes, elements, boundary_edges
def plot_family_on_ax(ax, kind, N=None, delta=None, epsilon=None,
show_node_ids=False,
show_elem_ids=False,
show_boundary_ids=False):
"""
1 つの family を与えられた Axes に描く.
"""
N, delta, epsilon = resolve_parameters(N=N, delta=delta, epsilon=epsilon)
nodes, elements, boundary_edges = build_family_mesh(kind, N=N, delta=delta, epsilon=epsilon)
for tri in elements:
tri = list(map(int, tri))
poly = np.vstack([nodes[tri], nodes[tri[0]]])
ax.plot(poly[:, 0], poly[:, 1], color="0.45", lw=0.9)
for k, (a, b) in enumerate(boundary_edges):
seg = nodes[[a, b]]
ax.plot(seg[:, 0], seg[:, 1], color="black", lw=1.4)
if show_boundary_ids:
m = seg.mean(axis=0)
ax.text(m[0], m[1], str(k), fontsize=7, color="tab:purple")
if show_node_ids:
for k, (x, y) in enumerate(nodes):
ax.text(x, y, str(k), fontsize=7, color="tab:blue")
if show_elem_ids:
for k, tri in enumerate(elements):
pts = nodes[np.array(tri, dtype=int)]
c = pts.mean(axis=0)
ax.text(c[0], c[1], str(k), fontsize=7, color="tab:red")
name_map = {
"I": "I : Standard",
"II": "II : Shishkin",
"III": "III : Anisotropic",
"IV": "IV : Graded",
"V": "V : Shishkin (bad family)",
"VI": "VI : Graded (bad family)",
}
ax.set_aspect("equal")
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_title(f"{name_map[kind]} (N={N})")
def plot_all_six_families(case=None,
show_node_ids=False,
show_elem_ids=False,
show_boundary_ids=False):
"""
I から VI までの 6 つの family を 2x3 に並べて描く.
"""
if case is None:
case = make_case()
N = case["N"]
delta = case["DELTA"]
epsilon = case["EPSILON"]
fig, axes = plt.subplots(2, 3, figsize=(15, 9))
for ax, kind in zip(axes.ravel(), ["I", "II", "III", "IV", "V", "VI"]):
plot_family_on_ax(
ax, kind, N=N, delta=delta, epsilon=epsilon,
show_node_ids=show_node_ids,
show_elem_ids=show_elem_ids,
show_boundary_ids=show_boundary_ids,
)
plt.tight_layout()
plt.show()
def compare_all_families(case=None):
"""
1 組の (N, DELTA, EPSILON) に対して,(I)-(VI) 全 family の指標表を返す.
返り値は,各 family につき 1 行ずつで,MinAngle, MaxAngle も 1 つずつになる.
"""
if case is None:
case = make_case()
N = case["N"]
delta = case["DELTA"]
epsilon = case["EPSILON"]
rows_all = []
for kind in ["I", "II", "III", "IV", "V", "VI"]:
nodes, elements, _ = build_family_mesh(kind, N=N, delta=delta, epsilon=epsilon)
min_like, max_like = section9_indicators(nodes, elements)
family_type = "good family" if kind in ["I", "II", "III", "IV"] else "bad-family reconstruction"
rows_all.append({
"Mesh": kind,
"Type": family_type,
"N": N,
"DELTA": delta,
"EPSILON": epsilon,
"MinAngle": min_like,
"MaxAngle": max_like,
})
return pd.DataFrame(rows_all)
def display_indicator_table(indicator_table):
display(
indicator_table.style.format({
"MinAngle": "{:16.12e}",
"MaxAngle": "{:16.12e}",
})
)
def run_one_case(kind=None, case=None,
show_tables_flag=True,
show_node_ids=True,
show_elem_ids=True,
show_boundary_ids=False):
"""
N, DELTA, EPSILON を 1 組与えて,1 つの結果を作る.
"""
if kind is None:
kind = KIND_DEFAULT
if case is None:
case = make_case()
N = case["N"]
delta = case["DELTA"]
epsilon = case["EPSILON"]
nodes, elements, boundary_edges = build_family_mesh(
kind, N=N, delta=delta, epsilon=epsilon
)
node_table, element_table, boundary_table = tables_from_mesh(
nodes, elements, boundary_edges
)
min_like, max_like = section9_indicators(nodes, elements)
if show_tables_flag:
show_tables(node_table, element_table, boundary_table)
plot_mesh_with_labels(
nodes, elements, boundary_edges,
show_node_ids=show_node_ids,
show_elem_ids=show_elem_ids,
show_boundary_ids=show_boundary_ids,
title=f"family {kind} (N={N}, DELTA={delta:g}, EPSILON={epsilon:g})"
)
return {
"case": case,
"kind": kind,
"nodes": nodes,
"elements": elements,
"boundary_edges": boundary_edges,
"node_table": node_table,
"element_table": element_table,
"boundary_table": boundary_table,
"MinAngle": min_like,
"MaxAngle": max_like,
}
# -------------------------
# Minimal workflow
# -------------------------
# 1. ここだけ変えれば,その設定に応じて結果が全部変わる.
CASE = make_case(N=N_DEFAULT, delta=DELTA, epsilon=EPSILON)
# 2. 1 つの family の結果を見るときの例
result = run_one_case(kind=KIND_DEFAULT, case=CASE)
# 3. 6 family の指標表を出すときの例
indicator_table = compare_all_families(CASE)
display_indicator_table(indicator_table)
# 4. 6 family を図として並べるときの例
plot_all_six_families(CASE)
