FreeFEM用メッシュファイルを取り出す
0. ここで紹介するコードがやること
このスクリプトの役割は,6つの mesh family を FreeFEM 用の .msh と CSV に整理して出力することです.流れは次の4つです.
- Section 9 の6つの mesh family のいずれかを生成する.
- 節点座標・要素接続・境界辺を取り出す.
- 正方形の4辺に応じて,底辺=1,右辺=2,上辺=3,左辺=4 の境界ラベルを付ける.
- FreeFEM の
.mshと,必要ならindicator_table_*.csv,manifest_*.csvを出力する.
1.冒頭の global configuration
このコードでは,設定値を冒頭にまとめています.一つの case は (N, DELTA, EPSILON) の組です.
EXPORT_ALL = True
KIND_DEFAULT = "IV"
N_DEFAULT = 16
DELTA = 1 / 128
EPSILON = 2.0
OUTDIR = "freefem_msh_data"
WRITE_INDICATOR_CSV = True
WRITE_MANIFEST_CSV = True
VERTEX_LABEL = 0
TRIANGLE_REGION_LABEL = 0
BOTTOM_LABEL = 1
RIGHT_LABEL = 2
TOP_LABEL = 3
LEFT_LABEL = 4
ここでの意味は次のとおりです.
N_DEFAULT:分割数です.DELTA:Shishkin 型 family に使うパラメータです.EPSILON:graded mesh の指数です.EXPORT_ALL:Trueなら I から VI まで全部出します.KIND_DEFAULT:EXPORT_ALL = Falseのときに出す family です.OUTDIR:出力先のディレクトリです.VERTEX_LABELとTRIANGLE_REGION_LABEL:頂点ラベルと三角形の領域ラベルです.BOTTOM_LABEL,RIGHT_LABEL,TOP_LABEL,LEFT_LABEL:底辺・右辺・上辺・左辺に対応する境界ラベルです.
2.6つの family をどう作っているか
good family 側の I–IV では,section9_lines(...) と build_tri_mesh_from_tensor_grid(...) により座標列から三角形分割を作ります.
bad family 側の V,VI では,x 方向に mesh1.h 型の staggered row を用い,y 方向には Shishkin または graded の節点列を使います.
これをまとめるのが build_family_mesh(...) です.
def build_family_mesh(kind, N=None, delta=None, epsilon=None):
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'.")
return nodes, elements
この段階では,まだ FreeFEM 用ファイルは作っていません.返ってくるのは,NumPy 配列としての nodes と elements です.
3.境界辺を取り出して4辺に分ける
FreeFEM 側で境界条件を辺ごとに分けられるようにするためには,境界辺の情報が必要です.
このコードではまず boundary_edges_from_elements(...) により外周の辺だけを抜き出し,次に
label_boundary_edges_square(...) で中点を見て,
底辺=1,右辺=2,上辺=3,左辺=4
のラベルを付けます.
def label_boundary_edges_square(nodes, boundary_edges, tol=1.0e-12):
nodes = np.asarray(nodes, dtype=float)
boundary_edges = np.asarray(boundary_edges, dtype=np.int64)
labels = np.zeros(len(boundary_edges), dtype=np.int64)
for k, edge in enumerate(boundary_edges):
i, j = map(int, edge)
mid = 0.5 * (nodes[i] + nodes[j])
x, y = float(mid[0]), float(mid[1])
if abs(y - 0.0) <= tol:
labels[k] = BOTTOM_LABEL
elif abs(x - 1.0) <= tol:
labels[k] = RIGHT_LABEL
elif abs(y - 1.0) <= tol:
labels[k] = TOP_LABEL
elif abs(x - 0.0) <= tol:
labels[k] = LEFT_LABEL
else:
raise ValueError(
f"Boundary edge midpoint {(x, y)} does not lie on the expected unit-square boundary."
)
return labels
つまり,この段階で FreeFEM に渡したい境界ラベル情報がそろいます.
4.FreeFEM の .msh に書き出す
FreeFEM 用ファイルの出力は write_freefem_msh(...) が担当します.
この関数は,
- 先頭行に
nv nt ne - 続いて頂点行
x y label - 続いて三角形行
i j k region - 最後に境界辺行
i j boundary_label
という形で .msh を書き出します.頂点番号・要素番号は FreeFEM 用に 1 始まりへ直しています.
def write_freefem_msh(path, nodes, elements, boundary_edges, boundary_edge_labels,
vertex_label=VERTEX_LABEL,
triangle_region_label=TRIANGLE_REGION_LABEL):
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
nodes = np.asarray(nodes, dtype=float)
elements = np.asarray(elements, dtype=np.int64)
boundary_edges = np.asarray(boundary_edges, dtype=np.int64)
with open(path, "w", encoding="utf-8") as f:
print(len(nodes), len(elements), len(boundary_edges), file=f)
for x, y in nodes:
print(f"{x:.16e} {y:.16e} {int(vertex_label)}", file=f)
for tri in elements:
i, j, k = (np.asarray(tri, dtype=np.int64) + 1).tolist()
print(f"{i} {j} {k} {int(triangle_region_label)}", file=f)
for edge, label in zip(boundary_edges, boundary_edge_labels):
i, j = (np.asarray(edge, dtype=np.int64) + 1).tolist()
print(f"{i} {j} {int(label)}", file=f)
return path
この形にしておけば,次の記事では readmesh(...) でそのまま読み込めます.
5.CSV は何のために出すのか
主役は .msh ですが,確認用として CSV も出しています.役割は二つです.
indicator_table_*.csv:その case に対する 6 family のMinAngleとMaxAngleを保存する.manifest_*.csv:どの family がどの.mshに対応するかを一覧にする.
つまり,.msh は FreeFEM 用,CSV は人間が確認するための補助資料です.
6.実行のしかた
最も簡単には,このファイルをそのまま実行します.
python ファイル名.py
既定では EXPORT_ALL = True なので,I–VI の6つ全部が出ます.1つだけ出したいときは,たとえば
EXPORT_ALL = False
KIND_DEFAULT = "IV"
のように変えれば,IV だけ出力できます.
7.出力されるファイル
たとえば既定値で動かした場合,出力先には概ね次のようなファイルが並びます.
freefem_msh_data/
family_I_N16_delta0.0078125_eps2.msh
family_II_N16_delta0.0078125_eps2.msh
family_III_N16_delta0.0078125_eps2.msh
family_IV_N16_delta0.0078125_eps2.msh
family_V_N16_delta0.0078125_eps2.msh
family_VI_N16_delta0.0078125_eps2.msh
indicator_table_N16_delta0.0078125_eps2.csv
manifest_N16_delta0.0078125_eps2.csv
この命名にしておくと,あとでどの case の結果かを見失いにくくなります.なお,境界ラベルは,底辺=1,右辺=2,上辺=3,左辺=4 です.
次の記事では,ここで出力した .msh を FreeFEM で読み込み,Poisson 方程式を解きます.
Appendix.完成版コード全文(HTML 版)
Appendix には,実際に使った完成版コード全文を載せます.
このコードは,6つの mesh family を FreeFEM 用の .msh と補助的な CSV に変換して出力するためのもので,Poisson solve は含みません.
(個人メモ: mesh_section9_families_export_freefem_msh_clean_v2.py)
役割をメッシュ出力に限定することで,次の記事では readmesh(...) から Poisson 問題に集中できます.
コード全文を開く/閉じる
"""
Export-only program for the six mesh families from Section 9
to FreeFEM .msh files.
This script
1. generates one of the six mesh families (I)-(VI),
2. writes a 2D FreeFEM .msh file with the structure
nv nt ne
x y vertex_label
i j k region_label
i j boundary_label
3. optionally writes one indicator CSV for the chosen (N, DELTA, EPSILON),
4. optionally writes one manifest CSV.
Poisson solves are intentionally NOT included here.
They are meant for a separate FreeFEM script / blog post.
"""
from __future__ import annotations
from pathlib import Path
import numpy as np
import pandas as pd
# -------------------------
# Global configuration
# -------------------------
EXPORT_ALL = True
KIND_DEFAULT = "IV" # used when EXPORT_ALL = False
# One case = one choice of (N, DELTA, EPSILON)
N_DEFAULT = 16
DELTA = 1 / 128
EPSILON = 2.0
# Output settings
OUTDIR = "freefem_msh_data"
WRITE_INDICATOR_CSV = True
WRITE_MANIFEST_CSV = True
# FreeFEM labels
VERTEX_LABEL = 0
TRIANGLE_REGION_LABEL = 0
# Boundary labels on the unit square:
# bottom = 1, right = 2, top = 3, left = 4
BOTTOM_LABEL = 1
RIGHT_LABEL = 2
TOP_LABEL = 3
LEFT_LABEL = 4
# -------------------------
# Utility: parameter handling
# -------------------------
def resolve_parameters(N=None, delta=None, epsilon=None):
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):
N, delta, epsilon = resolve_parameters(N=N, delta=delta, epsilon=epsilon)
return {"N": N, "DELTA": delta, "EPSILON": epsilon}
def case_to_stem(case):
return (
f"N{case['N']}_"
f"delta{case['DELTA']:.6g}_"
f"eps{case['EPSILON']:.6g}"
).replace("/", "_")
# -------------------------
# Section 9.1: good families (I)-(IV)
# -------------------------
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):
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):
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):
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=np.int64)
# -------------------------
# Indicators
# -------------------------
def tri_edge_lengths(pts):
lengths = [
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(lengths, dtype=float))
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[np.asarray(tri, dtype=int)]
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)
# -------------------------
# Section 9.3: bad families (V)-(VI)
# -------------------------
def section93_lines(kind, N=None, delta=None, epsilon=None):
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):
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):
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:
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:
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=np.int64)
def section93_reconstruction(kind, N=None, delta=None, epsilon=None):
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)
# -------------------------
# Unified family interface
# -------------------------
def build_family_mesh(kind, N=None, delta=None, epsilon=None):
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'.")
return nodes, elements
# -------------------------
# Boundary edges
# -------------------------
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=np.int64)
if len(boundary_edges) == 0:
return boundary_edges
idx = np.lexsort((boundary_edges[:, 1], boundary_edges[:, 0]))
return boundary_edges[idx]
def label_boundary_edges_square(nodes, boundary_edges, tol=1.0e-12):
"""
Label boundary edges of the unit square by their midpoint.
bottom: 1
right : 2
top : 3
left : 4
"""
nodes = np.asarray(nodes, dtype=float)
boundary_edges = np.asarray(boundary_edges, dtype=np.int64)
labels = np.zeros(len(boundary_edges), dtype=np.int64)
for k, edge in enumerate(boundary_edges):
i, j = map(int, edge)
mid = 0.5 * (nodes[i] + nodes[j])
x, y = float(mid[0]), float(mid[1])
if abs(y - 0.0) <= tol:
labels[k] = BOTTOM_LABEL
elif abs(x - 1.0) <= tol:
labels[k] = RIGHT_LABEL
elif abs(y - 1.0) <= tol:
labels[k] = TOP_LABEL
elif abs(x - 0.0) <= tol:
labels[k] = LEFT_LABEL
else:
raise ValueError(
f"Boundary edge midpoint {(x, y)} does not lie on the expected unit-square boundary."
)
return labels
# -------------------------
# FreeFEM .msh export
# -------------------------
def write_freefem_msh(path, nodes, elements, boundary_edges, boundary_edge_labels,
vertex_label=VERTEX_LABEL,
triangle_region_label=TRIANGLE_REGION_LABEL):
"""
Write a 2D FreeFEM .msh file.
Structure:
nv nt ne
x y vertex_label
i j k region_label
i j boundary_label
FreeFEM uses 1-based indexing in the file.
"""
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
nodes = np.asarray(nodes, dtype=float)
elements = np.asarray(elements, dtype=np.int64)
boundary_edges = np.asarray(boundary_edges, dtype=np.int64)
with open(path, "w", encoding="utf-8") as f:
print(len(nodes), len(elements), len(boundary_edges), file=f)
for x, y in nodes:
print(f"{x:.16e} {y:.16e} {int(vertex_label)}", file=f)
for tri in elements:
i, j, k = (np.asarray(tri, dtype=np.int64) + 1).tolist()
print(f"{i} {j} {k} {int(triangle_region_label)}", file=f)
boundary_edge_labels = np.asarray(boundary_edge_labels, dtype=np.int64)
if len(boundary_edge_labels) != len(boundary_edges):
raise ValueError("boundary_edge_labels must have the same length as boundary_edges.")
for edge, label in zip(boundary_edges, boundary_edge_labels):
i, j = (np.asarray(edge, dtype=np.int64) + 1).tolist()
print(f"{i} {j} {int(label)}", file=f)
return path
def preview_file_head(path, nlines=12):
path = Path(path)
with open(path, "r", encoding="utf-8") as f:
lines = []
for _ in range(nlines):
line = f.readline()
if not line:
break
lines.append(line.rstrip("\n"))
return lines
# -------------------------
# Export routines
# -------------------------
def export_family_msh(kind, case=None, outdir=None):
if case is None:
case = make_case()
if outdir is None:
outdir = OUTDIR
outdir = Path(outdir)
outdir.mkdir(parents=True, exist_ok=True)
nodes, elements = build_family_mesh(
kind,
N=case["N"],
delta=case["DELTA"],
epsilon=case["EPSILON"],
)
boundary_edges = boundary_edges_from_elements(elements)
boundary_edge_labels = label_boundary_edges_square(nodes, boundary_edges)
msh_path = outdir / f"family_{kind}_{case_to_stem(case)}.msh"
write_freefem_msh(msh_path, nodes, elements, boundary_edges, boundary_edge_labels)
return msh_path
def export_selected_or_all(case=None, outdir=None):
if case is None:
case = make_case()
if outdir is None:
outdir = OUTDIR
if EXPORT_ALL:
kinds = ["I", "II", "III", "IV", "V", "VI"]
else:
kinds = [KIND_DEFAULT]
paths = {}
for kind in kinds:
paths[kind] = export_family_msh(kind, case=case, outdir=outdir)
return paths
# -------------------------
# Optional metadata CSVs
# -------------------------
def build_indicator_table(case=None):
if case is None:
case = make_case()
rows = []
for kind in ["I", "II", "III", "IV", "V", "VI"]:
nodes, elements = build_family_mesh(
kind,
N=case["N"],
delta=case["DELTA"],
epsilon=case["EPSILON"],
)
min_like, max_like = section9_indicators(nodes, elements)
rows.append(
{
"Mesh": kind,
"N": case["N"],
"DELTA": case["DELTA"],
"EPSILON": case["EPSILON"],
"MinAngle": min_like,
"MaxAngle": max_like,
}
)
return pd.DataFrame(rows)
def write_indicator_csv(case=None, outdir=None):
if case is None:
case = make_case()
if outdir is None:
outdir = OUTDIR
outdir = Path(outdir)
outdir.mkdir(parents=True, exist_ok=True)
df = build_indicator_table(case)
csv_path = outdir / f"indicator_table_{case_to_stem(case)}.csv"
df.to_csv(csv_path, index=False)
return csv_path
def write_manifest_csv(exported_paths, case=None, outdir=None):
if case is None:
case = make_case()
if outdir is None:
outdir = OUTDIR
outdir = Path(outdir)
outdir.mkdir(parents=True, exist_ok=True)
rows = []
for kind, path in exported_paths.items():
rows.append(
{
"Mesh": kind,
"N": case["N"],
"DELTA": case["DELTA"],
"EPSILON": case["EPSILON"],
"MSH": str(path),
}
)
manifest = pd.DataFrame(rows)
csv_path = outdir / f"manifest_{case_to_stem(case)}.csv"
manifest.to_csv(csv_path, index=False)
return csv_path
# -------------------------
# Main
# -------------------------
def main():
case = make_case(N=N_DEFAULT, delta=DELTA, epsilon=EPSILON)
print("Using case:", case)
print("EXPORT_ALL =", EXPORT_ALL)
print("OUTDIR =", OUTDIR)
exported = export_selected_or_all(case=case, outdir=OUTDIR)
print()
print("Exported FreeFEM .msh files:")
for kind, path in exported.items():
print(f" {kind}: {path}")
if WRITE_INDICATOR_CSV:
indicator_csv = write_indicator_csv(case=case, outdir=OUTDIR)
print()
print(f"Indicator CSV: {indicator_csv}")
if WRITE_MANIFEST_CSV:
manifest_csv = write_manifest_csv(exported, case=case, outdir=OUTDIR)
print(f"Manifest CSV : {manifest_csv}")
print()
print("Done. This script only exports FreeFEM .msh files and metadata.")
print("Use readmesh(...) in a separate FreeFEM script to load the .msh file.")
print("Boundary labels are: bottom=1, right=2, top=3, left=4.")
first_kind = next(iter(exported))
print()
print(f"Head of {exported[first_kind]}:")
for line in preview_file_head(exported[first_kind], nlines=10):
print(line)
if __name__ == "__main__":
main()
