Magnetostatics¶
This chapter describes the magnetostatic subproblem solved alongside stereographic LLG
dynamics in Z_sim_dynamic_stereo_demag.
The demagnetizing field \(\mathbf{H}_{\mathrm{d}}=-\nabla\varphi\) is derived from a
scalar magnetostatic potential \(\varphi\), computed via the Fredkin–Koehler
decomposition \(\varphi=\varphi_0+\varphi_1\) described below. Build and runtime
setup for the BEM component are in Installation; weak-form coupling to the
stereographic chart coordinates is in Stereographic formulation.
Governing equations¶
Time-dependent equations¶
The fundamental equations governing electromagnetic phenomena are Maxwell’s equations. In the general time-dependent case they read as
where \(t\) is the time and:
\(\mathbf{E}\) is the electric field [\(\text{V}/\text{m}\)]
\(\mathbf{D}\) is the electric flux density (or electric displacement field) [\(\text{C}/\text{m}^2\)]
\(\mathbf{H}\) is the magnetic field intensity [\(\text{A}/\text{m}\)]
\(\mathbf{B}\) is the magnetic flux density (or magnetic induction) [\(\text{Wb}/\text{m}^2=\text{T}\)]
\(\mathbf{J}\) is the electric current density [\(\text{A}/\text{m}^2\)]
\(\rho\) is the electric charge volume density [\(\text{C}/\text{m}^3\)]
Maxwell’s equations are supplemented by the continuity equation
Of the five equations above, only three are independent; the standard choice is the Faraday-Henry law, the Maxwell-Ampere law, and the continuity equation (42). These are accompanied by the constitutive relationships describing the electromagnetic properties of the material:
where
\(\sigma\) is the electric conductivity [\(\text{S}/\text{m}\)]
\(\epsilon\) is the electric permittivity [\(\text{F}/\text{m}\)]
\(\mu\) is the magnetic permeability [\(\text{H}/\text{m}\)]
\(\mathbf{J}_\text{s}\) is the source current density [\(\text{A}/\text{m}^2\)]
(43) expresses the total current density as the sum of a conduction term (Ohm’s law) and a prescribed source term \(\mathbf{J}_\text{s}\). (44) relates the electric flux density to the electric field via the permittivity of the medium. (45) establishes a linear relationship between the magnetic flux density \(\mathbf{B}\) and the magnetic field intensity \(\mathbf{H}\) at every point in the material.
The constitutive parameters \(\sigma\), \(\epsilon\), and \(\mu\) are scalars for isotropic materials, tensors for anisotropic materials, and functions of position for heterogeneous media. The permittivity and permeability can be factored as \(\epsilon=\epsilon_r\epsilon_0\) and \(\mu=\mu_r\mu_0\), where \(\epsilon_r\) and \(\mu_r\) are the dimensionless relative permittivity and permeability of the material, and \(\mu_0=4\pi\times10^{-7}\,\text{H/m}\), \(\epsilon_0=1/(\mu_0 c^2)\approx 8.854\times10^{-12}\,\text{F/m}\) are the free-space values (\(c\) is the speed of light in vacuum).
Time-independent equations¶
Under electrostatic and magnetostatic conditions the field quantities do not vary in time. In this case, electric and magnetic phenomena decouple from each other. The governing equations reduce to
Assuming a vanishing electric current \({\bf J}={\bf 0}\), the time-independent governing equations simplify to
Electrostatic problem
Magnetostatic problem
For permanent magnets, (48) assumes the modified form
where \(\mathbf{M}\) is the magnetization field [\(\text{A}/\text{m}\)], equal to \(\mathbf{0}\) outside the magnetic material.
Magnetostatics¶
According to (46), the magnetostatic field \(\mathbf{H}\) is irrotational and therefore conservative. It can thus be expressed as the gradient of a scalar potential \(\varphi\) [A], called the magnetostatic potential:
Substituting (50) and the constitutive law (49) into the Gauss magnetic law (47) gives \(\nabla\cdot\mathbf{B}=\mu_0(\nabla\cdot\mathbf{M}-\Delta\varphi)=0\), which simplifies to the Poisson equation
This equation must be solved throughout \(\mathbb{R}^3\), decomposed into the magnetic domain \(\Omega_\text{m}\) and its complement, the vacuum \(\Omega_\text{v}\). The boundary conditions are
The first condition (52) is an open boundary condition, requiring the potential to decay at infinity. The second, (53), imposes continuity of \(\varphi\) across \(\partial\Omega_\text{m}\). The third, (54), is a jump condition on the normal derivative: the superscripts \(+\) and \(-\) denote the inner (magnet) and outer (vacuum) limits at \(\partial\Omega_\text{m}\), respectively, and \(\mathbf{n}\) is the outward unit normal to \(\partial\Omega_\text{m}\) (pointing from magnet into vacuum).
Boundary conditions¶
The continuity condition (53) follows from applying Stokes’ theorem to the magnetostatic Ampère law (46) [Wolski, n.d.]. To this aim, consider a surface \(\mathcal{S}\) bounded by a loop \(\partial\mathcal{S}\) that crosses the boundary \(\partial\Omega_\text{m}\) between the magnet and the vacuum, as shown in Figure 4 (left). Integrating both sides of (46) over \(\mathcal{S}\) and subsequently applying Stokes’ theorem leads to
Let \(\mathcal{S}\) be a thin rectangular strip whose long edges are parallel to \(\partial\Omega_\text{m}\) and whose short edges cross it perpendicularly. As the length of the short edges tends to zero, their contributions to the line integral vanish. Only the long-edge contributions \(\mathbf{H}_\parallel^+\) and \(\mathbf{H}_\parallel^-\) remain, giving
The tangential component of \(\mathbf{H}\), and hence of \(\nabla\varphi\), is therefore continuous across \(\partial\Omega_\text{m}\). This means \(\varphi\) can differ at most by a constant across the interface; setting that constant to zero yields (53).
Figure 4 Left: Thin rectangular loop \(\partial\mathcal{S}\) straddling the boundary \(\partial\Omega_\text{m}\) used to derive tangential continuity of \(\mathbf{H}\). Right: Gaussian pillbox \(\mathcal{V}\) straddling \(\partial\Omega_\text{m}\) used to derive the jump condition on the normal derivative of \(\varphi\).¶
The jump condition (54) follows from substituting the constitutive relation (49) into the Gauss magnetic law (47):
Consider a volume \(\mathcal{V}\) bounded by a surface \(\mathcal{A}\) that straddles \(\partial\Omega_\text{m}\), as shown in Figure 4 (right). Integrating the identity above over \(\mathcal{V}\) and applying the divergence theorem gives
where \(\mathbf{n}_\mathcal{A}\) is the outward normal to \(\mathcal{A}\). Partitioning \(\mathcal{V}\) into two subvolumes \(\mathcal{V}_1\) (inside \(\Omega_\text{m}\)) and \(\mathcal{V}_2\) (inside \(\Omega_\text{v}\)) by the shared surface \(\mathcal{A}_3\subset\partial\Omega_\text{m}\), the flux through the full surface decomposes as
The outward normal to \(\mathcal{A}_3\) points in the direction \(+\mathbf{n}\) as seen from \(\mathcal{V}_1\) and in the direction \(-\mathbf{n}\) as seen from \(\mathcal{V}_2\), so \(\Phi_{13}=-\Phi_{23}\). The contributions from the outer faces \(\mathcal{A}_1\) and \(\mathcal{A}_2\) vanish as the volume collapses onto \(\partial\Omega_\text{m}\), leaving
Since this must hold for any such surface element, the integrand must vanish pointwise, giving
Here \(+\) is the side facing \(\mathcal{V}_1\) (magnet interior) and \(-\) is the side facing \(\mathcal{V}_2\). When \(\mathcal{V}_2\) coincides with the vacuum, \(\mathbf{M}^-=\mathbf{0}\) and (55) reduces to (54).
Equation (55) is equivalent to continuity of the normal component of \(\mathbf{B}\) across the interface:
Note
The jump condition (55) and the continuity condition (53) apply equally at interfaces between two magnetic materials with different properties [Weddemann et al., 2011, Wolski, n.d.]. In summary, the solution sought is a function \(\varphi(\mathbf{x})\) defined for all \(\mathbf{x}\in\mathbb{R}^3\) that satisfies the Poisson equation (51), decays at infinity according to (52), and fulfills (53)–(55) at every material interface.
Hybrid FEM-BEM approach: homogeneous material¶
The Fredkin–Koehler approach [Fredkin and Koehler, 1990] introduces the decomposition of the magnetostatic potential \(\varphi\) into two contributions
where \(\varphi_0\) is defined as the solution of the Neumann problem
whereas \(\varphi_1\) is the solution of the Laplace problem
The resulting potential \(\varphi = \varphi_0 + \varphi_1\) is continuous across the boundary \(\partial\Omega_\text{m}\) (thus satisfying condition (53)), while the individual parts \(\varphi_0\) and \(\varphi_1\) are generally discontinuous. Splitting the potential into two parts allows one to avoid the difficulty that the interface conditions (53) and (54) are a priori unknown, since the value of the potential at the boundary \(\partial\Omega_\text{m}\) is not prescribed explicitly.
Since \(\varphi_1\) satisfies a homogeneous Laplace equation, its value at any point \({\bf x}\in\mathbb{R}^n\) can be recovered from the boundary data via the representation formula
where \(n\) is the system dimension and \({\bf x}^\prime\) is the integration variable on \(\partial\Omega_\text{m}\). The Green’s function \(\mathcal{G}\) of the boundary value problem is [Weddemann et al., 2011]
Unfortunately, evaluating (58) at every point in \(\mathbb{R}^n\) is numerically expensive, with storage requirements of order \(\mathcal{O}(\mathcal{N}^{5/3})\), where \(\mathcal{N}\) is the number of elements [Fredkin and Koehler, 1990, Weddemann et al., 2011]. Instead, the integral is evaluated only at the boundary nodes \({\bf x}\in\partial\Omega_\text{m}\), and the resulting values are used as the Dirichlet data needed to close the Laplace problem for \(\varphi_1\). In the framework of a hybrid finite element – boundary element formulation (FEM-BEM), the part \(\varphi_1\) is therefore the solution of
where \(\hat{\Omega}(\mathbf{x})\) is the solid angle subtended by \(\partial\Omega_\text{m}\) at \(\mathbf{x}\). For a smooth (non-corner, non-edge) boundary point the solid angle equals \(2\pi\); at corners and edges it takes a smaller value that depends on the local geometry. In the implementation each boundary node carries its own solid-angle weight, computed from the surrounding facets.
In summary, the Fredkin–Koehler approach consists of
first, solving the Neumann problem for \(\varphi_0\) in \(\Omega_\text{m}\);
second, solving the Laplace problem for \(\varphi_1\) with Dirichlet data on \(\partial\Omega_\text{m}\) supplied by the BEM operator (in the production solver this FEM step is carried out on \(\Omega_\text{m}\) only; see the mesh-scope note below).
Variational form of the Neumann problem¶
In the presence of a magnetic induction field \({\bf B}\cdot {\bf n}=-B_0\) applied on the external boundary \(\partial\Omega_\text{m}\) [Dornisch et al., 2019], the boundary condition for the Neumann problem reads as
where \(H_\text{zee} = B_0/\mu_0\). This leads to the following Neumann problem
where \(f\) and \(g\) are input data and \({\bf n}\) is the outward boundary normal. The approximation to the solution is called the trial function \(\varphi_0^\text{trial}\), and it is tested against a test function \(\varphi_0^\text{test}\). Both belong to a suitable function space. Multiplying the PDE by \(\varphi_0^\text{test}\) and integrating over \(\Omega_\text{m}\) gives
When deriving variational principles, the order of derivatives should be kept as small as possible. To lower the order of differentiation, the Laplacian is shifted onto the test function by integrating by parts (Green’s first identity):
The test function must vanish wherever the trial function is prescribed (Dirichlet data). Because the Neumann problem has no Dirichlet conditions, \(\varphi_0^\text{test}\neq 0\) on \(\partial\Omega_\text{m}\) and the boundary term is retained. Substituting the Neumann condition \(g\) gives
Rearranging yields the weak form of the Neumann problem
In the standard notation \(a(u,v)=L(v)\):
The non-dimensional form of the Neumann problem uses the reference scales of material 1 (consistent with (23) in Micromagnetic energy):
Dividing through by the common factor \(M_\mathrm{s1}^2\ell_\mathrm{ex1}\) yields the non-dimensional weak form
where the non-dimensional source terms are the pointwise fields
The non-dimensional standard form \(a^\ast(u^\ast,v^\ast)=L^\ast(v^\ast)\) is
Variational form of the Laplace problem¶
The FEM/BEM formulation of the Laplace problem reads as
The integral expression of the Dirichlet boundary condition can be replaced by its discretized counterpart as [Fredkin and Koehler, 1990]
where \(\mathcal{B}\) is the BEM matrix, assembled from the kernel integral (58). Multiplying the Laplace equation by \(\varphi_1^\text{test}\) and integrating over \(\Omega\) gives
Integrating by parts:
Since \(\varphi_1^\text{trial}\) is prescribed on \(\partial\Omega_\text{m}\) (Dirichlet condition) and vanishes at infinity, the test function satisfies \(\varphi_1^\text{test} = 0\) on the full boundary \(\partial\Omega\), and the surface integral drops out. The weak form is
which can be expressed in the usual form \(a(u,v)=L(v)\) as
The non-dimensional form of the Laplace problem is obtained as follows
with the non-dimensional standard form \(a^\ast(u^\ast,v^\ast)=L^\ast(v^\ast)\) is
Hybrid FEM-BEM approach: heterogeneous material¶
Consider a magnetic material \(\Omega_\text{m}\) comprised of two different materials \(\Omega_\text{m1}\) and \(\Omega_\text{m2}\), divided by the interface \(\Gamma\). The magnetic domain is surrounded by the vacuum \(\Omega_\text{v}\). The overall volume is \(\Omega = \Omega_\text{m} \cup \Omega_\text{v}\), where \(\Omega_\text{m}=\Omega_\text{m1}\cup\Omega_\text{m2}\). The boundary between magnetic domain and vacuum is \(\partial\Omega_\text{m}=\partial\Omega_\text{m1}\cup\partial\Omega_\text{m2}\). The outward normal to \(\Omega_\text{m}\) is \({\bf n}\). The outward normal to \(\Omega_\text{m1}\) (respectively, \(\Omega_\text{m2}\)) is \({\bf n}^+\) (respectively, \({\bf n}^-\)), with \({\bf n}^- = -{\bf n}^+\).
The magnetostatic equations are
In the hybrid FEM-BEM approach
The previous equations are rewritten as follows.
Equation \(\Delta \varphi = \nabla \cdot {\bf M}\):
Neumann equations on \(\partial\varphi/\partial{\bf n}\):
Equation \(\varphi^+ = \varphi^-\):
Variational form of the Neumann problem¶
The variational form of the Neumann problem is
which results in
by applying the Neumann boundary conditions
The previous equation is non-dimensionalized by taking material 1 as a reference
with this notation, one obtains
which in the general case can be recast as
The boundary integral over \(\partial\Omega_\text{m}\) ensures \({\bf B}_\perp^+ = {\bf B}_\perp^-\) between the magnet and the vacuum. The boundary integral over \(\Gamma\) ensures \({\bf B}_\perp^+ = {\bf B}_\perp^-\) across the interface between different materials.
Variational form of the Laplace problem¶
The variational form of the Laplace problem is
which results in
\(\varphi_1^\text{test}=0\) in parts of the boundary where the solution \(\varphi_1^\text{trial}\) is known. In the Laplace problem, \(\varphi_1^\text{trial}\) is known on \(\partial\Omega_\text{m}\) (Dirichlet boundary condition). The first boundary integral can thus be dropped. The second boundary integral can also be dropped, given the Neumann boundary condition \(\nabla\varphi_1^\text{trial+}\cdot{\bf n}^+ = \nabla\varphi_1^\text{trial-}\cdot{\bf n}^-\).
The previous equation is non-dimensionalized by taking material 1 as a reference. After some algebra, one obtains
this needs to be solved together with the boundary conditions
Note
Nullspace of the Fredkin–Koehler decomposition
Because \(\varphi_0\) solves a Neumann (Poisson) problem with purely natural boundary conditions, it is determined only up to an additive constant — its solution space contains a one-dimensional nullspace of constant functions.
\(\varphi_1\), by contrast, solves a Laplace problem with Dirichlet data on the external surface and therefore uniquely fixes this constant offset. Consequently, the total potential \(\varphi = \varphi_0 + \varphi_1\) is well-defined, and it is not necessary to explicitly pin the nullspace of \(\varphi_0\) in the linear solver.
BEM assembler¶
The Laplace subproblem for \(\varphi_1\) requires Dirichlet data on
\(\partial\Omega_\text{m}\) at every time-step. Those data are provided by the
boundary integral (59), whose integrand depends on the boundary geometry
but not on the magnetization. The integral can therefore be discretized once into the
BEM matrix \(\mathbf{B}\), assembled at the start of the simulation by
bem_assembler.bem_assembler.BEMAssembler and stored as a read-only operator;
at each step, applying it reduces to a single matrix–vector product. The assembly is
implemented in C++, with the matrix written directly into a pre-allocated NumPy buffer to
avoid redundant copies.
The subsections below cover the discretization of \(\mathbf{B}\), its distributed
assembly and memory layout, and its use in
Z_sim_magnetostatic.
Integral discretization¶
The boundary representation formula (59) gives \(\varphi_1\) at any point \(\mathbf{x}\in\partial\Omega_\text{m}\) as
The first term is the double-layer potential: each source point \(\mathbf{x}^\prime\) on the boundary contributes to the field at the observation point \(\mathbf{x}\) through the normal derivative of the three-dimensional free-space Green’s function \(\mathcal{G}(\mathbf{x},\mathbf{x}^\prime)=-1/(4\pi\lvert\mathbf{x}-\mathbf{x}^\prime\rvert)\):
The second term is a local correction: \(\hat{\Omega}(\mathbf{x})\) is the solid angle subtended by \(\partial\Omega_\text{m}\) at \(\mathbf{x}\), equal to \(2\pi\) on a smooth surface and smaller at corners or edges.
The boundary surface \(\partial\Omega_\text{m}\) is triangulated into \(N_\text{bnd}\) nodes. Approximating \(\varphi_0\) on the boundary by piecewise-linear (P1) nodal functions \(\{\Lambda_j\}\):
and evaluating at each boundary node \(\mathbf{x}_i\), the integral in (61) becomes a matrix–vector product:
where the matrix entries \(B_{ij}\) are
The Dirichlet condition for the Laplace subproblem therefore reads
\(\mathbf{B}\) is a dense \(N_\text{bnd}\times N_\text{bnd}\) matrix whose entries depend only on the boundary geometry. It is assembled once and reused at every time-step as a read-only operator. Total storage is \(8\,N_\text{bnd}^2\) bytes, distributed across \(P\) MPI ranks as described below.
Off-diagonal entries¶
The off-diagonal entries of \(\mathbf{B}\) come from the surface integral in
(64). Because each P1 basis function \(\Lambda_j\) is nonzero only on
the triangles that share node \(j\), the global integral over \(\partial\Omega_\text{m}\)
reduces to a sum over individual triangles. For each boundary triangle \(T_k\) with
outward unit normal \(\mathbf{n}_k\) and vertex indices \(g_0,g_1,g_2\), and for each
observation node \(\mathbf{x}_i\) that is not a vertex of \(T_k\), the C++ routine
compute_belement (bem_assembler/cpp/include/bem_assembler.hpp) evaluates three scalar
weights, one per vertex of \(T_k\):
where \(N_\ell\) is the barycentric coordinate equal to 1 at vertex \(g_\ell\) and 0 at the other two vertices of \(T_k\). The weight \(w_\ell\) captures the influence of basis function \(\Lambda_{g_\ell}\) on the potential at \(\mathbf{x}_i\): it integrates the Green’s function normal derivative over triangle \(T_k\) weighted by the fraction of that triangle “attributed” to vertex \(g_\ell\). Each weight is added to the matrix entry at row \(i\), column \(g_\ell\):
The assembly is organized as four nested loops over source rank, source triangle, owned observation row, and triangle vertex. The body of the two innermost loops evaluates (66) and accumulates the three weights into the corresponding matrix entries:
for (int k = 0; k < size; ++k) { // (k) source ranks
for (int j = 0; j < n_facets_k; ++j) { // (j) triangles on rank k
for (int i = 0; i < n_local; ++i) { // (i) owned observation rows
if (node_is_vertex_of_facet(global_i, facet_nodes_index)) continue;
Vector3D w = compute_belement(x_i, v0, v1, v2);
for (int l = 0; l < 3; ++l) // (l) vertices of the triangle
add_entry(i, facet_nodes_index[l], w[l]);
}
}
}
Each loop level has a distinct role:
Loop
kover source ranks. Every rank holds a complete local replica of all boundary coordinates after the geometry-exchange step described in Stage 2 of the distributed assembly. This loop therefore iterates over all \(P\) partitions without any MPI communication.Loop
jover boundary triangles on rankk. For each triangle,compute_belementreturns three scalar weights \(w_0, w_1, w_2\), one per vertex, each added into the corresponding column of \(\mathbf{B}^{(r)}\).Loop
iover the \(n_r\) observation rows owned by the current rank. This is the loop that fills one row block of \(\mathbf{B}^{(r)}\). Whenever node \(i\) coincides with a vertex of the current triangle, the integrand of (66) becomes singular; those pairs are skipped here and handled analytically by the diagonal entries described in the next section.Loop
lover the three vertices of trianglej. Aftercompute_belementreturns the weight vector \((w_0, w_1, w_2)\), this loop adds each component into the matrix column corresponding to the global boundary index of vertex \(g_\ell\). The calladd_entry(i, facet_nodes_index[l], w[l])accumulates \(B_{i,\,g_\ell} \mathrel{+}= w_\ell\), i.e., it executes (67).
Diagonal entries¶
The diagonal entry \(B_{ii}\) accounts for all triangles that contain node \(i\) as a vertex, precisely those excluded from the off-diagonal loop. Rather than integrating a singular kernel, \(B_{ii}\) is set analytically from the solid angle \(\Omega_i\) subtended by the volume mesh at \(\mathbf{x}_i\):
where the sum runs over all tetrahedra \(C\) that share node \(i\), and \(\Omega(C,i)\) is the solid angle of \(C\) as seen from vertex \(i\) [Weddemann et al., 2011]:
with \(\mathbf{a},\mathbf{b},\mathbf{c}\) the edge vectors from \(i\) to the other three vertices of \(C\) and \(a=|\mathbf{a}|\), etc. On a smooth surface, \(\Omega_i\approx 2\pi\), so \(B_{ii}\approx -\tfrac{1}{2}\).
In code, after the off-diagonal loop completes, each diagonal entry is overwritten
(set_entry replaces, it does not accumulate):
// run after the off-diagonal triple loop
// solid_angles[i] was computed in Stage 3 (C++ kernel + scatter_reverse)
for (int i = 0; i < n_local; ++i)
set_entry(i, global_index(i), solid_angles[i] / (4*M_PI) - 1.0);
// ^ overwrites any spurious value; off-diagonal loop skipped these entries
Distributed assembly¶
The DOLFINx partitioner divides volume elements among ranks; boundary node ownership follows from the resulting partition. A rank whose volume partition does not intersect \(\partial\Omega_\text{m}\) owns no boundary nodes, which can occur depending on the mesh and the number of MPI ranks. Each rank \(r\) is responsible for the rows of \(\mathbf{B}\) corresponding to its \(n_r\) owned boundary nodes, stored as a dense array of shape \((n_r,\,N_\text{bnd})\) called the row block \(\mathbf{B}^{(r)}\), with \(N_\text{bnd}=\sum_r n_r\).
A complication arises at partition boundaries. A boundary triangle belonging to rank \(r\) may have one or two vertices whose DOFs are owned by a neighbouring rank \(s\). These are called ghost nodes on rank \(r\): rank \(r\) needs their coordinates to evaluate the kernel integral (66), even though it is rank \(s\) that holds the corresponding matrix rows. The indexing structures described below record, for every ghost node, the owning rank and the local boundary index on that rank, so that each computed weight is added into the correct global column of \(\mathbf{B}\).
bem_assembler.bem_assembler.BEMAssembler constructs the distributed matrix through
four stages, all triggered by BEMAssembler.assemble():
Boundary indexing (
compute_boundary_nodes). Each rank scans its tagged exterior facets and builds three local index structures:bnd_nodes: an ordered list of boundary vertex indices in geometry numbering. Owned vertices appear first (indices \(0,\ldots,n_r-1\)); ghost vertices are appended. This ordering is used throughout assembly to avoid indirect lookups.gnodes_to_bnodes: the inverse map from any mesh vertex index \(v\) to its position inbnd_nodes. Returns \(-1\) for interior nodes not on the boundary.bnd_f_to_bnd_n: for each owned boundary facet, the threebnd_nodesindices of its vertices (local boundary indices, not raw mesh indices).
Ghost nodes are additionally catalogued in
ghost_bnd_indices: a table with one row per ghost, storing the owning rank and the local boundary index on that rank. A lightweightallgatherof per-rank node counts gives every rank the arrayglobal_n_bnd_nodes, from which the global column offset of any rank is \(\sum_{k<r} n_k\).Geometry exchange (
gather_boundary_data). Each rank broadcasts its boundary coordinates, facet connectivity, and ghost index table via a singlecomm.allgathercollective. After this step every rank holds the complete boundary geometry of the entire mesh. This is the only MPI communication during matrix assembly; all subsequent integral evaluations proceed independently on each rank.Solid-angle accumulation (
compute_solid_angles). The C++ kernelcompute_solid_angles_localaccumulates \(\Omega(C,i)\) from the locally owned volume cells. A boundary node near a partition boundary may have incident volume cells on multiple ranks; a DOLFINxscatter_reversethen sums these partial contributions so that the owning rank holds the correct total solid angle \(\Omega_i\).Matrix fill (
_assemble_cpp). The C++ routineassemble_directruns the triple loop described in the previous section on the locally replicated geometry, accumulating off-diagonal entries (67) and then overwriting diagonal entries (68). All ranks execute this loop simultaneously and independently, with no further MPI calls.
BEM: distributed assembly & matrix–vector product
How the BEM matrix is split across ranks and filled in parallel, and how it is applied
as a distributed matrix–vector product at each time-step in compute_phi.
Interactive figure: row ownership across ranks, the allgather geometry exchange, the parallel
fill loop, and the distributed matrix–vector product in compute_phi.
C++ implementation¶
Dense row blocks. Each rank \(r\) stores its share of the BEM matrix as a C-contiguous float64 NumPy array of shape \((n_r, N_\text{bnd})\). Each row of \(\mathbf{B}^{(r)}\) corresponds to one observation node owned by rank \(r\); its \(N_\text{bnd}\) columns store the influence of every boundary source node on that observation point. The local memory footprint is
If the boundary nodes were distributed evenly, \(n_r \approx N_\text{bnd}/P\), and the per-rank footprint would scale as \(8\,N_\text{bnd}^2/P\): doubling the rank count would halve the memory per process. Summing over all \(P\) ranks always recovers the total \(8\,N_\text{bnd}^2\) bytes, the \(\mathcal{O}(N_\text{bnd}^2)\) cost inherent to any dense boundary operator.
The geometry exchange (Stage 2) additionally replicates the full boundary geometry — coordinates, facet connectivity, and ghost index tables — on every rank. This auxiliary replica grows only as \(\mathcal{O}(N_\text{bnd})\) per rank, while the matrix block on the same rank occupies \(\mathcal{O}(N_\text{bnd}^2/P)\). For any realistic problem size and rank count, the geometry replica is orders of magnitude smaller than the matrix block and contributes negligibly to total memory.
Zero-copy fill.
The production assembly backend (bem_cpp) eliminates all intermediate data copies
across the Python/C++ boundary. The sequence is as follows:
Allocation. Python pre-allocates
output_buffer = np.zeros((n_local, n_global))as the definitive storage for all matrix entries. No further allocation occurs on the hot path.Pointer passing. The raw memory address of
output_bufferis forwarded through the pybind11 binding layer directly to the C++ routineassemble_direct, without creating any intermediate Python or C++ object.Direct write. Inside C++, every call to
add_entryexecutesoutput_ptr[i * n_global + j] += value, writing into the NumPy buffer in place. Nostd::vectoror other intermediate container is allocated on the hot path.No data copy. Only the pointer value crosses the language boundary; the matrix data itself is never duplicated.
Cache-friendly layout. The row-major (C-contiguous) layout means that consecutive column accesses within one row are physically adjacent in memory. The nine coordinates of a triangle’s three vertices fit in at most two 64-byte cache lines, allowing the hardware prefetcher to pipeline triangle loads.
The convenience wrapper assemble and the debug module bem_cpp_debug bypass this
path and incur an extra copy; they are intended for testing and validation only.
Persistent solve buffers.
initialize_bem_cache is called once after assembly completes. It allocates the
following persistent objects, which are reused without reallocation at every subsequent
magnetostatic solve:
global_recvcountsandglobal_displs— MPI displacement arrays derived from the per-rank node counts, used to driveMPI_Allgathervat each time-step.A receive buffer of length \(N_\text{bnd}\) that accumulates the complete global \(\varphi_0\) boundary vector after the collective.
A result buffer of length \(n_r\) that holds the local matrix–vector product \(\mathbf{B}^{(r)}\,\boldsymbol{\varphi}_{0,\text{glob}}\) before it is scattered back into the DOLFINx function for \(\varphi_1\).
The matrix \(\mathbf{B}^{(r)}\) is never modified or reallocated after the initial assembly; only the two boundary buffers are overwritten at each magnetostatic update.
BEM: memory layout & solve buffers
How the dense row block is stored in RAM, how the C++ kernel writes directly into it, and what buffers are allocated once for the solve phase.
Interactive figure: the dense row-block layout per rank, the zero-copy C++ pointer path, and the persistent solve buffers.
Fredkin–Koehler solve cycle¶
compute_phi executes the following four operations at each time-step:
Neumann solve. Assemble \(f = -M_s\nabla\cdot\mathbf{m}\) and \(g = M_s\mathbf{m}\cdot\mathbf{n}+H_\text{zee}\) from the current magnetization and solve the weak form (60) to obtain \(\varphi_0\) on \(\Omega_\text{m}\).
BEM step. Each rank extracts its \(n_r\) owned boundary values and assembles the global \(\varphi_0\) boundary vector of length \(N_\text{bnd}\) via
MPI_Allgatherv. The local Dirichlet data for \(\varphi_1\) follow from\[\boldsymbol{\varphi}_1\big|_r = \mathbf{B}^{(r)}\,\boldsymbol{\varphi}_0\big|_{\partial\Omega_\text{m}}\]computed as a single local matrix–vector product (
np.dot). No further MPI communication is needed, because every rank already holds the complete global \(\varphi_0\) vector after the collective.Laplace solve. The \(n_r\) Dirichlet values are imposed on \(\partial\Omega_\text{m}\) and the homogeneous problem \(\int_{\Omega_\text{m}}\nabla\varphi_1\cdot\nabla v\,dV=0\) is solved for \(\varphi_1\).
Total field. Set \(\varphi = \varphi_0+\varphi_1\) and \(\mathbf{H}_\mathrm{d} = -\nabla\varphi\).
\(\mathbf{B}^{(r)}\) is read-only throughout; the single MPI_Allgatherv in step 2
is the only collective communication per magnetostatic solve.
Dynamic demag coupling¶
In Z_sim_dynamic_stereo_demag,
the demagnetizing field is \(\mathbf{H}_{\mathrm{d}} = -\nabla\varphi\), with
\(\varphi\) the total Fredkin–Koehler potential \(\varphi_0+\varphi_1\).
The demagnetization contribution to \(\mathcal{E}_{\mathrm{SI}}\) is
(17); the stored density \(W_{\mathrm{d}}\) used in the LLG weak form
is (39) in Micromagnetic energy. The weak form uses a weighted blend of chart
magnetizations \(\mathbf{m}(q_A,+1)\) and \(\mathbf{m}(q_B,-1)\) as source for the Poisson
subproblem.
Note
Mesh scope in the production demag path.
Although the theory above formulates \(\varphi_1\) on the full domain
\(\Omega_\text{m}\cup\Omega_\text{v}\), the production solver meshes only
\(\Omega_\text{m}\). The exterior (vacuum) contribution is encoded entirely by the
BEM matrix \(\mathbf{B}\), which supplies the Dirichlet boundary condition for
\(\varphi_1\) on \(\partial\Omega_\text{m}\). No FEM vacuum mesh is required
or constructed. A vacuum FEM mesh is available as an optional extension via
Zmag_magnetostatic_vacuum.py (used together with for_vacuum_dir checkpoints),
but is not part of the standard dynamic demag workflow.
Python entry points:
Z_sim_magnetostatic— FEM/BEM helperZ_sim_dynamic_stereo_demag.set_magnetostatics/set_BEMcpp— wiring for a run