"""Simply-supported beam under uniformly-distributed transverse load.
Companion to :class:`SimplySupportedBeamCentralLoad` and
:class:`CantileverUDL` — same geometry as the SS beam problems
but with a uniformly-distributed transverse load ``q`` instead
of a central concentrated load.
The Euler-Bernoulli closed form for the mid-span deflection is
.. math::
\\delta_{\\text{mid}} = \\frac{5 q L^{4}}{384 E I},
\\qquad I = \\frac{w h^{3}}{12}.
The reaction at each support is :math:`R = q L / 2` by symmetry.
This is the familiar "5/384" coefficient every introductory
mechanics-of-materials course covers.
References
----------
* Timoshenko, S. P. *Strength of Materials, Part I*, 3rd ed.,
Van Nostrand, 1955, §5.6 — simply-supported beam under
uniformly distributed load.
* Gere & Goodno (2018), *Mechanics of Materials* 9th ed., §9.3
Table 9-2 case 1 — SS beam UDL.
Cross-references (public verification manuals — fair-use
problem-ID citations only; no vendor content vendored):
* **Abaqus AVM 1.5.x** simply-supported-beam-UDL family.
* **NAFEMS** *Background to Benchmarks* §3.2 SS beam under UDL.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
import numpy as np
import pyvista as pv
import femorph_solver
from femorph_solver import ELEMENTS
from femorph_solver.validation._benchmark import (
BenchmarkProblem,
PublishedValue,
)
[docs]
@dataclass
class SimplySupportedBeamUDL(BenchmarkProblem):
"""SS beam mid-span deflection under uniformly-distributed load."""
name: str = "ss_beam_udl"
description: str = (
"Prismatic simply-supported beam with uniformly-distributed "
"transverse load — Euler-Bernoulli closed-form "
"(Timoshenko 1955 §5.6)."
)
analysis: str = "static"
L: float = 1.0
width: float = 0.05
height: float = 0.05
E: float = 2.0e11
nu: float = 0.3
rho: float = 7850.0
#: Uniformly distributed load along the span [N/m] (acts in -z).
q: float = 1.0e3
@property
def published_values(self) -> tuple[PublishedValue, ...]:
I = self.width * self.height**3 / 12.0 # noqa: E741
delta = 5.0 * self.q * self.L**4 / (384.0 * self.E * I)
return (
PublishedValue(
name="mid_span_deflection",
value=delta,
unit="m",
source="Timoshenko 1955 §5.6",
formula="delta_mid = 5 q L^4 / (384 E I)",
tolerance=0.05,
),
)
[docs]
def build_model(self, **mesh_params: Any) -> femorph_solver.Model:
nx = int(mesh_params.get("nx", 40))
ny = int(mesh_params.get("ny", 3))
nz = int(mesh_params.get("nz", 3))
xs = np.linspace(0.0, self.L, nx + 1)
ys = np.linspace(0.0, self.width, ny + 1)
zs = np.linspace(0.0, self.height, nz + 1)
grid = pv.StructuredGrid(
*np.meshgrid(xs, ys, zs, indexing="ij")
).cast_to_unstructured_grid()
m = femorph_solver.Model.from_grid(grid)
m.assign(
ELEMENTS.HEX8(integration="enhanced_strain"),
material={"EX": self.E, "PRXY": self.nu, "DENS": self.rho},
)
pts = np.asarray(m.grid.points)
# Knife-edge supports at both ends (same convention as the
# static SS beam central-load problem).
left_line = np.where((pts[:, 0] < 1e-9) & (pts[:, 2] < 1e-9))[0]
for n in left_line:
m.fix(nodes=int(n + 1), dof="UZ", value=0.0)
left_pin = np.where((pts[:, 0] < 1e-9) & (pts[:, 1] < 1e-9) & (pts[:, 2] < 1e-9))[0]
for n in left_pin:
m.fix(nodes=int(n + 1), dof="UX", value=0.0)
m.fix(nodes=int(n + 1), dof="UY", value=0.0)
right_pin = np.where((pts[:, 0] > self.L - 1e-9) & (pts[:, 1] < 1e-9) & (pts[:, 2] < 1e-9))[
0
]
for n in right_pin:
m.fix(nodes=int(n + 1), dof="UY", value=0.0)
right_line = np.where((pts[:, 0] > self.L - 1e-9) & (pts[:, 2] < 1e-9))[0]
for n in right_line:
m.fix(nodes=int(n + 1), dof="UZ", value=0.0)
# Distribute UDL across the top face (same trapezoid-rule
# convention as cantilever_udl).
top = np.where(pts[:, 2] > self.height - 1e-9)[0]
dx = self.L / nx
for n in top:
x = pts[n, 0]
col_weight = 0.5 if (x < 1e-9 or x > self.L - 1e-9) else 1.0
y = pts[n, 1]
y_weight = 0.5 if (y < 1e-9 or y > self.width - 1e-9) else 1.0
fz = -(self.q * dx * col_weight * y_weight) / ny
m.apply_force(int(n + 1), fz=fz)
# Top-face mid-span centerline for extract.
x_mid = 0.5 * self.L
m._bench_midspan_top_nodes = np.where( # type: ignore[attr-defined]
(np.abs(pts[:, 0] - x_mid) < 1e-9) & (pts[:, 2] > self.height - 1e-9)
)[0]
return m