Elastic solver code layout improvements and displacement at required node

Hi,

I wrote the following simple code to solve for linear elasticity and to compute the maximum principal stress. The displacement elements are linear and the computed stresses are interpolated to a DG 0 space.

  1. My first question is: from a quick look of the code, is there anything I can do to make it simpler/easier to read?

  2. I am using a wrapper for the CGAL library for meshing, and there are always 1 or 2 low quality nodes generated. How could I compute the mesh quality and find out what elements not to consider when I am looking for the maximum principal stress:

max_stress_per_N = np.max(pmax.x.array)

  1. I tried increasing the order of the Lagrange elements by 1, and using DG 1 for the stress, but the program crashes. Is there a reason for this?

  2. When I look for the displacement I use the line below, is there another way of obtaining this if you know the node, but for an arbitrary element degree?

dof_coords = Vp.tabulate_dof_coordinates()
distances  = np.linalg.norm(dof_coords - grasp_node, axis=1)
dof_index  = np.argmin(distances)
u_node     = np.sqrt(uh.x.array[3*dof_index]**2 + uh.x.array[3*dof_index+1]**2 + uh.x.array[3*dof_index+2]**2)

This is the full code:

import numpy as np
#import pygalmesh
import meshio
import time

from mpi4py import MPI
from petsc4py import PETSc

import ufl
import basix.ufl as bu
from dolfinx import mesh, fem, io, default_scalar_type
from dolfinx.fem.petsc import assemble_matrix, assemble_vector, apply_lifting, set_bc


r_loading = 3.0e-3


def make_dolfinx_mesh_from_pygalmesh(vol_mesh):
    points = vol_mesh.points
   
    tet_cells = None
    for c in vol_mesh.cells:
        if c.type in ["tetra", "tetra10", "tetrahedron"]:
            tet_cells = c.data
            break
    if tet_cells is None:
        raise RuntimeError("No tetrahedral cells found in the volume mesh.")

    # Vector Lagrange P1 element for mesh
    finiteElement = bu.element("Lagrange", "tetrahedron", 1, shape=(3,))
    domain = mesh.create_mesh(MPI.COMM_WORLD, cells=tet_cells, x=points, e=finiteElement)
    return domain


def mark_boundaries(domain, facet_node,grasp_node,grasp_node_normal, eps=1e-3):
    
    dim = domain.topology.dim
    fdim = dim - 1
    domain.topology.create_connectivity(fdim, dim)

    def left_facets(xq):
        return np.sqrt((xq[0]-facet_node[0])**2 + (xq[1]-facet_node[1])**2 + (xq[2]-facet_node[2])**2) < r_loading

    def right_facets(xq):
        return np.sqrt((xq[0]-grasp_node[0])**2 + (xq[1]-grasp_node[1])**2 + (xq[2]-grasp_node[2])**2) < r_loading

    facets_left  = mesh.locate_entities(domain, fdim, left_facets)
    facets_right = mesh.locate_entities(domain, fdim, right_facets)

    facet_indices = np.concatenate([facets_left, facets_right])
    facet_markers = np.concatenate([np.full_like(facets_left, 1, dtype=np.int32),
                                    np.full_like(facets_right, 2, dtype=np.int32)])

    # Sort by facet index
    sort_perm = np.argsort(facet_indices)
    facet_indices = facet_indices[sort_perm]
    facet_markers = facet_markers[sort_perm]

    mt = mesh.meshtags(domain, fdim, facet_indices, facet_markers)
    return mt


def elasticity_parameters(E=10.0e4, nu=0.35):
    """LamΓ© parameters from E, nu."""
    lambda_ = E * nu  / ((1.0 + nu) * (1.0 - 2.0 * nu))
    mu      = E * 0.5 /  (1.0 + nu)
    
    return lambda_, mu


def epsilon(u):
    return ufl.sym(ufl.grad(u))


def sigma(u, lambda_, mu):
    return lambda_ * ufl.nabla_div(u) * ufl.Identity(len(u)) + 2.0 * mu * epsilon(u)


def setup_function_space(domain):
    V = fem.functionspace(domain, ("Lagrange", 1, (domain.geometry.dim,)))
    return V


def build_primal_problem(domain, facet_tags, lambda_, mu, node_normal,
                         p_traction=1.0/(np.pi*r_loading**2), body_force=(0.0, 0.0, -1000.0*9.8)):
  
    dim = domain.topology.dim
    fdim = dim - 1
    ds = ufl.Measure("ds", domain=domain, subdomain_data=facet_tags)

    V = setup_function_space(domain)
    u = ufl.TrialFunction(V)
    v = ufl.TestFunction(V)

    f_vec = fem.Constant(domain, default_scalar_type(body_force))

    # Traction direction: push along +x (change sign if needed)
    T = fem.Constant(domain, -p_traction * default_scalar_type((node_normal[0], node_normal[1], node_normal[2])))

    a = ufl.inner(sigma(u, lambda_, mu), epsilon(v)) * ufl.dx
    L = ufl.dot(f_vec, v) * ufl.dx + ufl.dot(T, v) * ds(2)

    # Dirichlet BC (tag 1)
    left_facets = facet_tags.find(1)
    domain.topology.create_connectivity(fdim, dim)
    left_dofs = fem.locate_dofs_topological(V, fdim, left_facets)

    u_D = np.array((0.0, 0.0, 0.0), dtype=default_scalar_type)
    bc_left = fem.dirichletbc(u_D, left_dofs, V)

    return V, a, L, [bc_left]



def solve_linear_system(domain, V, a, L, bcs):
    
    a_form = fem.form(a)
    L_form = fem.form(L)

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

    b = assemble_vector(L_form)
    apply_lifting(b, [a_form], bcs=[bcs])
    b.ghostUpdate(addv=PETSc.InsertMode.ADD, mode=PETSc.ScatterMode.REVERSE)
    set_bc(b, bcs)

    uh = fem.Function(V)
    solver = PETSc.KSP().create(domain.comm)
    solver.setType(PETSc.KSP.Type.PREONLY)
    solver.getPC().setType(PETSc.PC.Type.LU)
    solver.setOperators(A)
    solver.solve(b, uh.x.petsc_vec)
    uh.x.scatter_forward()
    
    return uh



def principal_stress(domain, uh, lambda_, mu):
 
    #Compute principal stresses via invariants (analytical eigenvalue solution)
    sigma_uh = sigma(uh, lambda_, mu)
    q = ufl.tr(sigma_uh) / 3                           # mean stress (I1/3)
    B = sigma_uh - q * ufl.Identity(3)                 # deviatoric stress tensor
    j = ufl.tr(B * B) / 2                              # J2 = 1/2 tr(B^2)
    b = ufl.tr(B * B * B) / 3                          # J3 = 1/3 tr(B^3)
    p = 2 / ufl.sqrt(3) * ufl.sqrt(j + 1e-12)          # p = 2/sqrt(3)*sqrt(J2) (small epsilon added)
    r = 4 * b / (p**3)                                 # r = 4 J3 / p^3
    r = ufl.max_value(ufl.min_value(r, +1-1e-6), -1+1e-6) # clamp r to [-1+eps, 1-eps] to avoid acos domain error
    phi = ufl.acos(r) / 3
    
    lambda2 = q + p * ufl.cos(phi)                     # largest principal stress (max)
    
    V_dg = fem.functionspace(domain, ("DG", 0))
    pmax_expr = fem.Expression(lambda2, V_dg.element.interpolation_points)
    pmax = fem.Function(V_dg, name="PrincipalStress")
    pmax.interpolate(pmax_expr)
        
    return pmax



def fea_solve(domain, facet_node, grasp_node, grasp_node_normal):
 
    lambda_, mu = elasticity_parameters()

    facet_tags  = mark_boundaries(domain, facet_node,grasp_node,grasp_node_normal)

    Vp, a_p, L_p, bcs_p = build_primal_problem(domain, facet_tags, lambda_, mu, grasp_node_normal)
    
    uh = solve_linear_system(domain, Vp, a_p, L_p, bcs_p)

    # Principal stress
    pmax = principal_stress(domain, uh, lambda_, mu)

    # Get displacement at node where load is applied
    dof_coords = Vp.tabulate_dof_coordinates()
    distances  = np.linalg.norm(dof_coords - grasp_node, axis=1)
    dof_index  = np.argmin(distances)
    u_node     = np.sqrt(uh.x.array[3*dof_index]**2 + uh.x.array[3*dof_index+1]**2 + uh.x.array[3*dof_index+2]**2)
    
    return domain, uh, u_node, pmax


     
# Part dependent mesh for FEA
##################################################################
# min_facet_angle                  = 25.0
# max_radius_surface_delaunay_ball = 0.0006
# max_facet_distance               = 0.0006
# max_circumradius_edge_ratio      = 3.0

# t_start = time.perf_counter()
# vol_mesh = pygalmesh.generate_volume_mesh_from_surface_mesh("bracket_ver2.STL",
#     min_facet_angle=min_facet_angle,
#     max_radius_surface_delaunay_ball=max_radius_surface_delaunay_ball,
#     max_facet_distance=max_facet_distance,
#     max_circumradius_edge_ratio=max_circumradius_edge_ratio,
#     odt=True, lloyd=True,
#     verbose=False,
# )
# t_end   = time.perf_counter()
# print(f"Meshing elapsed time: {t_end  - t_start:.6f} seconds")

# vol_mesh.write("mesh_braket.vtk")
    
    
domain = make_dolfinx_mesh_from_pygalmesh(meshio.read("mesh_braket.vtk"))

 

grasp_facet_node   = [0.02, 0.018, 0.008]    
grasp_node2        = [0.0,  0.018, 0.008]
grasp_node_normals = [-1.0, 0.0, 0.0]



t_start = time.perf_counter()
domain_f, uh, u_at_node, pmax = fea_solve(domain, 
    grasp_facet_node, grasp_node2, grasp_node_normals)
t_end   = time.perf_counter()
print(f"FEA     elapsed time: {t_end  - t_start:.6f} seconds")
print('\n')

with io.XDMFFile(domain_f.comm, "max_p_stress_braket.xdmf", "w") as xdmf:
    xdmf.write_mesh(domain_f)
    uh.name   = "u"
    xdmf.write_function(uh)
    pmax.name = "PrincipalStress"
    xdmf.write_function(pmax)

max_stress = np.max(pmax.x.array)


print(u_at_node)  
print(max_stress)

The mesh .vtk file can be found here:

Thank you,
Alex

The solver logic here can definitely be replaced by dolfinx.fem.petsc.LinearProblem, as there is nothing custom going on in this code.

There are several functions in DOLFINx for doing this properly.
For instance:

which gives you all the information needed to call Function.eval (dolfinx.fem β€” DOLFINx 0.10.0.post5 documentation)
by considering the docs of
dolfinx.geometry β€” DOLFINx 0.10.0.post5 documentation
i.e.

from mpi4py import MPI
import dolfinx

domain = dolfinx.mesh.create_unit_square(MPI.COMM_WORLD, 10, 10)
import numpy as np


grasp_node = np.array([[0.5, 0.4, 0.0]], dtype=np.float64)


if domain.comm.rank == 0:
    nodes = grasp_node
else:
    nodes = np.zeros((0, 3), dtype=grasp_node.dtype)
point_data = dolfinx.geometry.determine_point_ownership(domain, nodes, 1e-12)

V = dolfinx.fem.functionspace(domain, ("DG", 0))
uh = dolfinx.fem.Function(V)
uh.interpolate(lambda x: x[0] + x[1])
print(domain.comm.rank, uh.eval(point_data.dest_points, point_data.dest_cells))

Thank you, yes this is correct, plus I can setup PETSc from the function

The easiest way I have found to compute mesh quality in FEniCSx is to interface with PyVista. I have a tool here that I have used. There is a demo on how to use it in the main-function of the python file.

Thank you for the reply, PyVista seems really good, but if you need to check only one mesh quality metric it might be best to program it instead of using another library. One good metric for tets is the Radius Ratio metric: https://www.researchgate.net/publication/221561655_Proposal_of_Benchmarks_for_3D_Unstructured_Tetrahedral_Mesh_Optimization