How to properly map DoFs coordinates and displacements?

Trying to have cantilever beam case Converting legacy FEniCS code to FEniCSx to work, I want to plot displacement to make sure loads and boundaries are ok.

I fetch DOFs coordinates from :

dof_coords = W0.tabulate_dof_coordinates()

… and y displacements from :

dof_displacements_y = u.sub(0).sub(1).collapse().x.array

which gives me this sawtooth curve and raises interrogations on index discrepancy between the values :
DoFs coordinates and displacements
I have been through different threads including Recover deformation field vectors from solution but could not find how to fix my case.
I would greatly appreciate any insights or suggestions on how to resolve this issue.

Here bellow my full code :

import os
arch = os.getenv("ARGS", "real")

try:
    import google.colab  # noqa: F401
except ImportError:
    import ufl
    import dolfinx
else:
    try:
        import ufl
        import dolfinx
    except ImportError:
        if arch != "complex":
            !wget "https://fem-on-colab.github.io/releases/fenicsx-install-real.sh" -O "/tmp/fenicsx-install.sh" && bash "/tmp/fenicsx-install.sh"
        else:
            !wget "https://fem-on-colab.github.io/releases/fenicsx-install-complex.sh" -O "/tmp/fenicsx-install.sh" && bash "/tmp/fenicsx-install.sh"
        import ufl
        import dolfinx

# Install meshio for mesh handling
!pip install meshio

# Importing necessary libraries and modules
import numpy as np       # Numerical operations
from mpi4py import MPI   # Parallel computing
import basix             # Finite element basis functions
from dolfinx import fem
from dolfinx import plot
from dolfinx.fem import (Constant, FunctionSpace, locate_dofs_geometrical,
                         Function, dirichletbc, assemble_scalar, form)
                         # FEM module functions
from dolfinx.io import XDMFFile  # I/O operations
from dolfinx.fem.petsc import LinearProblem
                         # Solving linear problems
from dolfinx.mesh import locate_entities, meshtags
                         # Mesh utility functions
from ufl import (Jacobian, as_vector, sqrt, inner, cross, dot, TestFunction,
                TrialFunction, split, diag, grad, Measure, dx, avg)
                         # Defining variational forms
import meshio            # Mesh I/O operations
import matplotlib.pyplot as plt
import pyvista as pv     # Visualization

# Initialize PyVista
pv.start_xvfb()

## Simple mesh
length = 10.0
N = 10 # number of elements

points = np.zeros((N+1, 3))                     # N+1 array of zeros in 3D space
points[:, 0] = np.linspace(0, length, N+1)      # Distribute points along x from 0 to 'length'
cells = np.array([[i, i+1] for i in range(N)])  # line elements connectivity
meshio.write("mesh.xdmf", meshio.Mesh(points, {"line": cells}))  # Write XDMF mesh file

# Reading the mesh from the XDMF file
with XDMFFile(MPI.COMM_WORLD, "mesh.xdmf", "r") as xdmf:
    domain = xdmf.read_mesh(name="Grid")

# Creating connectivity information for the mesh
domain.topology.create_connectivity(domain.topology.dim, domain.topology.dim-1)

# Explicitly compute the entity-to-cell connectivity
# domain.topology.create_connectivity(domain.topology.dim, 0)

# mesh test

# Get the geometrical and topological dimensions of the mesh from the 'domain' object.
gdim = domain.geometry.dim    # = 3
tdim = domain.topology.dim    # = 1

# Print the geometrical and topological dimensions of the mesh.
print(f"Geometrical dimension = {gdim}")
print(f"Topological dimension = {tdim}")

# Extract coordinates of vertices
vertices_coordinates = domain.geometry.x
print("len(vertices_coordinates)=",len(vertices_coordinates)) # = 11

# Create a 3D plot
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')

# Plot vertices
ax.scatter(vertices_coordinates[:, 0],
           vertices_coordinates[:, 1],
           vertices_coordinates[:, 2],
           c='b',
           marker='o')

# Set labels
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')

# Show plot
plt.show()

# Defining a function to compute the tangent vector to the mesh
def tangent(domain):
    t = Jacobian(domain)
    return as_vector([t[0,0], t[1, 0], t[2, 0]])/sqrt(inner(t,t))

# Calculating the tangent vector for the domain
t = tangent(domain)

# Defining basis vectors orthogonal to the tangent vector
ez = as_vector([0, 0, 1])   # Basis vector in the z-direction
a1 = cross(t, ez)           # First orthogonal vector
a1 /= sqrt(dot(a1, a1))     # Normalizing the first orthogonal vector
a2 = cross(t, a1)
a2 /= sqrt(dot(a2, a2))

# Geometric properties
thick_val = 0.3
thick = Constant(domain,thick_val)
width_val = thick_val
width = thick/3

# Material Properties
E_val = 70e3
E = Constant(domain,E_val)      # Young's modulus
nu = Constant(domain,0.3)       # Poisson's ratio
G = E/2/(1+nu)                  # Shear modulus
rho = Constant(domain,2.7e-3)   # Density
g = Constant(domain,9.81)       # Gravitational acceleration

S = thick*width                 # Cross-sectional area
ES = E*S                        # Axial stiffness
EI1 = E*width*thick**3/12       # Bending stiffness about one axis
EI2 = E*width**3*thick/12       # Bending stiffness about the other axis
J = (width * thick**3 + thick * width**3) / 3
GJ = G*J                        # Torsional stiffness
kappa = Constant(domain,5./6.)  # Shear correction factor
GS1 = kappa*G*S                 # Shear stiffness in one direction
GS2 = kappa*G*S                 # Shear stiffness in the other direction

# Define the displacement vector element using basix
Ue = basix.ufl.element(
    family=basix.ElementFamily.P,  # Lagrange family
    cell=basix.CellType.interval,  # 1D cell type
    degree=2,                      # Polynomial degree
    shape=(3,))                    # 3 components vector

# Define the rotation vector element using basix
Te = basix.ufl.element(
    family=basix.ElementFamily.P,  # Lagrange family
    cell=basix.CellType.interval,  # 1D cell type
    degree=1,                      # Polynomial degree
    shape=(3,))                    # 3 components vector

# Creating a mixed element for displacement and rotation
Ueprod = basix.ufl.mixed_element([Ue, Te])    # Mixed element
W = dolfinx.fem.functionspace(domain, Ueprod) # Creating a function space

# Defining test and trial functions
u_ = TestFunction(W)       # Test function
du = TrialFunction(W)      # Trial function
(w_, theta_) = split(u_)   # Split test function into displacement and rotation
(dw, dtheta) = split(du)   # Split trial function into displacement and rotation

# Defining generalized strains function
def tgrad(u):
    return dot(grad(u), t)

def generalized_strains(u):
    (w, theta) = split(u)
    return as_vector([dot(tgrad(w), t),
                      dot(tgrad(w), a1)-dot(theta, a2),
                      dot(tgrad(w), a2)+dot(theta, a1),
                      dot(tgrad(theta), t),
                      dot(tgrad(theta), a1),
                      dot(tgrad(theta), a2)])

# Defining generalized stresses function
def generalized_stresses(u):
    return dot(diag(as_vector([ES, GS1, GS2, GJ, EI1, EI2])),
               generalized_strains(u))

# Calculating stresses and strains
Sig = generalized_stresses(du)   # Generalized stresses
Eps =  generalized_strains(u_)   # Generalized strains

# Locate left boundary
def left(x):
    return(np.isclose(x[0], 0, 0.1))   # value, target, tolerance

# Locate load application
def load(x):
    return np.isclose(x[0], length, 0.1)

# Defining boundaries and marking facets
boundaries = [(1, left),              # Marker 1 for left boundary
              (2, load)]              # Marker 2 for load application point

facet_indices, facet_markers = [], [] # Lists to store facet indices and markers
fdim = domain.topology.dim - 1        # Facet dimension

# Locating and marking facets for each boundary
for (marker, locator) in boundaries:
    facets = locate_entities(domain, fdim, locator)      # Locate facets
    facet_indices.append(facets)                         # Add facet indices
    facet_markers.append(np.full_like(facets, marker))   # Add facet markers

facet_indices = np.hstack(facet_indices).astype(np.int32)# Combine facet indices
facet_markers = np.hstack(facet_markers).astype(np.int32)# Combine facet markers

sorted_facets = np.argsort(facet_indices)                # Sort facets

facet_tag = meshtags(domain,
                     fdim,
                     facet_indices[sorted_facets],
                     facet_markers[sorted_facets])       # Create mesh tags

# Collapsing subspaces of the function space
W0, submap0 = W.sub(0).collapse()   # Collapsing displacement field subspace
W1, submap1 = W.sub(1).collapse()   # Collapsing rotation field subspace

left_dofs_x = locate_dofs_geometrical((W.sub(0),W0),left )
left_dofs_r = locate_dofs_geometrical((W.sub(1),W1),left)

# Create zero function for the first subspace (W0)
zero_0 = Function(W0)
with zero_0.vector.localForm() as zero_local:
    zero_local.set(0.0)

# Create zero function for the second subspace (W1)
zero_1 = Function(W1)
with zero_1.vector.localForm() as zero_local:
    zero_local.set(0.0)

# Define boundary conditions
bcs = [
    dirichletbc(zero_0, left_dofs_x, W.sub(0)),
    dirichletbc(zero_1, left_dofs_r, W.sub(1))]

metadata = {"quadrature_scheme": "default", "quadrature_degree": 4}

# Define a measure for surface integrals
ds = Measure("ds",
             domain=domain,
             subdomain_data=facet_tag,
             metadata = {"quadrature_degree": 4})

# Define a measure for surface integrals in 3D
dS = Measure("dS",
             domain=domain,
             subdomain_data=facet_tag,
             metadata = {"quadrature_degree": 4})

# Define a measure for volume integrals, used for shear stress
dx_shear = dx(scheme="default",
              metadata=metadata)

# Define the bilinear form (stiffness matrix)
k_form = (sum([Sig[i]*Eps[i]*dx for i in [0, 3, 4, 5]]) # normal and bending terms
             +(Sig[1]*Eps[1]+Sig[2]*Eps[2])             # shear terms
             *dx_shear)                                 # Integrate

P = -1.0   # load intensity

# Define the linear form (load vector)
l_form = (inner(Constant(domain,P),    # apply P
               avg(u_[1]))             # along y
         *dS(2))                       # on mark 2

u = Function(W)                             # Define the solution function

# Define linear problem
problem = LinearProblem(k_form,
                        l_form,
                        u=u,
                        bcs=bcs)
uh=problem.solve()                          # Solve the linear problem

xyz = W0.tabulate_dof_coordinates()
x_coor = xyz[:,0]
y_coor = xyz[:,1]
z_coor = xyz[:,2]

x_value, y_value, z_value = [], [], []
for i in range(len(x_coor)):
    x_value.append(u.sub(0).x.array[3*i:3*(i+1)][0])
    y_value.append(u.sub(0).x.array[3*i:3*(i+1)][1])
    z_value.append(u.sub(0).x.array[3*i:3*(i+1)][2])

# Get the coordinates of the DoFs in the collapsed subspace
dof_coords = W0.tabulate_dof_coordinates()

# Get the y-displacements in the collapsed subspace
dof_displacements_y = u.sub(0).sub(1).collapse().x.array

# Ensure `submap0` does not contain out-of-bound indices
valid_indices = [i for i in submap0 if i < len(dof_displacements_y)]

# Initialize mapped_displacements array with zeros
mapped_displacements = np.zeros(len(valid_indices))

# Map the displacements
for i, original_index in enumerate(valid_indices):
    mapped_displacements[i] = dof_displacements_y[original_index]

# Now you can plot or use `mapped_displacements` with `dof_coords`
plt.plot(dof_coords[:, 0], mapped_displacements, 'o-')
plt.xlabel('X-coordinate')
plt.ylabel('Y-displacement')
plt.title('Y-displacements along the beam')
plt.show()

# Print the maximum y-displacement
print("Max y-displacement =", min(mapped_displacements))

# Print the maximum y-displacement
dof_displacements_y = u.sub(0).sub(1).collapse().x.array
print("dof_displacements_y = ",dof_displacements_y)
print ("len(dof_displacements_y) = " ,len(dof_displacements_y))  # = 21 because degree=2

# Get DOFs coordinates for the displacement field
dof_coords = W.sub(0).collapse()[0].tabulate_dof_coordinates()

# Plot the data
plt.plot(dof_coords[:, 0], dof_displacements_y)

# Add labels and title
plt.xlabel('x_coordinates')
plt.ylabel('y_displacements')
plt.title('DOFs')

# Show the plot
plt.show()

print("Max y-displacement =",
      min(dof_displacements_y))                           # = -161.5063564124886

# Calculate maximum y-displacement using beam theory
beam_theory_deflection = (P * length**3) / (3 * E_val*width_val*thick_val**3/12)
print("Beam theory =", beam_theory_deflection)            # = -7.054673721340389

Instead of this, why don’t you call

uy = u.sub(0).sub(1).collapse()
dof_displacements_y = uy.x.array
coords = function_space.tabulate_dof_coordinates()[:, 0]
sort = np.argsort(coords)
plt.plot(coords[sort], dof_displacements_y[sort])

or am I missing something?

Thanks
I understand it confirms u and W0 DoFs index should match :

uy = u.sub(0).sub(1).collapse()
dof_displacements_y = uy.x.array
coords = W0.tabulate_dof_coordinates()[:, 0]
sort = np.argsort(coords)
plt.plot(coords[sort], dof_displacements_y[sort])

Untitled
So this sawtooth curve is real and there is actually something wrong in my code, which I suspected from the output

Max y-displacement = -161.5063564124886
Beam theory = -7.054673721340389

Good evening
I’ve been stuck on this issue for days now and I’m really running out of ideas. I can’t find any working example of a 1D cantilever beam under a concentrated load around here.
I’m sure that this is a common problem and that there is a simple solution out there, but I just can’t find it.
I’m really desperate for help at this point, so if you have any suggestions or can take a look at my code, I would be extremely grateful.