Cantilever beam under a tip moment#

Pure-bending end-loaded prismatic beam, the simplest test of moment-driven Bernoulli kinematics. A clamped-free cantilever of length \(L\) carries a moment \(M_0\) about the out-of-plane axis applied at its tip; equilibrium and elementary integration give a closed form (Roark & Young 1989, Table 8 case 4; Timoshenko 1955 §32):

\[\theta(x) = \frac{M_0\, x}{E I}, \qquad v(x) = \frac{M_0\, x^{2}}{2\, E I},\]

so the slope and tip rotation / deflection are

\[\theta_\mathrm{tip} = \frac{M_0\, L}{E I}, \qquad v_\mathrm{tip} = \frac{M_0\, L^{2}}{2\, E I},\]

and the curvature \(\kappa = M_0 / (E I)\) is constant along the entire span — a cantilever under pure end moment deforms into a circular arc of radius \(E I / M_0\).

Implementation#

A 10-element BEAM2 (Hermite-cubic Bernoulli) line spans the beam. Boundary conditions: clamp all 6 DOFs at the root. Loading: a single \(+M_z\) moment at the tip node.

Hermite cubic shape functions interpolate displacement and slope simultaneously, so the FE response under a pure tip moment matches the analytic parabolic deflection profile to machine precision at every node.

References#

  • Timoshenko, S. P. (1955) Strength of Materials, Part I, 3rd ed., Van Nostrand, §32.

  • Roark, R. J. and Young, W. C. (1989) Roark’s Formulas for Stress and Strain, 6th ed., McGraw-Hill, Table 8 case 4 (cantilever, end moment).

  • Cook, R. D., Malkus, D. S., Plesha, M. E., Witt, R. J. (2002) Concepts and Applications of Finite Element Analysis, 4th ed., Wiley, §2.5 (Hermite cubic shape functions).

from __future__ import annotations

import numpy as np
import pyvista as pv
from vtkmodules.util.vtkConstants import VTK_LINE

import femorph_solver
from femorph_solver import ELEMENTS

Problem data#

E = 2.0e11  # Pa (steel)
NU = 0.30
RHO = 7850.0  # kg/m^3
b = 0.050  # square cross-section side [m]
A = b * b
I = b**4 / 12.0  # noqa: E741
J = 2.0 * I
L = 1.0  # span [m]
M0 = 1.0e3  # tip moment [N m] about +z

N_ELEM = 10  # 10 segments → exact at every node for Bernoulli kinematics

# Closed-form quantities -----------------------------------------------

theta_tip_published = M0 * L / (E * I)
v_tip_published = M0 * L**2 / (2.0 * E * I)
kappa_published = M0 / (E * I)
R_arc = 1.0 / kappa_published  # radius of the deformed arc

print(f"M0 = {M0:.1f} N m, L = {L:.2f} m, EI = {E * I:.3e} N m^2")
print(f"θ_tip = M L / (E I)        = {theta_tip_published:.6e} rad")
print(f"v_tip = M L^2 / (2 E I)    = {v_tip_published:.6e} m")
print(f"κ     = M / (E I)          = {kappa_published:.6e} 1/m  (constant)")
print(f"R_arc = E I / M            = {R_arc:.4f} m  (radius of deformed arc)")
M0 = 1000.0 N m, L = 1.00 m, EI = 1.042e+05 N m^2
θ_tip = M L / (E I)        = 9.600000e-03 rad
v_tip = M L^2 / (2 E I)    = 4.800000e-03 m
κ     = M / (E I)          = 9.600000e-03 1/m  (constant)
R_arc = E I / M            = 104.1667 m  (radius of deformed arc)

Build a 10-element BEAM2 line clamped at x=0#

points = np.array(
    [[i * L / N_ELEM, 0.0, 0.0] for i in range(N_ELEM + 1)],
    dtype=np.float64,
)
cells_list: list[int] = []
for i in range(N_ELEM):
    cells_list.extend([2, i, i + 1])
cells = np.asarray(cells_list, dtype=np.int64)
cell_types = np.full(N_ELEM, VTK_LINE, dtype=np.uint8)
grid = pv.UnstructuredGrid(cells, cell_types, points)

m = femorph_solver.Model.from_grid(grid)
m.assign(
    ELEMENTS.BEAM2,
    material={"EX": E, "PRXY": NU, "DENS": RHO},
    real=(A, I, I, J),
)

m.fix(nodes=[1], dof="ALL")  # full clamp at root
m.apply_force(N_ELEM + 1, mz=M0)  # tip moment about +z

Static solve + extract tip rotation / deflection#

res = m.solve()
dof = m.dof_map()
tip_uy_idx = np.where((dof[:, 0] == N_ELEM + 1) & (dof[:, 1] == 1))[0][0]
tip_rotz_idx = np.where((dof[:, 0] == N_ELEM + 1) & (dof[:, 1] == 5))[0][0]
v_tip = float(res.displacement[tip_uy_idx])
theta_tip = float(res.displacement[tip_rotz_idx])

err_v = (v_tip - v_tip_published) / v_tip_published
err_th = (theta_tip - theta_tip_published) / theta_tip_published

print()
print("tip response  (femorph-solver) → (analytical)")
print(f"  v_tip      = {v_tip:+.6e}{v_tip_published:+.6e}  ({err_v * 100:+.6f}%)")
print(f"  θ_tip      = {theta_tip:+.6e}{theta_tip_published:+.6e}  ({err_th * 100:+.6f}%)")

assert abs(err_v) < 1e-8, f"tip deflection error {err_v:.2e} too large for Bernoulli BEAM2"
assert abs(err_th) < 1e-8, f"tip rotation error {err_th:.2e} too large for Bernoulli BEAM2"
tip response  (femorph-solver) → (analytical)
  v_tip      = +4.800000e-03  →  +4.800000e-03  (-0.000000%)
  θ_tip      = +9.600000e-03  →  +9.600000e-03  (-0.000000%)

Verify the parabolic deflection profile#

Every node should satisfy \(v(x) = M_0 x^2 / (2 E I)\) to machine precision.

x_nodes = points[:, 0]
v_published_profile = M0 * x_nodes**2 / (2.0 * E * I)
v_computed_profile = np.zeros(N_ELEM + 1)
for i in range(N_ELEM + 1):
    rows = np.where((dof[:, 0] == i + 1) & (dof[:, 1] == 1))[0]
    v_computed_profile[i] = float(res.displacement[rows[0]])
np.testing.assert_allclose(v_computed_profile, v_published_profile, atol=1e-14, rtol=1e-10)
print()
print("OK — full deflection profile v(x) = M_0 x^2 / (2 E I) matches at every node.")
OK — full deflection profile v(x) = M_0 x^2 / (2 E I) matches at every node.

Verify constant curvature κ = M / (EI)#

A second central difference on the slope \(\theta_z(x)\) at every interior node should recover the constant \(\kappa = M_0 / (E I)\) to within the Bernoulli-FE-at-node precision floor.

theta_profile = np.zeros(N_ELEM + 1)
for i in range(N_ELEM + 1):
    rows = np.where((dof[:, 0] == i + 1) & (dof[:, 1] == 5))[0]
    theta_profile[i] = float(res.displacement[rows[0]])
kappa_estimates = np.diff(theta_profile) / np.diff(x_nodes)
print(
    f"κ estimates (per element): mean = {kappa_estimates.mean():.6e}, "
    f"std = {kappa_estimates.std():.2e}"
)
np.testing.assert_allclose(kappa_estimates, kappa_published, rtol=1e-10)
print(f"OK — κ uniformly equals M_0/(E I) = {kappa_published:.6e} 1/m within 1e-10.")
κ estimates (per element): mean = 9.600000e-03, std = 6.48e-16
OK — κ uniformly equals M_0/(E I) = 9.600000e-03 1/m within 1e-10.

Render the deflected line + reference circular arc#

x_dense = np.linspace(0, L, 200)
v_dense = M0 * x_dense**2 / (2.0 * E * I)
arc_pts = np.column_stack([x_dense, v_dense, np.zeros_like(x_dense)])
arc = pv.PolyData(arc_pts)

warp = grid.copy()
warp.points = grid.points + np.column_stack(
    [np.zeros(N_ELEM + 1), 200.0 * v_computed_profile, np.zeros(N_ELEM + 1)]
)
warp["uy"] = v_computed_profile

plotter = pv.Plotter(off_screen=True, window_size=(720, 320))
plotter.add_mesh(grid, color="grey", opacity=0.3, line_width=2)
plotter.add_mesh(
    arc,
    style="points",
    color="black",
    opacity=0.4,
    point_size=3,
    label="closed form v(x)",
)
plotter.add_mesh(warp, scalars="uy", line_width=5, cmap="viridis")
plotter.view_xy()
plotter.camera.zoom(1.05)
plotter.show()
example verify cantilever tip moment

Total running time of the script: (0 minutes 0.159 seconds)

Gallery generated by Sphinx-Gallery