Arbitrary Shapes with Tetrahedral Meshes#
Shapes that cannot be meshed into cuboid or cylinder cells are handled through
magpylib.magnet.TriangularMesh: any closed triangular surface can be
discretized into conforming Tetrahedron cells with mesh_TriangularMesh,
which drives TetGen under the hood. TetGen is an optional
dependency:
pip install magpylib-material-response[tetgen]
Tetrahedral cells take the point-matched generic interaction path (see the solver performance example): both solvers share it and agree to solver tolerance, and the computation cost is dominated by the cells’ analytical field evaluation rather than by the solver choice.
This example validates the whole chain on a case with an exact analytical answer: a soft magnetic sphere, for which the demagnetizing factor is exactly 1/3, so a remanent polarization \(J_0\) relaxes to
Build and mesh a spherical TriangularMesh#
import magpylib as magpy
import numpy as np
from magpylib_material_response.demag import apply_demag
from magpylib_material_response.demag_fft import analyze_collection
from magpylib_material_response.meshing import mesh_TriangularMesh
magpy.defaults.display.backend = "plotly"
def fibonacci_sphere(n, radius):
"""n near-uniform points on a sphere (SI units: m)."""
k = np.arange(n)
phi = np.pi * (3.0 - np.sqrt(5.0)) * k
z = 1.0 - 2.0 * (k + 0.5) / n
r = np.sqrt(1.0 - z**2)
return radius * np.column_stack([r * np.cos(phi), r * np.sin(phi), z])
sphere = magpy.magnet.TriangularMesh.from_ConvexHull(
points=fibonacci_sphere(200, radius=1e-3),
polarization=(0, 0, 1),
)
mesh = mesh_TriangularMesh(sphere, target_elems=300)
cells = mesh.sources_all
print(f"{len(cells)} tetrahedra")
magpy.show(
{"objects": sphere, "col": 1},
{"objects": mesh, "col": 2},
)
853 tetrahedra
The structure analysis confirms the cells land in the point-matched generic cluster:
_, clusters = analyze_collection(cells)
[(c["kind"], len(c["indices"])) for c in clusters]
[('generic', 853)]
Apply demagnetization and compare with the exact sphere solution#
chi = 0.5
n = len(cells)
mesh_direct = apply_demag(mesh, susceptibility=[chi] * n, solver="direct")
mesh_iter = apply_demag(
mesh, susceptibility=[chi] * n, solver="iterative", solver_tol=1e-8
)
pol_direct = np.array([s.polarization for s in mesh_direct.sources_all])
pol_iter = np.array([s.polarization for s in mesh_iter.sources_all])
print(
"solver agreement:"
f" {np.abs(pol_direct - pol_iter).max() / np.abs(pol_direct).max():.1e}"
)
solver agreement: 1.3e-08
# volume-weighted mean polarization vs the analytical sphere result
volumes = np.array(
[
abs(np.linalg.det(np.array(c.vertices[1:]) - np.array(c.vertices[0]))) / 6
for c in cells
]
)
Jz_mean = (volumes * pol_direct[:, 2]).sum() / volumes.sum()
J_exact = 1.0 / (1.0 + chi / 3.0)
print(f"volume-averaged Jz : {Jz_mean:.4f} T")
print(f"analytical sphere : {J_exact:.4f} T")
print(f"relative error : {abs(Jz_mean - J_exact) / J_exact:.2%}")
volume-averaged Jz : 0.8572 T
analytical sphere : 0.8571 T
relative error : 0.01%
The volume-averaged polarization of the meshed sphere reproduces the exact demagnetizing-factor result to a fraction of a percent — the residual is the polyhedral approximation of the sphere surface and shrinks with finer surface and volume meshes.
Practical notes#
The generic path builds a dense point-matched interaction matrix, so cost and memory grow as N² regardless of solver — keep tetrahedra counts moderate (thousands, not tens of thousands), and prefer cuboid meshes when the geometry allows them.
target_elemsis a lower-bound hint: TetGen inserts Steiner points to meet its quality constraints, so the resulting cell count is typically larger. Theminratioandmindihedralarguments trade cell count against element quality.Everything shown in the solver performance example (agreement guarantee,
solver_tol,max_iter, non-convergence raising) applies unchanged.