Mapping of results to mesh nodes, debugging of dolfinx implentation of shell formulation

Hi, all. As I’ve mentioned elsewhere, I have been (slowly) working on implementing this linear shell model from @bleyerj in Dolfinx. I’m working via docker with Dolfinx version 0.3.1.0, git commit fdb1d1871945e308db07f94175fe3dd08033be5f. I have a code that runs, but the results are NaN at present (I will share the mesh geometry if there is a way to upload these non-image files that that I am unaware of…):

from dolfinx.fem import Constant, Function, FunctionSpace, locate_dofs_geometrical, dirichletbc
from dolfinx.fem.petsc import LinearProblem
from dolfinx import io
import ufl
import numpy as np
from mpi4py import MPI
from petsc4py import PETSc

# read mesh from file
filename = "I_beam.xdmf"
infile = io.XDMFFile(MPI.COMM_WORLD, filename, "r")
mesh = infile.read_mesh(name="Grid")

# define material parameters
thick = Constant(mesh, 1e-3)
E = Constant(mesh, 210e9)
nu = Constant(mesh, 0.3)
lmbda = E * nu / (1 + nu) / (1 - 2 * nu)
mu = E / 2 / (1 + nu)
lmbda_ps = 2 * lmbda * mu / (lmbda + 2 * mu)

# loading (self-weight)
f = Constant(mesh, (0, 0.2, -1))

def local_frame(mesh):
    t = ufl.Jacobian(mesh)
    if mesh.topology.dim == 2:
        t1 = ufl.as_vector([t[0, 0], t[1, 0], 0])
        t2 = ufl.as_vector([t[0, 1], t[1, 1], 0])
    else:
        t1 = ufl.as_vector([t[0, 0], t[1, 0], t[2, 0]])
        t2 = ufl.as_vector([t[0, 1], t[1, 1], t[2, 1]])
    e3 = ufl.cross(t1, t2)
    e3 /= ufl.sqrt(ufl.dot(e3, e3))
    ey = ufl.as_vector([0, 1, 0])
    ez = ufl.as_vector([0, 0, 1])
    e1 = ufl.cross(ey, e3)
    norm_e1 = ufl.sqrt(ufl.dot(e1, e1))
    e1 = ufl.conditional(ufl.lt(norm_e1, 0.5), ez, e1 / norm_e1)

    e2 = ufl.cross(e3, e1)
    e2 /= ufl.sqrt(ufl.dot(e2, e2))
    return e1, e2, e3

frame = local_frame(mesh)
e1, e2, e3 = frame

# create test and trial function spaces
Ue = ufl.VectorElement("Lagrange", mesh.ufl_cell(), 2, dim=3)
Te = ufl.VectorElement("CR", mesh.ufl_cell(), 1, dim=3)
V = FunctionSpace(mesh, ufl.MixedElement([Ue, Te]))
v = Function(V)
u, theta = ufl.split(v)
v_ = ufl.TestFunction(V)
u_, theta_ = ufl.split(v_)
dv = ufl.TrialFunction(V)

# boundary conditions
def clamped_boundary(x):
    xMin1 = -0.1250
    xMin2 = 0.1250
    return np.logical_or(np.isclose(x[0], xMin1), np.isclose(x[0], xMin2))

u_D = Function(V)
with u_D.vector.localForm() as loc:
    loc.set(0.0)

# locate dofs
Vs = [V.sub(i).collapse()[0] for i in range(V.num_sub_spaces)]
dofs = [locate_dofs_geometrical((V.sub(i), Vs[i]), clamped_boundary) for i in range(V.num_sub_spaces)]
bc = [dirichletbc(u_D, dofs[i]) for i in range(V.num_sub_spaces)]

# define projectors
def vstack(vectors):
    """Stack a list of vectors vertically."""
    return ufl.as_matrix([[v[i] for i in range(len(v))] for v in vectors])

def hstack(vectors):
    """Stack a list of vectors horizontally."""
    return vstack(vectors).T

# In-plane projection
P_plane = hstack([e1, e2])

# define tangential gradient operator
def t_grad(u):
    """Tangential gradient operator"""
    g = ufl.grad(u)
    return ufl.dot(g, P_plane)

# extract gradient of in-plane displacement, define membrane strain, bending curvature, shear strain
t_gu = ufl.dot(P_plane.T, t_grad(u))
eps = ufl.sym(t_gu)
kappa = ufl.sym(ufl.dot(P_plane.T, t_grad(ufl.cross(e3, theta))))
gamma = t_grad(ufl.dot(u, e3)) - ufl.dot(P_plane.T, ufl.cross(e3, theta))
eps_ = ufl.replace(eps, {v: v_})
kappa_ = ufl.replace(kappa, {v: v_})
gamma_ = ufl.replace(gamma, {v: v_})

# stress measures
def plane_stress_elasticity(e):
    return lmbda_ps * ufl.tr(e) * ufl.Identity(2) + 2 * mu * e

N = thick * plane_stress_elasticity(eps)
M = thick ** 3 / 12 * plane_stress_elasticity(kappa)
Q = mu * thick * gamma

# drilling rotation stabilization
drilling_strain = (t_gu[0, 1] - t_gu[1, 0]) / 2 - ufl.dot(theta, e3)
drilling_strain_ = ufl.replace(drilling_strain, {v: v_})
drilling_stress = E * thick ** 3 * drilling_strain

# build equations
Wdef = (
    ufl.inner(N, eps_)
    + ufl.inner(M, kappa_)
    + ufl.dot(Q, gamma_)
    + drilling_stress * drilling_strain_
) * ufl.dx
a = ufl.derivative(Wdef, v, dv)
Wext = ufl.dot(f, u_) * ufl.dx

# solve
problem = LinearProblem(a, Wext, bcs=bc, petsc_options={"ksp_type": "preonly", "pc_type": "lu"})
u = problem.solve()

If, for example, I do

print(u.sub(0).x.array)

the output is [nan nan nan ... nan nan nan].

So there are problems. The are numerous changes from the original implementation, some of which may well be ill-advised(?). I will of course accept any help whatsoever, but what would be particularly useful to assist in my debugging would be an explanation of how I can map the DOF result vector the nodes of the input mesh? Specifically, the sizes of the mesh nodal geometry and one of the DOF vectors are:

print(mesh.geometry.x.shape)
(306, 3)
print(u.sub(0).x.array.shape)
(5748,)

How can I pull out, for example, only the X-direction displacements (in which case I would expect a size of (306,))? I see various instances of dofmap properties but it isn’t clear to me out these work.

Any help is appreciated, thanks in advance!

Hi @aherrema, for your specific question that

how I can map the DOF result vector the nodes of the input mesh ? Specifically, the sizes of the mesh nodal geometry and one of the DOF vectors are:

I think the problem is that you’re using CG2 function space for the subspace one of the solution function u, and thus you won’t be able to directly get its vertex values by extracting all of its dofs. What you may want to do is to project it onto CG1 space and then extract it.

Please take a look at the following code, where you should be able to get nodal x-direction displacements as u_x_array.


####### Project the CG2CR1 space onto CG1CR1 ##############

Ue1 = VectorElement("Lagrange",mesh.ufl_cell(),1)
VE1 = MixedElement([Ue1,Te])
V1 = FunctionSpace(mesh,VE1)
u1 = Function(V1)
project(u, u1)
u_x = u1.sub(0).sub(0).collapse()
u_x_array = u_x.vector.getArray()
print(len(u_x_array))

And project() is a self-defined function you can use here: (I don’t know if it’s now implemented in DOLFINx, but this self-defined function won’t give you errors).

from dolfinx import *
from dolfinx.fem import (assemble_scalar, assemble_vector, assemble_matrix,
                        apply_lifting, set_bc)
from ufl import TestFunction, TrialFunction, dx, inner
from petsc4py import PETSc

def project(v, target_func, bcs=[]):

    """ 
    Adapted from 
    https://fenicsproject.discourse.group/t/problem-interpolating-mixed-
    function-dolfinx/4142/6
    """
    
    # Ensure we have a mesh and attach to measure
    V = target_func.function_space
    # Define variational problem for projection
    w = TestFunction(V)
    Pv = TrialFunction(V)
    a = inner(Pv, w) * dx
    L = inner(v, w) * dx

    # Assemble linear system
    A = assemble_matrix(a, bcs)
    A.assemble()
    b = assemble_vector(L)
    apply_lifting(b, [a], [bcs])
    b.ghostUpdate(addv=PETSc.InsertMode.ADD, mode=PETSc.ScatterMode.REVERSE)
    set_bc(b, bcs)
    
    solver = PETSc.KSP().create(A.getComm())
    solver.setOperators(A)
    solver.solve(b, target_func.vector)

You might also want to be careful to import the ufl methods in the beginning, or adding ufl. before the function names, i.e. ufl.VectorElement(...). I used the former for my own code.

Hope you can find it helpful ~

Thanks @Ru_Xiang, this is helpful. I suspected that projection to CG1 might indeed be the way to proceed but wasn’t sure of the implementation details to achieve that in dolfinx–the function you shared is a great step in the right direction. With my initial tests I’m getting the below error in the call to assemble_matrix.

Traceback (most recent call last):
File “/root/linear-shell-demo/linear-shell.py”, line 247, in
project_custom(u, u1)
File “/root/linear-shell-demo/project_custom.py”, line 25, in project_custom
print(a.mesh)
AttributeError: ‘Form’ object has no attribute ‘mesh’

Perhaps there is some dolfinx version issue here–I’ll dig into this in more detail at a later time if nobody is immediately aware of the solution. Thanks again.

Hi @aherrema, I agree that projection onto CG1 might not be very helpful to solve the issue of getting Nans in the solutions, so it was just for your specific question of how to get displacement vector of the right size. And I was using an old version of DOLFINx (last updated in Jan 2022), and it was only yesterday that I just realized there have been great changes in the Python API. So for the projection() function, you may try the following code instead, which works with the latest commit of DOLFINx pulled from Github.

import dolfinx
# import the `assemble` functions from the `dolfinx.fem.petsc` 
# module instead of `dolfinx.fem`
from dolfinx.fem.petsc import (assemble_vector, assemble_matrix, apply_lifting)
# import `form` wrapper from the `dolfinx.fem` module
from dolfinx.fem import (set_bc, Function, FunctionSpace, form)
from ufl import TestFunction, TrialFunction, dx, inner
from petsc4py import PETSc



def project(v, target_func, bcs=[]):

    """ 
    Solution from 
    https://fenicsproject.discourse.group/t/problem-interpolating-mixed-
    function-dolfinx/4142/6
    """
    
    # Ensure we have a mesh and attach to measure
    V = target_func.function_space
    # Define variational problem for projection
    w = TestFunction(V)
    Pv = TrialFunction(V)
    a = inner(Pv, w) * dx
    L = inner(v, w) * dx
    
    # Assemble linear system
    # a -> form(a): wrap up the the <class 'ufl.form.Form'> 
    # as a <class 'dolfinx.fem.forms.Form'> object
    A = assemble_matrix(form(a), bcs)
    A.assemble()
    b = assemble_vector(form(L))
    apply_lifting(b, [form(a)], [bcs])
    b.ghostUpdate(addv=PETSc.InsertMode.ADD, mode=PETSc.ScatterMode.REVERSE)
    set_bc(b, bcs)
    
    solver = PETSc.KSP().create(A.getComm())
    solver.setOperators(A)
    solver.solve(b, target_func.vector)

And the error you’re getting there should also be resolved.

1 Like

Hi @Ru_Xiang, this updated code does indeed run successfully with the version of dolfinx that I’m using, and u_x_array has the expected length of 306. All zeros at this point :stuck_out_tongue:, but this approach will definitely be helpful as I continue to debug. Thanks!

Please note that you can also interpolate from one space to another,i.e.

from dolfinx import fem, mesh as msh
from mpi4py import MPI
import ufl
mesh = msh.create_unit_cube(MPI.COMM_WORLD, 1,1,1)
Ue = ufl.VectorElement("Lagrange", mesh.ufl_cell(), 2,dim=3)
Te = ufl.VectorElement("CR", mesh.ufl_cell(), 1, dim=3)
V = fem.FunctionSpace(mesh, ufl.MixedElement([Ue, Te]))
v = fem.Function(V)
v.sub(0).interpolate(lambda x: (x[0], x[1], x[2]))
v.sub(1).interpolate(lambda x: (x[0], x[1],x[2]))

VCG1 = fem.VectorFunctionSpace(mesh, ("CG", 1))
u = fem.Function(VCG1)
u.interpolate(v.sub(0))


coords = VCG1.tabulate_dof_coordinates()
for i, coord in enumerate(coords):
    print("Coord ", coord, "Value: ", end="")
    for b in range(VCG1.dofmap.bs):
        print(u.x.array[i*VCG1.dofmap.bs+b],end=" ")
    print("")
4 Likes