Hello everyone! I have been working recently in a magneto-mechanical problem in which I have several permanent magnets pointing to a solid shere (partially hollow, for BC imposition) which is sensible to magnetic stimuli. The whole scenario, including the magnets, the solid and the surrounding air, has been meshed and tagged. I wanted to tackle the problem in the most efficient way, so I decided to use submeshes, following the guidelines given in Coupling PDEs of multiple dimensions. Therefore, the whole scenario would be the parent_mesh, where the magnetic potential field is computed; the solid_submesh, where the displacement field is solved coupled to the magnetic potential field, and the inner_solid_surface_submesh, where a lagrange multiplier is imposed to force radial-only displacements.
The issue is that NonlinearProblem requires a list of entity_maps that map from the submeshes to the parent mesh and the nested submesh maps from the inner_solid_surface_submesh to the solid_submesh. I have tried to create en entity_map with “dolfinx.mesh.entity_map” that maps from inner_solid_surface_submesh to parent_mesh, but that function does not create the cpp object required for the forms compilation. Is there a way to fix this?
Here is a MWE (still lengthy due to multiphysics coupling):
from dolfinx import fem, default_scalar_type
import dolfinx.fem.petsc
import dolfinx
from dolfinx.common import Timer
from dolfinx.io.gmsh import model_to_mesh
import scifem
import ufl
from mpi4py import MPI
import gmsh
import numpy as np
def get_entity_map(entity_map: dolfinx.mesh.EntityMap, inverse: bool = False) -> np.typing.NDArray[np.int32]:
"""Get an entity map from the sub-topology to the topology.
Args:
entity_map: An `EntityMap` object or a numpy array representing the mapping.
inverse: If `True`, return the inverse mapping.
Returns:
Mapped indices of entities.
"""
sub_top = entity_map.sub_topology
assert isinstance(sub_top, dolfinx.mesh.Topology)
sub_map = sub_top.index_map(entity_map.dim)
indices = np.arange(sub_map.size_local + sub_map.num_ghosts, dtype=np.int32)
return entity_map.sub_topology_to_topology(indices, inverse=inverse)
def transfer_meshtags_to_submesh(
mesh: dolfinx.mesh.Mesh,
entity_tag: dolfinx.mesh.MeshTags,
submesh: dolfinx.mesh.Mesh,
vertex_entity_map: dolfinx.mesh.EntityMap,
cell_entity_map: dolfinx.mesh.EntityMap,
) -> tuple[dolfinx.mesh.MeshTags, np.typing.NDArray[np.int32]]:
"""
Transfer a meshtag from a parent mesh to a sub-mesh.
Args:
mesh: Mesh containing the meshtags
entity_tag: The meshtags object to transfer
submesh: The submesh to transfer the `entity_tag` to
sub_to_vertex_map: Map from each vertex in `submesh` to the corresponding
vertex in the `mesh`
sub_cell_to_parent: Map from each cell in the `submesh` to the corresponding
entity in the `mesh`
Returns:
The entity tag defined on the submesh, and a map from the entities in the
`submesh` to the entities in the `mesh`.
"""
sub_cell_to_parent = get_entity_map(cell_entity_map, inverse=False)
sub_vertex_to_parent = get_entity_map(vertex_entity_map, inverse=False)
num_cells = (
mesh.topology.index_map(mesh.topology.dim).size_local + mesh.topology.index_map(mesh.topology.dim).num_ghosts
)
mesh_to_submesh = np.full(num_cells, -1, dtype=np.int32)
mesh_to_submesh[sub_cell_to_parent] = np.arange(len(sub_cell_to_parent), dtype=np.int32)
submesh.topology.create_connectivity(entity_tag.dim, 0)
num_child_entities = (
submesh.topology.index_map(entity_tag.dim).size_local + submesh.topology.index_map(entity_tag.dim).num_ghosts
)
submesh.topology.create_connectivity(submesh.topology.dim, entity_tag.dim)
c_c_to_e = submesh.topology.connectivity(submesh.topology.dim, entity_tag.dim)
c_e_to_v = submesh.topology.connectivity(entity_tag.dim, 0)
child_markers = np.full(num_child_entities, 0, dtype=np.int32)
mesh.topology.create_connectivity(entity_tag.dim, 0)
mesh.topology.create_connectivity(entity_tag.dim, mesh.topology.dim)
p_f_to_v = mesh.topology.connectivity(entity_tag.dim, 0)
p_f_to_c = mesh.topology.connectivity(entity_tag.dim, mesh.topology.dim)
sub_to_parent_entity_map = np.full(num_child_entities, -1, dtype=np.int32)
for facet, value in zip(entity_tag.indices, entity_tag.values):
facet_found = False
for cell in p_f_to_c.links(facet):
if facet_found:
break
if (child_cell := mesh_to_submesh[cell]) != -1:
for child_facet in c_c_to_e.links(child_cell):
child_vertices = c_e_to_v.links(child_facet)
child_vertices_as_parent = sub_vertex_to_parent[child_vertices]
is_facet = np.isin(child_vertices_as_parent, p_f_to_v.links(facet)).all()
if is_facet:
child_markers[child_facet] = value
facet_found = True
sub_to_parent_entity_map[child_facet] = facet
tags = dolfinx.mesh.meshtags(
submesh,
entity_tag.dim,
np.arange(num_child_entities, dtype=np.int32),
child_markers,
)
tags.name = entity_tag.name
return tags, sub_to_parent_entity_map
petsc_options = {
"snes_type": "newtonls",
"snes_linesearch_type": "basic",
"ksp_type": "preonly",
"pc_type": "lu",
"pc_factor_mat_solver_type": "mumps",
"snes_atol": 0.0,
"snes_rtol": 1e-12,
"snes_max_it": 100,
"ksp_error_if_not_converged": True,
"snes_error_if_not_converged": True,
"snes_monitor": None,
}
def create_mesh(
comm: MPI.Comm,
order: int = 1,
rank: int = 0,
partitioner = None,
solid_radius: float = 0.5
) -> dolfinx.mesh.Mesh:
if comm.rank == rank:
gmsh.initialize()
gmsh.model.add("Model")
box = gmsh.model.occ.addBox(-5,-5,-5, 10,10,10)
outer_sphere = gmsh.model.occ.addSphere(0,0,0, solid_radius)
inner_sphere = gmsh.model.occ.addSphere(0,0,0, 0.6*solid_radius)
gmsh.model.occ.synchronize()
air = gmsh.model.occ.cut([(3, box)], [(3, outer_sphere)], removeObject=True, removeTool=False)
hollow_solid = gmsh.model.occ.cut([(3, outer_sphere)], [(3, inner_sphere)], removeObject=True, removeTool=False)
volume_PG = {"air": [box, inner_sphere],
"solid": outer_sphere}
gmsh.model.occ.synchronize()
box_surfs = gmsh.model.get_boundary([(3, box)], oriented=False)
box_surfs_bb = [bbs[3]-bbs[0] for bbs in (gmsh.model.get_bounding_box(*inds) for inds in box_surfs)]
top_surf = np.array(box_surfs)[np.array(box_surfs_bb) <= 1e-6][:,1]
surface_PG = {"box_surface": top_surf,
"solid_inner": gmsh.model.get_boundary([(3, inner_sphere)], oriented=False)[0][1]}
for i, (name, value) in zip(range(len(volume_PG)), volume_PG.items()):
gmsh.model.addPhysicalGroup(3, np.unique(value), name=name)
for name, value in surface_PG.items():
gmsh.model.addPhysicalGroup(2, np.unique(value), name=name)
gmsh.model.occ.synchronize()
ConstantField = gmsh.model.mesh.field.add("MathEval")
general_value = 2.0
inside_solid = solid_radius/3.0
gmsh.model.mesh.field.setString(ConstantField, "F", f"{general_value}")
solidField = gmsh.model.mesh.field.add("Constant")
gmsh.model.mesh.field.setNumber(solidField, "VIn", inside_solid)
gmsh.model.mesh.field.setNumber(solidField, "VOut", general_value)
gmsh.model.mesh.field.setNumbers(solidField, "VolumesList", [volume_PG["solid"]])
DistanceField = gmsh.model.mesh.field.add("Distance")
distance_list = gmsh.model.get_boundary([(3, volume_PG["solid"])], oriented=False)
distance_list = [dimtag[1] for dimtag in distance_list]
gmsh.model.mesh.field.setNumbers(DistanceField, "SurfacesList", distance_list)
gmsh.model.mesh.field.setNumber(DistanceField, "Sampling", 100)
ThresholdField = gmsh.model.mesh.field.add("Threshold")
gmsh.model.mesh.field.setNumber(ThresholdField, "InField", DistanceField)
gmsh.model.mesh.field.setNumber(ThresholdField, "SizeMin", inside_solid)
gmsh.model.mesh.field.setNumber(ThresholdField, "SizeMax", general_value)
gmsh.model.mesh.field.setNumber(ThresholdField, "DistMin", 0.0)
gmsh.model.mesh.field.setNumber(ThresholdField, "DistMax", 10*solid_radius)
FieldsRefineMesh = [ConstantField, solidField, ThresholdField]
BackgroundMesh = gmsh.model.mesh.field.add("Min")
gmsh.model.mesh.field.setNumbers(BackgroundMesh, "FieldsList", FieldsRefineMesh)
gmsh.model.mesh.field.setAsBackgroundMesh(BackgroundMesh)
gmsh.option.setNumber("Mesh.Algorithm", 6)
gmsh.option.setNumber("Mesh.Algorithm3D", 1)
gmsh.option.setNumber("Mesh.ElementOrder", order)
gmsh.option.setNumber("Mesh.MeshSizeFromCurvature", 1)
gmsh.option.setNumber("Mesh.MeshSizeExtendFromBoundary", 2)
gmsh.option.setNumber("Mesh.HighOrderOptimize", 2)
gmsh.option.setNumber("Mesh.ColorCarousel", 2)
gmsh.option.setNumber("General.Verbosity", 0)
gmsh.model.mesh.removeDuplicateNodes()
gmsh.model.mesh.generate(3)
mesh_data = model_to_mesh(gmsh.model, comm, rank, gdim=3, partitioner=partitioner)
gmsh.finalize()
else:
mesh_data = model_to_mesh(gmsh.model, comm, rank, gdim=3, partitioner=partitioner)
return mesh_data, mesh_data.mesh, mesh_data.physical_groups, mesh_data.cell_tags, mesh_data.facet_tags
def assemble_Jacobian_nest(form, funcs, W):
trials = ufl.TrialFunctions(W)
Jacobian = []
for form_i in form:
Jacobian_i = []
for func_j, trial_j in zip(funcs, trials):
Jacobian_i += [ufl.derivative(form_i, func_j, trial_j)]
Jacobian += [Jacobian_i]
return Jacobian
def main():
####### Problem definition #######
mesh_comm = MPI.COMM_WORLD
model_rank = MPI.COMM_WORLD.rank
TOLERANCE = float(np.finfo(default_scalar_type).resolution)
# Mesh generation
with Timer("Mesh generation"):
print('\nCREATING MESH...')
file_data, domain, physicalGroups, cell_markers, facet_markers = create_mesh(mesh_comm, 2, model_rank, solid_radius=0.5)
domain_s, cell_map_s, vertex_map_s, node_map_s = \
dolfinx.mesh.create_submesh(domain, domain.topology.dim, cell_markers.find(physicalGroups["solid"].tag))
solid_tags, solid_map = transfer_meshtags_to_submesh(domain, facet_markers, domain_s, vertex_map_s, cell_map_s)
domain_inter, cell_map_inter, vertex_map_inter, node_map_inter = \
dolfinx.mesh.create_submesh(domain_s, domain_s.topology.dim-1, solid_tags.find(physicalGroups["solid_inner"].tag))
inter_to_domain_map = dolfinx.mesh.entity_map(domain.topology, domain_inter.topology, domain_inter.topology.dim, get_entity_map(cell_map_inter, inverse=False))
quadrature_degree = 4
dx = ufl.Measure(integral_type='dx',
domain=domain,
subdomain_data=cell_markers,
metadata={'quadrature_degree': quadrature_degree})
ds = ufl.Measure("ds",
domain=domain,
subdomain_data=facet_markers,
metadata={'quadrature_degree': quadrature_degree})
####### Function spaces and functions #######
# Create function spaces
with Timer("Function spaces"):
Vu = fem.functionspace(domain_s, ("Lagrange", 2, (domain.geometry.dim,)))
Vv = fem.functionspace(domain, ("Lagrange", 2))
Vlm = fem.functionspace(domain_inter, ("Lagrange", 2, (domain.geometry.dim,)))
Vlmu = scifem.create_real_functionspace(domain)
W = ufl.MixedFunctionSpace(Vu, Vv, Vlm, Vlmu)
# Functions:
(du, dv, dlm, dlmu) = ufl.TestFunctions(W)
u = fem.Function(Vu, name='displacement')
v = fem.Function(Vv, name='mag potential')
lm = fem.Function(Vlm, name='lm')
lmu = fem.Function(Vlmu, name='lm_u')
funcs = [u, v, lm, lmu]
x = ufl.SpatialCoordinate(domain)
####### Boundary conditions #######
bcs = []
####### Formulation coupled problem #######
# Define derived fields
I = ufl.variable(ufl.Identity(3))
F = ufl.variable(ufl.grad(u) + I)
Finv = ufl.inv(F)
C = ufl.dot(F.T, F)
J = ufl.det(F)
I1c = (J**(-2/3))*ufl.tr(C)
H = -ufl.grad(v)
mu = fem.Constant(domain_s, 2000.0)
k = fem.Constant(domain_s, 1e5)
psi_solid = (mu/2) * (I1c - 3) + (k/2) * (ufl.ln(J))**2
# Mechanical term
mechanical = ufl.diff(psi_solid, F)
# Maxwell term
mu0 = fem.Constant(domain, 4*np.pi*1e-7)
I7 = ufl.inner(ufl.outer(H, H), ufl.dot(Finv, Finv.T))
dI7dF = -2 * ufl.outer(ufl.dot(Finv.T, H), ufl.dot(ufl.dot(H, Finv), Finv.T))
maxwell = -(mu0/2) * J * (I7*(Finv.T) + dI7dF)
# Magnetization term
mur = fem.Constant(domain, 1.2)
ms = fem.Constant(domain, 2.4)
chi = mur - 1 + TOLERANCE
I7_sqrt = ufl.sqrt(I7 + TOLERANCE)
term = ((3*chi)/ms) * I7_sqrt
magnetization = -((mu0*(ms**2))/(3*chi)) * J * (Finv.T) * (ufl.ln(ufl.sinh(term)) - ufl.ln(term)) +\
-((mu0*ms)/2) * J * ((1/ufl.tanh(term))-(1/term)) * (1/I7_sqrt) * dI7dF
# Total
Psolid = mechanical + maxwell + magnetization
form_ = ufl.inner(Psolid, ufl.grad(du)) * dx(physicalGroups["solid"].tag)\
+ ufl.inner(ufl.cross(x, u), dlm) * ds(physicalGroups["solid_inner"].tag)\
+ ufl.inner(ufl.cross(x, lm), du) * ds(physicalGroups["solid_inner"].tag)\
+ ufl.inner(v, dlmu) * dx\
+ ufl.inner(lmu, dv) * dx
B_dict = {}
for key, value in physicalGroups.items():
if value.dim != 3:
continue
elif key.find("air") != -1:
B_term = mu0 * H
elif key.find("solid") != -1:
# Maxwell term
dI7dH = 2 * ufl.dot(ufl.dot(Finv, Finv.T), H)
maxwell_B = (mu0/2) * J * dI7dH
# Magnetization term (Mukherjee 2020)
magnetization_B = ((mu0*ms)/2) * J * ((1/ufl.tanh(term))-(1/term)) * (1/I7_sqrt) * dI7dH
B_term = maxwell_B + magnetization_B
else:
raise ValueError
B_dict[key] = B_term
form_ += - ufl.dot(B_term, ufl.grad(dv)) * dx(value.tag)
Bimposed = fem.Constant(domain, np.array([2.0, 0.0, 0.0], dtype=default_scalar_type))
form_ += - ufl.inner(Bimposed, ufl.grad(dv)) * ds(physicalGroups["box_surface"].tag)
# EXTRACTING RESIDUAL AND JACOBIAN:
residual = ufl.extract_blocks(form_)
if model_rank == 0:
print(f"- Residual extracted ({len(residual)} blocks)")
Jacobian = assemble_Jacobian_nest(residual, funcs, W)
# SET UP NONLINEAR PROBLEM:
solver_fsi = dolfinx.fem.petsc.NonlinearProblem(residual,
u=funcs,
J=Jacobian,
kind='nest',
bcs=bcs,
entity_maps=[cell_map_s, inter_to_domain_map],
petsc_options_prefix="Example_",
petsc_options=petsc_options,
)
print('\nSTARTING SIMULATION:')
with Timer("solving"):
solver_fsi.solve()
if __name__ == "__main__":
main()
The error is due to the “artificial” entity_map and it is the following:
AttributeError: 'dolfinx.cpp.mesh.EntityMap' object has no attribute '_cpp_object'
Note: Instead of using lagrange multipliers, one could use dolfinx_mpc for imposing no-slip condition on two tangential directions to the surface, needing only the solid_submesh without the nested surface submesh, but I’m getting a “core dumped” error (need to test it deeper).