Dolfinx mpc overlapping constraints

Hi,

I would like to have a set of mixed periodic conditions, for some I use create_periodic_constraint_geometrical, for others create_periodic_constraint_topological. Doing this, there could be issues when the dofs where the periodicity is applied overlap, as already discussed in https://fenicsproject.discourse.group/t/handling-contraints-with-dolfinx-mpc/9886/2 . Would it be possible to have a method just to discard the slaves dofs that also appear as master right before finalizing the mpc (e.g. removing them from slaves and adjusting offsets and coeffs) rather then trying to find the implicit constraint as in https://fenicsproject.discourse.group/t/handling-contraints-with-dolfinx-mpc/9886/2 ?

Mattia

It would be possible to implement such a method, but note that there are many corner cases for such problems, and therefore it is hard to create a generalized version that will work for every potential combination.

As DOLFINx_MPC (during the finalize call, creates the proper data structures, you could make a custom finalize that does the additional analysis of discarding slaves that are also masters (i.e. modify the self._.... objects before calling the lines after: dolfinx_mpc/python/src/dolfinx_mpc/multipointconstraint.py at main · jorgensd/dolfinx_mpc · GitHub)

Yes, for the moment I did as you said, obviously this custom method depends on the implementation details of dolfinx_mpc and it is going to break if there will be changes in the logic. For such case where a dof is a master and slave at the same time, would it make sense to have a warning?

The problem with that is you need a sync across all MPI processes to check this, as a degree of freedom can be a master on another process than the one that originally owns it (and wants to use it as a ghost).
It would mean keeping a lookup array (num_dofs_global in size) on every process, which does not scale.

Ok, then it’s better to try to impose the periodic conditions on non-overlapping dofs from the beginning. To make this easier in my case, I would need to know if in create_contact_inelastic_condition api, it would be possible to optionally take in a rototranslation matrix to apply to the slave_marker dofs coords before finding the collisions with the master_marker ones. I am using this condition to do a magnetostatic 2d of a sector (like 2pi/8) of an electric motor where I want to apply the rotation to the rotor and ensure potential continuity through the contact periodicity (the mesh is non conformal at the airgap, as in Dolfinx_mpc on a curved interface - #4 by dokken ), while on the boundaries I have periodic or antiperiodic conditions, and dirichlet Az=0 on the outer stator boundary.

Would that be possible by using dolfinx_mpc — DOLFINx-MPC: An extension to DOLFINx for multi point constraints ?

I guess not, given a rotation angle, rotor and stator will be in contact just for a portion of the line arc and this portion does not have a tag. Right now I am implementing manually the rotation on the mesh.geometry.x for the rotor nodes, call contact_inelastic with allow_missing_master=True, then rotate back the mesh.geometry.x coords to the original position.

Instead would create_periodic_geometrical work in this case even if master and slave dofs lies on 2 curves that are geometrically coincident?

It might work (i don’t remember if I added a check to exclude its own dof when searching).
If it excludes its own dof as a potential candidate it should work (you could test and let me know, and then I’ll see if I can fix it if it is broken).

@dokken , here the results of the test I made. I think in this way it is not easy to identify the slaves from the masters with a geometric criteria. You can find here the code I am playing with (for simplicity, I have formulated the problem as 2 squares that are sliding rather than 2 concentric circles that rotates).

This is the mesh generation through gmsh:

import gmsh

gmsh.initialize()
gmsh.option.setNumber("General.Terminal", 1)
gmsh.model.add("2squares")

r = 0.1
cx, cy = 0.5, 0.6

# Create base surfaces
rect_left  = gmsh.model.occ.addRectangle(0, 0, 0, 1, 1)
disk       = gmsh.model.occ.addDisk(cx, cy, 0.0, r, r)
rect_right = gmsh.model.occ.addRectangle(1, 0, 0, 1, 1)

gmsh.model.occ.synchronize()

# Fragment: object = left rect, tools = disk + right rect
outDimTags, outMap = gmsh.model.occ.fragment([(2, rect_left)], [(2, disk)])
gmsh.model.occ.synchronize()


for i, (dim, ctag) in enumerate(gmsh.model.getEntities(2), start=1):
     pg = gmsh.model.addPhysicalGroup(2, [ctag], tag=i)
     gmsh.model.setPhysicalName(2, pg, f"surf_{ctag}")

for i, (dim, ctag) in enumerate(gmsh.model.getEntities(1), start=1):
     pg = gmsh.model.addPhysicalGroup(1, [ctag], tag=100 + i)
     gmsh.model.setPhysicalName(1, pg, f"line_{ctag}")
     
# Mesh and write
# Keep left-square sizes unchanged; refine only in the right square.
def right_square_refine(dim, tag, x, y, z, lc):
    if x > 1.0 and 0.0 <= y <= 1.0:
        return min(lc, 0.02)
    return lc

#make right square mesh finer to see the non conformality
field = gmsh.model.mesh.field.add("Constant")
gmsh.model.mesh.field.setNumber(field, "VIn", 0.19)   # size inside selected entities
gmsh.model.mesh.field.setNumbers(field, "SurfacesList", [rect_right])
gmsh.model.mesh.field.setAsBackgroundMesh(field)

gmsh.model.mesh.generate(2)
gmsh.model.mesh.refine()
gmsh.write("2squares.msh")
gmsh.finalize()

While this is the implementation in dolfinx:

import dolfinx
import dolfinx_mpc
from dolfinx.io import gmsh
import numpy as np
import pyvista
import ufl
from mpi4py import MPI

#cell tags are -> 1: disk, 2 right square, 3 left square
mesh,ct,ft,*_ = gmsh.read_from_msh("2squares.msh",MPI.COMM_WORLD,0)
dx = ufl.Measure("dx", domain=mesh, subdomain_data=ct)
disk = 1
right_square = 2
left_square = 3
left_square_contact = 104
right_square_contact = 107

tdim = mesh.topology.dim
shift = 0.15
tags_to_shift = right_square
#shift upward domain with tag 2
mesh.topology.create_connectivity(tdim, 0)
c2v = mesh.topology.connectivity(tdim, 0)
mask = np.isin(ct.values, tags_to_shift)
cells = ct.indices[mask]
verts = np.unique(np.concatenate([c2v.links(c) for c in cells]))

sp = dolfinx.fem.functionspace(mesh, ("CG", 2))
nu0 = 1/(4*3.14*1e-7)
def shift_upward(x):
    x[1,:]+=1
    return x

#dirichlet bc on left edge
bc = dolfinx.fem.dirichletbc(0.0, dolfinx.fem.locate_dofs_geometrical(sp, lambda x: np.isclose(x[0], 0.0)), sp)
#mpc: antiperiodic upper and lower facets, contact between left and right square, antiperiodic between right and left edge for the unmatched parts
mpc = dolfinx_mpc.MultiPointConstraint(sp)
#antiperiodic condition on left square lower-upper edge
mpc.create_periodic_constraint_geometrical(sp, lambda x: np.bitwise_and(np.isclose(x[1], 0.0), x[0]<=1),shift_upward, [bc],scale=-1.0)
#antiperiodic condition right square lower-upper edge
mpc.create_periodic_constraint_geometrical(sp, lambda x: np.bitwise_and(np.isclose(x[1], 0.),x[0]>=1),shift_upward, [bc],scale=-1.0)
#combination of contact and antiperiodicity on the sliding face
mpc.create_periodic_constraint_geometrical(sp, lambda x: np.bitwise_and(np.isclose(x[0], 1.), x[1]<shift),shift_upward, [bc],scale=-1.0)
###
#VERSION WITH CONTACT CONDITION...
#mesh.geometry.x[verts, 1] += shift
#mpc.create_contact_inelastic_condition(ft, left_square_contact, right_square_contact, eps2=1e-11,allow_missing_masters=True)
#mesh.geometry.x[verts, 1] -= shift
#VERSION WITH GEOMETRICAL CONSTRAINTS(NOT WORKING)
mpc.create_periodic_constraint_geometrical(sp, lambda x: np.bitwise_and(np.isclose(x[0], 1.), x[1]>=shift,x[1]<=1.0), lambda x: np.vstack((x[0], x[1] - shift)), [bc],scale=1.0)

#finalize mpc
mpc.finalize()

#linear form: source inside domain disk, all materials are linear with nu0
a = nu0*ufl.inner(ufl.grad(ufl.TrialFunction(sp)), ufl.grad(ufl.TestFunction(sp)))*ufl.dx
L= 10000.0*ufl.TestFunction(sp)*dx(disk)

#solve linear problem
problem = dolfinx_mpc.LinearProblem(a,L, mpc=mpc,bcs=[bc])
sol_lin= problem.solve()


from dolfinx import plot
owned = mesh.topology.index_map(mesh.topology.dim).size_local
ghosts = mesh.topology.index_map(mesh.topology.dim).num_ghosts
topology, cell_types, geometry = plot.vtk_mesh(mpc.function_space,np.arange(owned + ghosts))
grid = pyvista.UnstructuredGrid(topology, cell_types, geometry)
grid.point_data["ulin"] = sol_lin.x.array
grid.set_active_scalars("ulin")
vmin = min(sol_lin.x.array.min(), sol_lin.x.array.min())
vmax = max(sol_lin.x.array.max(), sol_lin.x.array.max())
levels = np.linspace(vmin, vmax, 25)

plotter = pyvista.Plotter()
iso_lin = grid.contour(isosurfaces=levels, scalars="ulin")
plotter.add_mesh(grid, scalars="ulin", cmap="viridis", clim=[vmin, vmax], show_edges=False)
plotter.add_mesh(grid, style="wireframe", color="k", opacity=0.2)
plotter.add_mesh(iso_lin, color="white", line_width=2)
plotter.add_text("ulin", font_size=12)
plotter.view_xy()
plotter.show()

Using the part with #VERSION WITH CONTACT CONDITION makes the results I expect (with the hack of manual shift of mesh coords upwards and downwards to make the contact_inelastic work as I want) even if there is there is still the issues at the corners due to the multiple periodicities overapping. If I use #VERSION WITH GEOMETRICAL CONSTRAINTS, where the slaves (on left square) are mapped to the master (on right square) with a downward shift, I get an unexpected result, with a zero potential on both sides of the contact edge. I guess this happen because all dofs on the contact, i.e. the ones on left and right square, are identified as slaves with no master, and the only thing that is making a nonzero solution on the right square is the antiperiodicity on the vertical noncontact edges.