Using dolfinx_mpc in generalised plain strain formulation

Hello, I wish to implement create_slip_constraint from dolfinx_mpc on the generalized plane strain case. The left and bottom edges are allowed to slide tangentially but not normally.

I am stuck in the part dolfinx.fem.petsc.assemble_matrix_block implementation with mpc. A guidance will be quite helpful. Here is my code below.

import gmsh
from mpi4py import MPI
from petsc4py import PETSc
import ufl
from dolfinx import fem, io, plot
import dolfinx.fem.petsc
from dolfinx.cpp.la.petsc import get_local_vectors
from scifem import create_real_functionspace
from dolfinx_mpc.utils import (create_point_to_point_constraint, determine_closest_block, rigid_motions_nullspace, facet_normal_approximation)
from dolfinx_mpc import LinearProblem, MultiPointConstraint
hsize = 0.2

Re = 11.0
Ri = 9.0

gmsh.initialize()
gmsh.option.setNumber("General.Terminal", 0)  # to disable meshing info
gdim = 2
model_rank = 0
gmsh.model.add("Model")

geom = gmsh.model.geo
center = geom.add_point(0, 0, 0)
p1 = geom.add_point(Ri, 0, 0)
p2 = geom.add_point(Re, 0, 0)
p3 = geom.add_point(0, Re, 0)
p4 = geom.add_point(0, Ri, 0)

x_radius = geom.add_line(p1, p2)
outer_circ = geom.add_circle_arc(p2, center, p3)
y_radius = geom.add_line(p3, p4)
inner_circ = geom.add_circle_arc(p4, center, p1)

boundary = geom.add_curve_loop([x_radius, outer_circ, y_radius, inner_circ])
surf = geom.add_plane_surface([boundary])

geom.synchronize()

gmsh.option.setNumber("Mesh.CharacteristicLengthMin", hsize)
gmsh.option.setNumber("Mesh.CharacteristicLengthMax", hsize)

gmsh.model.addPhysicalGroup(gdim, [surf], 1)
gmsh.model.addPhysicalGroup(gdim - 1, [x_radius], 1, name="bottom")
gmsh.model.addPhysicalGroup(gdim - 1, [y_radius], 2, name="left")
gmsh.model.addPhysicalGroup(gdim - 1, [outer_circ], 3, name="outer")


gmsh.model.mesh.generate(gdim)

domain, _, facets = io.gmshio.model_to_mesh(
    gmsh.model, MPI.COMM_WORLD, model_rank, gdim=gdim
)
gmsh.finalize()
def eps(v, ezz):
    return ufl.sym(
        ufl.as_tensor(
            [
                [v[0].dx(0), v[0].dx(1), 0],
                [v[1].dx(0), v[1].dx(1), 0],
                [0, 0, ezz],
            ]
        )
    )


E = fem.Constant(domain, 1e5)
nu = fem.Constant(domain, 0.3)
mu = E / 2 / (1 + nu)
lmbda = E * nu / (1 + nu) / (1 - 2 * nu)


def sigma(v, ezz):
    return lmbda * ufl.tr(eps(v, ezz)) * ufl.Identity(3) + 2.0 * mu * eps(v, ezz)


def eps(v, ezz):
    return ufl.sym(
        ufl.as_tensor(
            [
                [v[0].dx(0), v[0].dx(1), 0],
                [v[1].dx(0), v[1].dx(1), 0],
                [0, 0, ezz],
            ]
        )
    )


E = fem.Constant(domain, 1e5)
nu = fem.Constant(domain, 0.3)
mu = E / 2 / (1 + nu)
lmbda = E * nu / (1 + nu) / (1 - 2 * nu)


def sigma(v, ezz):
    return lmbda * ufl.tr(eps(v, ezz)) * ufl.Identity(3) + 2.0 * mu * eps(v, ezz)


# Define Function Space
degree = 2
V = fem.functionspace(domain, ("P",degree, (gdim,)))
R = create_real_functionspace(domain)
W = MixedFunctionSpace(V, R)

# Define Variational Problem
du, dezz = TrialFunctions(W)
u_, ezz_ = TestFunctions(W)


#u = fem.Function(V, name = "Displacement")
a_form = inner(sigma(du, dezz), eps(u_, ezz_)) * dx


T = fem.Constant(domain, 1000000.0)


#Self-weight on the surface
n = FacetNormal(domain)

ds = Measure("ds", domain=domain, subdomain_data=facet_markers)
dx = Measure("dx", domain=domain, subdomain_data=cell_markers)
dS = Measure("dS", domain=domain, subdomain_data=facet_markers)

L_form = dot(T*n,u_) * ds(3)

a_blocked_compiled = fem.form(extract_blocks(a_form))
L_blocked_compiled = fem.form(extract_blocks(L_form))


# Get the facet markers for boundary edges (ps1 and ps2)

mpc = MultiPointConstraint(V)

for tag in [1, 2]:  # ps1 and ps2
    normal_vec = create_normal_approximation(V, facet_markers, tag)
    mpc.create_slip_constraint(V, (facet_markers, tag), normal_vec)

mpc.finalize()



a_blocked_compiled = fem.form(ufl.extract_blocks(a_form))
L_blocked_compiled = fem.form(ufl.extract_blocks(L_form))

after this part I am stuck and need your guidance.

A = dolfinx.fem.petsc.assemble_matrix_block(a_blocked_compiled, bcs=bcs)
A.assemble()
b = dolfinx.fem.petsc.assemble_vector_block(
    L_blocked_compiled, a_blocked_compiled, bcs=bcs
)

ksp = PETSc.KSP().create(domain.comm)
ksp.setOperators(A)
ksp.setType("preonly")
pc = ksp.getPC()
pc.setType("lu")
pc.setFactorSolverType("mumps")

# solve system
xh = dolfinx.fem.petsc.create_vector_block(L_blocked_compiled)
ksp.solve(b, xh)
xh.ghostUpdate(addv=PETSc.InsertMode.INSERT, mode=PETSc.ScatterMode.FORWARD)

maps = [(Wi.dofmap.index_map, Wi.dofmap.index_map_bs) for Wi in W.ufl_sub_spaces()]
u = fem.Function(V, name="Displacement")
x_local = get_local_vectors(xh, maps)
u.x.array[: len(x_local[0])] = x_local[0]
u.x.scatter_forward()
ezz = x_local[1][0]

Block-assembly is not implemented in DOLFINx_MPC, only nest assembly:

which can use direct solvers, as illustrated in:

On the main branch of DOLFINx_MPC the nest interface has gotten a serious face-lift:

which makes it way easier to use than it was previously.

I tried implement your suggestions for this geometry but I am getting some errors. I think there is some syntax mistake. Your intervention will be helpful here. Here is the geometry and my code.

// Gmsh project created on Tue May  6 11:35:50 2025
//+
Point(1) = {0, 0.509795579104159, 0, 1.0};
//+
Point(2) = {0.195090322016128, 0.470989701299072, 0, 1.0};
//+
Point(3) = {0, 1.01959115820832, 0, 1.0};
//+
Point(4) = {0.390180644032256, 0.941979402598143, 0, 1.0};
//+
Line(1) = {1, 2};
//+
Line(2) = {2, 4};
//+
Line(3) = {4, 3};
//+
Line(4) = {3, 1};
//+
Curve Loop(1) = {4, 1, 2, 3};
//+
Surface(1) = {1};
//+
Physical Curve("ps1", 5) = {2};
//+
Physical Curve("ps2", 6) = {4};
//+
Physical Curve("bottom", 7) = {1};
//+
Physical Curve("top", 8) = {3};
//+
Physical Surface("my_surf", 9) = {1};
//+
Show "*";

my code

import gmsh
import os
import math
import numpy as np
import matplotlib.pyplot as plt
import sys
from mpi4py import MPI
from petsc4py import PETSc
from dolfinx.cpp.mesh import to_type, cell_entity_type
from dolfinx import (mesh, fem, io, common, default_scalar_type, geometry, plot)
from dolfinx.fem.petsc import apply_lifting, assemble_matrix, assemble_vector, set_bc
from dolfinx.io import VTXWriter, distribute_entity_data, gmshio
from dolfinx.mesh import create_mesh, meshtags_from_entities
from dolfinx.plot import vtk_mesh
from dolfinx.io import XDMFFile, VTKFile
from ufl import (FacetNormal, as_matrix, as_tensor, extract_blocks, Identity, Measure, TestFunctions, tr, TrialFunctions, as_vector, div, dot, ds, dx, inner, lhs, grad, nabla_grad, rhs, sym, MixedFunctionSpace)
from dolfinx_mpc.utils import (create_point_to_point_constraint, determine_closest_block, rigid_motions_nullspace, facet_normal_approximation, create_normal_approximation, gather_transformation_matrix, gather_PETScMatrix, gather_PETScVector)
from dolfinx_mpc import LinearProblem, MultiPointConstraint
from dolfinx.cpp.la.petsc import get_local_vectors
from packaging.version import Version
from dolfinx.cpp.la.petsc import scatter_local_vectors, get_local_vectors
from scifem import create_real_functionspace, assemble_scalar
from scifem.petsc import apply_lifting_and_set_bc, zero_petsc_vector

# Material properties
E = fem.Constant(domain, 200e9)       # Young's modulus
nu = fem.Constant(domain, 0.3)        # Poisson's ratio

lmbda = E * nu / ((1 + nu)*(1 - 2*nu))
mu = E / (2*(1 + nu))

# Strain with plane strain + extra ezz
def eps(u, ezz):
    return sym(as_tensor([
        [u[0].dx(0), u[0].dx(1), 0],
        [u[1].dx(0), u[1].dx(1), 0],
        [0, 0, ezz]
    ]))

def sigma(u, ezz):
    return lmbda * tr(eps(u, ezz)) * Identity(3) + 2 * mu * eps(u, ezz)


# Function spaces: V for displacement, R for scalar ezz
degree = 2
V = fem.functionspace(domain, ("P", degree, (2,)))
R = create_real_functionspace(domain)
W = MixedFunctionSpace(V, R)


# Trial and test functions
(du, dezz) = TrialFunctions(W)
(v, vezz) = TestFunctions(W)

zero_vec = fem.Constant(domain, PETSc.ScalarType((0.0, 0.0)))

# Variational forms
a00 = inner(sigma(du, 0), eps(v, 0)) * dx
a01 = inner(sigma(zero_vec, dezz), eps(v, 0)) * dx
a10 = inner(sigma(du, 0), eps(zero_vec, vezz)) * dx
a11 = inner(sigma(zero_vec, dezz), eps(zero_vec, vezz)) * dx


T = fem.Constant(domain, PETSc.ScalarType((0.0, 1e6)))
n = FacetNormal(domain)
L0 = inner(T, v) * ds
L1 = fem.Constant(domain, PETSc.ScalarType(0.0)) * vezz * dx


# Assemble nested matrix and vector
a = [[a00, a01], [a10, a11]]
L = [L0, L1]
a_compiled = fem.form(a)
L_compiled = fem.form(L)


# Slip constraints on selected facets (example facets 5 and 6)

mpc = MultiPointConstraint(V)

for tag in [5, 6]:  # ps1 and ps2
    normal_vec = create_normal_approximation(V, facet_markers, tag)
    mpc.create_slip_constraint(V, (facet_markers, tag), normal_vec)

mpc.finalize()

# Assemble matrix WITHOUT mpc argument
A = assemble_matrix(a_compiled, bcs=[])
A.assemble()

# Assemble RHS vector
b = assemble_vector(L_compiled)
apply_lifting(b, [a_compiled], bcs=[])
set_bc(b, [])
b.ghostUpdate(addv=PETSc.InsertMode.ADD, mode=PETSc.ScatterMode.REVERSE)


# Create and setup solver
ksp = PETSc.KSP().create(A.getComm())
ksp.setOperators(A)
ksp.setType("preonly")
ksp.getPC().setType("lu")
ksp.getPC().setFactorSolverType("mumps")

# Solve system
X = A.createVecRight()
ksp.solve(b, X)

# Split solution vector
x_u, x_ezz = X.getNestSubVecs()

# Distribute solution to function space
u_sol = fem.Function(mpc.function_space)
mpc.distribute_solution(x_u, u_sol.vector)

ezz_sol = x_ezz.array

Depending on your DOLFINx version, you need to:

Stable/0.9.0

  1. Load your geometry as usual (you have omitted this part of the code)
  2. Create the two function spaces V, R
  3. Create MPC on V
  4. Use the assembly procedure from dolfinx_mpc/python/demos/demo_stokes_nest.py at v0.9.2 · jorgensd/dolfinx_mpc · GitHub
  5. Create your ksp object as in: dolfinx_mpc/python/demos/demo_stokes_nest.py at v0.9.2 · jorgensd/dolfinx_mpc · GitHub

Main branch
Step 1-3 are the same.
4-5. Use dolfinx_mpc.LinearProblem or dolfinx_mpc.NonLinearProblem as described in: dolfinx_mpc/python/demos/demo_stokes_nonlinear_nest.py at main · jorgensd/dolfinx_mpc · GitHub

Hello @dokken, I have some doubts regarding this topic. I want to combine the “lagrange multipliers” from the real spaces (scifem package) with the periodic boundary conditions (dolfinx_mpc package). I have seen that in some recent posts (Non-linear blocked newton solver with real spaces #93) you managed to make it work for a non-linear problem but using block-assembly. Can this be done with nest assembly since

Block-assembly is not implemented in DOLFINx_MPC, only nest assembly

?
Thank you in advance.

Yes, it can be done. One has to make a custom implementation of the assemble_residual required by the Newton-solver, but it is possible.

Ok, thank you very much for the information. So, to make it clear, I should combine the assemble_residual_mpc from dolfinx_mpc/problem.py with the _assemble_residual of scifem/solvers.py, right?