3D Permanent Magnet

I’m trying to implement a basic case of a cylindrical permanent magnet.

However, my vector potential is null outside the magnet. I’m looking for some guidance on what is causing this.

Here’s my code:

#!/usr/bin/env python3
"""
magnet.py
Solve vector potential A for a permanent magnet in an airbox and output A and B.

"""
import os
import numpy as np
import ufl
from mpi4py import MPI
from petsc4py import PETSc
from dolfinx import fem, io, mesh
from dolfinx.fem import Function, functionspace, Expression
from dolfinx.fem.petsc import (
    assemble_matrix as p_assemble_matrix,
    assemble_vector as p_assemble_vector,
    apply_lifting as p_apply_lifting,
    set_bc as p_set_bc,
)
from mesh_utils import generate_or_load_mesh

comm = MPI.COMM_WORLD
rank = comm.rank

# -----------------------------
# Load mesh + cell tags
# -----------------------------
msh, domain_tags = generate_or_load_mesh("magnet.geo", "magnet.xdmf")

msh.topology.create_connectivity(msh.topology.dim, msh.topology.dim)


# -----------------------------
# Function spaces
# -----------------------------
V = functionspace(msh, ("N1curl", 1))                   # H(curl) for A
V_cg1_vec = functionspace(msh, ("CG", 1, (msh.geometry.dim,)))  # CG1^3 for vis/projection
W_mat = functionspace(msh, ("DG", 0))                   # DG0 for material field

# -----------------------------
# Material constants
# -----------------------------
mu0 = 4 * np.pi * 1e-7
nu = fem.Constant(msh, PETSc.ScalarType(1.0 / mu0))     # assume air everywhere

# -----------------------------
# Magnetization: uniform +z inside magnet subdomain
# -----------------------------
magnet_tag = 2

# Use a physically meaningful magnitude (A/m). For Br≈1.1T, M≈Br/μ0 ≈ 8.75e5 A/m
Br = 1.1  # Tesla (adjust to your geometry)
Mmag = Br / mu0

# DG0 magnetization
M_space = functionspace(msh, ("CG", 1, (msh.geometry.dim,)))
M_func = fem.Function(M_space)
M_func.x.array[:] = 0.0

magnet_cells = np.where(domain_tags.values == magnet_tag)[0]
for cell in magnet_cells:
    dofs = fem.locate_dofs_topological(M_space, msh.topology.dim, [cell])
    for dof in dofs:
        M_func.x.array[dof * msh.geometry.dim + 2] = Mmag  # z
M_func.x.scatter_forward()

# Define variational problem
u = ufl.TrialFunction(V)
v = ufl.TestFunction(V)
dx = ufl.Measure("dx", domain=msh, subdomain_data=domain_tags)

a = ufl.inner(nu * ufl.curl(u), ufl.curl(v)) * dx
L = ufl.inner(M_func, ufl.curl(v)) * dx


# -----------------------------
# Boundary condition: A×n = 0 (implemented as A=0 on outer box facets)
# -----------------------------
facet_dim = msh.topology.dim - 1
box_half = 0.5  # must match your .geo

def outer_boundary(x):
    return np.any(np.isclose(np.abs(x), box_half), axis=0)

outer_facets = mesh.locate_entities_boundary(msh, facet_dim, outer_boundary)
bc_dofs = fem.locate_dofs_topological(V, facet_dim, outer_facets)
bc_func = Function(V)
bc_func.x.array[:] = 0.0
#bc = fem.dirichletbc(bc_func, bc_dofs)

# -----------------------------
# Helpers
# -----------------------------
def assemble_system(a_ufl, L_ufl, bcs=None, name="System"):
    """Assemble PETSc matrix and vector with BCs (dolfinx.fem.petsc backend)."""
    if rank == 0:
        print(f"{name}: Assembling system")

    a_form = fem.form(a_ufl)
    L_form = fem.form(L_ufl)

    A = p_assemble_matrix(a_form, bcs=bcs)
    A.assemble()

    b = p_assemble_vector(L_form)
    if bcs:
        p_apply_lifting(b, [a_form], [bcs])
        b.ghostUpdate(addv=PETSc.InsertMode.ADD, mode=PETSc.ScatterMode.REVERSE)
        p_set_bc(b, bcs)
    return A, b

def save_function(fnc, name, filename):
    fnc.name = name
    with io.XDMFFile(msh.comm, filename, "w") as xdmf:
        xdmf.write_mesh(msh)
        xdmf.write_function(fnc)
    if rank == 0:
        arr = fnc.x.array
        print(f"{name}: min={arr.min():.3e}, max={arr.max():.3e}")

def l2_project_to(V_proj, expr, bcs=None, label="proj"):
    """L2 projection of an expression into a target space V_proj."""
    u = ufl.TrialFunction(V_proj)
    v = ufl.TestFunction(V_proj)

    aP = ufl.inner(u, v) * ufl.dx
    LP = ufl.inner(expr, v) * ufl.dx

    a_form = fem.form(aP)
    L_form = fem.form(LP)

    A = p_assemble_matrix(a_form, bcs=bcs if bcs else [])
    A.assemble()
    b = p_assemble_vector(L_form)
    if bcs:
        p_apply_lifting(b, [a_form], [bcs])
        b.ghostUpdate(addv=PETSc.InsertMode.ADD, mode=PETSc.ScatterMode.REVERSE)
        p_set_bc(b, bcs)

    ksp = PETSc.KSP().create(A.getComm())
    ksp.setOperators(A)
    ksp.setType("cg")
    ksp.getPC().setType("jacobi")

    uh = fem.Function(V_proj)
    ksp.solve(b, uh.x.petsc_vec)
    uh.x.scatter_forward()
    return uh



# -----------------------------
# Assemble and solve for A
# -----------------------------
A_mat, b_vec = assemble_system(a, L, bcs=[], name="A_system")

solver = PETSc.KSP().create(comm)
solver.setOperators(A_mat)
solver.setType("minres")              
solver.getPC().setType("hypre")       
solver.getPC().setHYPREType("boomeramg")
solver.setTolerances(rtol=1e-8, max_it=5000)
solver.setFromOptions()


A_sol = Function(V)
solver.solve(b_vec, A_sol.x.petsc_vec)
A_sol.x.scatter_forward()

if rank == 0:
    print("Solved A. H(curl) DOFs:", V.dofmap.index_map.size_global)

# -----------------------------
# Interpolate A to CG1^3 for visualization (optional)
# -----------------------------
A_vis = Function(V_cg1_vec)
A_expr = Expression(A_sol, V_cg1_vec.element.interpolation_points())
A_vis.interpolate(A_expr)
A_vis.x.scatter_forward()

# -----------------------------
# Compute B = curl(A) and L2-project to CG1^3 for ParaView
# -----------------------------
B_vis = l2_project_to(V_cg1_vec, ufl.curl(A_sol), label="B_from_A")

# -----------------------------
# Material field as DG0 (cell tags)
# -----------------------------
mat = Function(W_mat)
mat.x.array[:] = domain_tags.values
mat.x.scatter_forward()

# -----------------------------
# Write outputs
# -----------------------------
for f in (
    "A_field.xdmf",
    "A_field.xdmf.h5",
    "B_from_A.xdmf",
    "B_from_A.xdmf.h5",
    "material_field.xdmf",
    "material_field.xdmf.h5",
):
    try:
        os.remove(f)
    except FileNotFoundError:
        pass

save_function(A_vis, "A_field", "A_field.xdmf")
save_function(B_vis, "B_field", "B_from_A.xdmf")
save_function(mat, "Material_field", "material_field.xdmf")

if rank == 0:
    print("All outputs written for ParaView.")

I’ve tried debugging by solving with null boundary conditions, increasing solver tolerence, changing geometries, not much seems to work.

Any help would be appreciated.

Your code is not executable and hence it is difficult to assess what goes wrong. Below I have worked out a simple model of the same problem. I have used a direct solver which works because the source term is tested with `\nabla\times\mathbf{\tilde{A}}`. If instead the source is tested with `\mathbf{\tilde{A}}` as in coils then be sure to use an iterative solver or explicitly fix the gauge.

import gmsh, dolfinx, petsc4py, mpi4py, ufl
from dolfinx.io import gmshio
import numpy as np
import dolfinx.fem.petsc
from time import time

mpi_rank = mpi4py.MPI.COMM_WORLD.rank

gmsh.initialize()
gmsh.clear()
gmsh.option.setNumber("General.Terminal", 0) # disable output messages
gmsh.clear()

gdim = 3
occ = gmsh.model.occ

cyl = occ.addCylinder(0, 0, -0.5, 0, 0, 1, 0.5)
fov = occ.addCylinder(0, 0, -1, 0, 0, 2, 1)

frag, _ = occ.fragment([(gdim, fov)], [(gdim, cyl)])
occ.synchronize()

# add_sub physical tags
all_doms = occ.getEntities(gdim)
for j, dom in enumerate(all_doms):
    gmsh.model.addPhysicalGroup(dom[0], [dom[1]], j + 1)  # create the main group/node

# number all boundaries
all_edges = gmsh.model.getEntities(gdim - 1)
for j, edge in enumerate(all_edges):
    gmsh.model.addPhysicalGroup(edge[0], [edge[1]], edge[1])  # create the main group/node

gmsh.model.mesh.generate()
gmsh.model.mesh.refine()
gmsh.model.mesh.optimize("Netgen")

mesh, ct, ft = gmshio.model_to_mesh(gmsh.model, mpi4py.MPI.COMM_WORLD, mpi_rank, gdim)
# by inspection, magnet is domain 1 and air/fov around it is domain 2

fem_order = 2
Omega = dolfinx.fem.functionspace(mesh, ("N1curl", fem_order))
A, At = ufl.TrialFunction(Omega), ufl.TestFunction(Omega)

Omega_dg = dolfinx.fem.functionspace(mesh, ("DG", 0))
mur_param = dolfinx.fem.Function(Omega_dg)
mur_param.x.array[:] = 1.0
mur_param.x.array[ct.find(1)] = 1.05 # recoil permeability of the magnet

dx = ufl.Measure("dx", domain=mesh, subdomain_data=ct)

Brem_ufl = ufl.as_vector((0, 0, 1))

F = ufl.inner(ufl.inv(mur_param)*ufl.curl(A), ufl.curl(At)) * dx
F -= ufl.inner(ufl.inv(mur_param)*Brem_ufl, ufl.curl(At)) * dx(1)

# nxA = 0 boundary condition
dbc = []
exterior_elems = dolfinx.mesh.exterior_facet_indices(mesh.topology)
exterior_dofs = dolfinx.fem.locate_dofs_topological(Omega, gdim-1, exterior_elems)
A_bnd = dolfinx.fem.Function(Omega)
dbc.append(dolfinx.fem.dirichletbc(A_bnd, exterior_dofs))

if mpi_rank == 0:
    print("Solved A. H(curl) DOFs:", Omega.dofmap.index_map.size_global)

jit_options = {"cffi_extra_compile_args": ["-Ofast", "-march=native"], #-Ofast
               "cffi_libraries": ["m"]}

a_form = dolfinx.fem.form(ufl.lhs(F), jit_options=jit_options)
L_form = dolfinx.fem.form(ufl.rhs(F))

A_mat = dolfinx.fem.petsc.create_matrix(a_form)
A_mat.zeroEntries()
dolfinx.fem.petsc.assemble_matrix(A_mat, a_form, bcs=dbc)
A_mat.assemble()

b = dolfinx.fem.petsc.create_vector(L_form)
dolfinx.fem.petsc.assemble_vector(b, L_form)
dolfinx.fem.petsc.apply_lifting(b, [a_form], bcs=[dbc])
b.ghostUpdate(petsc4py.PETSc.InsertMode.INSERT_VALUES, petsc4py.PETSc.ScatterMode.REVERSE)

A_sol = dolfinx.fem.Function(Omega)

t = time()
ksp = petsc4py.PETSc.KSP().create(mesh.comm)
ksp.setOperators(A_mat)
ksp.setType(petsc4py.PETSc.KSP.Type.PREONLY)
ksp.getPC().setType(petsc4py.PETSc.PC.Type.LU)
ksp.getPC().setFactorSolverType("mumps")
ksp.solve(b, A_sol.x.petsc_vec)
print("Solve time: {}".format(time() - t))

Omega_B_dg = dolfinx.fem.functionspace(mesh, ("DG", fem_order-1, (mesh.geometry.dim,)))
B_expr = dolfinx.fem.Expression(ufl.curl(A_sol), Omega_B_dg.element.interpolation_points())
B_sol = dolfinx.fem.Function(Omega_B_dg)
B_sol.interpolate(B_expr)

# with dolfinx.io.VTXWriter(mpi4py.MPI.COMM_WORLD, "results/A_field.bp", [A_sol], engine="BP4") as vtx:
#     vtx.write(0.0)

with dolfinx.io.VTXWriter(mpi4py.MPI.COMM_WORLD, "results/B_field.bp", [B_sol], engine="BP4") as vtx:
    vtx.write(0.0)
2 Likes