Sat. Dec 6th, 2025

About

In the past, I compared computational cost between Skfem(Scikit-fem) and Ngsolve. Without doing experiments, I had already known the superiority of Ngsolve in terms this point, but I was unsure how much difference there is between them. What I found is that Scikit-fem is not so compettive with Ngsolve, but not super bad.

But I felt like, I want to have other option for solving heavier tasks. For sure we can use Ngsolve, but C++ language is NOT user friendly. Even though we have Python interface to access Ngsolve, the flexibility is limited.

To compare with other option, “Gridap” which I introduced before, I implemented script that solve FEM on the same task.

Result

This is what I really wanted😊

However, it should be noted that these results correspond to a warm start (i.e., the second and subsequent runs). During the first execution, JIT compilation is performed, which consumes computational resources in addition to the actual numerical calculation. As a result, the first run takes a total of 51.6 seconds, whereas subsequent runs are accelerated to under 2 seconds.

While Gridap already offers advantages such as convenient handling of I/O and integration with various packages, this study shows that it also delivers excellent computational performance.

In addition, I had forgotten to mention that Gridap also supports automatic differentiation.

Implementation

import time
import numpy as np
import skfem as sk


def run_skfem(mesh_file: str):
    from skfem.models.elasticity import lame_parameters
    from skfem.models.elasticity import linear_elasticity
    from skfem.helpers import dot

    E, nu = 210e3, 0.3
    traction_value = 1e-3
    lam, mu = lame_parameters(E, nu)

    t0_total = time.perf_counter()
    mesh = sk.MeshTet.load(mesh_file)
    basis = sk.Basis(mesh, sk.ElementVector(sk.ElementTetP1()))
    print(f"mesh.p {mesh.p.shape}")

    # @sk.BilinearForm
    # def elasticity(u, v, w):
    #     eps_u, eps_v = sk.sym_grad(u), sk.sym_grad(v)
    #     return lam * sk.trace(eps_u) * sk.trace(eps_v) + 2 * mu * sk.ddot(eps_u, eps_v)

    @sk.LinearForm
    def traction(v, w):
        return traction_value * dot(np.array([1, 0, 0]), v)

    left = mesh.facets_satisfying(lambda x: np.isclose(x[0], 0))
    right = mesh.facets_satisfying(lambda x: np.isclose(x[0], 1.0))
    fixed = basis.get_dofs(facets=left).all()
    print(f"fixed:{fixed.shape}")
    fbasis = sk.FacetBasis(
        mesh, sk.ElementVector(sk.ElementTetP1()), facets=right
    )

    # アセンブリ
    t0 = time.perf_counter()
    # K = sk.asm(elasticity, basis)
    K = linear_elasticity(lam, mu).assemble(basis)
    F = sk.asm(traction, fbasis)
    t_asm = time.perf_counter() - t0

    # Dirichlet条件 + 解く
    K, F = sk.enforce(K, F, D=fixed)
    t1 = time.perf_counter()
    u = sk.solve(K, F)
    t_solve = time.perf_counter() - t1

    t_total = time.perf_counter() - t0_total

    print(f"[scikit-fem] Total: {t_total:.3f}s | Assembly: {t_asm:.3f}s | Solve: {t_solve:.3f}s | max|u|={np.abs(u).max():.3e}")
    return mesh.p.T, u.reshape(-1, 3), [t_total, t_asm, t_solve]


def run_ngsolve(mesh_file: str):
    import time
    import numpy as np
    from ngsolve import (
        Mesh, VectorH1, BilinearForm, LinearForm, GridFunction,
        CoefficientFunction, grad, Trace, Id, InnerProduct,
        dx, ds, x, IfPos,
    )
    from netgen.read_gmsh import ReadGmsh

    # --- 材料・荷重パラメータ ---
    E, nu = 210e3, 0.3
    traction_value = 1e-3
    lam = E * nu / ((1 + nu) * (1 - 2 * nu))
    mu = E / (2 * (1 + nu))

    t0_total = time.perf_counter()

    # --- メッシュ読み込み (.msh) ---
    ngmesh = ReadGmsh(mesh_file)
    mesh = Mesh(ngmesh)
    print(f"nv = {mesh.nv}")

    # --- x のレンジを自動取得 ---
    xs = [vtx.point[0] for vtx in mesh.vertices]
    xmin, xmax = min(xs), max(xs)
    print(f"x-range: [{xmin:.6g}, {xmax:.6g}]")

    # Dirichlet は座標で後から指定するので、ここでは dirichlet="" は指定しない
    fes = VectorH1(mesh, order=1)

    u = fes.TrialFunction()
    v = fes.TestFunction()

    # --- 線形弾性の弱形式 ---
    def eps(w):
        return 0.5 * (grad(w) + grad(w).trans)

    def sigma(w):
        return lam * Trace(eps(w)) * Id(3) + 2 * mu * eps(w)

    a = BilinearForm(fes, symmetric=True)
    a += InnerProduct(sigma(u), grad(v)) * dx

    # --- Neumann: x ≈ xmax の面だけに traction をかける ---
    eps_coord = 1e-8 * max(1.0, abs(xmax - xmin))  # スケールに合わせて少しだけ幅を取る
    # x > xmax - eps のとき 1、それ以外 0
    indicator_right = IfPos(x - (xmax - eps_coord), 1.0, 0.0)

    tvec = CoefficientFunction((traction_value, 0.0, 0.0))
    f = LinearForm(fes)
    # 「すべての境界 ds」に対して、x≃xmax のところだけ traction が効く
    f += InnerProduct(indicator_right * tvec, v) * ds

    # --- アセンブリ ---
    t0 = time.perf_counter()
    a.Assemble()
    f.Assemble()
    t_asm = time.perf_counter() - t0

    print("||f|| =", f.vec.Norm())  # 荷重がちゃんと入っているか確認用

    # --- GridFunction と FreeDofs(BitArray コピー) ---
    gfu = GridFunction(fes)

    freedofs0 = fes.FreeDofs()             # BitArray
    freedofs = type(freedofs0)(freedofs0)  # 古い NGSolve 向けコピー

    # --- Dirichlet: x ≈ xmin の頂点の DoF を拘束 & 0 固定 ---
    for vtx in mesh.vertices:
        if abs(vtx.point[0] - xmin) < eps_coord:
            dnums = fes.GetDofNrs(vtx)  # この頂点に対応する DoF 番号
            for d in dnums:
                freedofs[d] = False   # 拘束
                gfu.vec[d] = 0.0      # Dirichlet 値

    # --- 解く ---
    t1 = time.perf_counter()
    gfu.vec.data = a.mat.Inverse(freedofs, inverse="sparsecholesky") * f.vec
    t_solve = time.perf_counter() - t1

    t_total = time.perf_counter() - t0_total

    # --- 頂点座標と変位を numpy 配列に ---
    coords = np.zeros((mesh.nv, 3))
    u_vals = np.zeros((mesh.nv, 3))

    for i, vtx in enumerate(mesh.vertices):
        p = vtx.point
        x0, y0, z0 = p[0], p[1], p[2]
        coords[i, :] = (x0, y0, z0)

        # MeshPoint を通して点評価
        val = gfu(mesh(x0, y0, z0))
        u_vals[i, :] = np.array(val)

    print(
        f"[NGSolve] Total: {t_total:.3f}s | "
        f"Assembly: {t_asm:.3f}s | "
        f"Solve: {t_solve:.3f}s | "
        f"max|u|={np.max(np.abs(u_vals)):.3e}"
    )

    return coords, u_vals, [t_total, t_asm, t_solve]


def visualize(
    skfem_times, ngsolve_times,
    dst_path: str = "benchmark_comparison.jpg"
):
    import matplotlib.pyplot as plt
    import numpy as np

    # データ(あなたの測定結果)
    labels = ["Total", "Assembly", "Solve"]
    
    x = np.arange(len(labels))  # X軸位置
    width = 0.35                # 棒の幅

    fig, ax = plt.subplots(figsize=(6, 4))
    rects1 = ax.bar(x - width/2, skfem_times, width, label='scikit-fem', color='#4C72B0')
    rects2 = ax.bar(x + width/2, ngsolve_times, width, label='ngsolve', color='#55A868')

    # 軸ラベル・タイトル
    ax.set_ylabel('Time [s]')
    ax.set_title('FEM Benchmark Comparison')
    ax.set_xticks(x)
    ax.set_xticklabels(labels)
    ax.legend()

    # 各棒に値を表示
    def autolabel(rects):
        for rect in rects:
            height = rect.get_height()
            ax.annotate(f'{height:.2f}',
                        xy=(rect.get_x() + rect.get_width() / 2, height),
                        xytext=(0, 3),
                        textcoords="offset points",
                        ha='center', va='bottom', fontsize=9)

    autolabel(rects1)
    autolabel(rects2)

    fig.tight_layout()
    plt.savefig(dst_path, dpi=200)
    # plt.show()

    print("✅ Saved: benchmark_comparison.png")


def compare_displacements(coords1, u1, coords2, u2, tol=1e-8):
    # 両メッシュが一致する前提
    assert np.allclose(coords1, coords2, atol=tol)

    diff = u1 - u2
    abs_err = np.linalg.norm(diff, axis=1)
    l2_err = np.linalg.norm(abs_err)
    rel_err = l2_err / np.linalg.norm(np.linalg.norm(u1, axis=1))
    max_err = np.max(abs_err)

    print(f"‣ L2 error = {l2_err:.3e}")
    print(f"‣ Relative L2 error = {rel_err:.3e}")
    print(f"‣ Max abs error = {max_err:.3e}")


if __name__ == "__main__":
    msh = "tension_bar.msh"  # Gmshで生成済みメッシュ

    print("run_skfem")
    skfem_coords, skfem_disp, skfem_times = run_skfem(msh)

    print("run_ngsolve")
    ngsolve_coords, ngsolve_disp, ngsolve_times = run_ngsolve(msh)
    tT_s, tA_s, tS_s = skfem_times
    tT_m, tA_m, tS_m = ngsolve_times

    print("skfem max|u| =", np.abs(skfem_disp).max())
    print("ngsolve  max|u| =", np.abs(ngsolve_disp).max())
    print("ratio (skfem/ngsolve) ≈", np.abs(skfem_disp).max() / np.abs(ngsolve_disp).max())

    compare_displacements(skfem_coords, skfem_disp, ngsolve_coords, ngsolve_disp)

    print("\n=== Benchmark Summary ===")
    print(f"scikit-fem: Total {tT_s:.3f}s | Assembly {tA_s:.3f}s | Solve {tS_s:.3f}s")
    print(f"ngsolve:       Total {tT_m:.3f}s | Assembly {tA_m:.3f}s | Solve {tS_m:.3f}s")

    visualize(
        skfem_times, ngsolve_times,
    )

Result