Source code for femorph_solver.validation._problems.ss_beam_udl

"""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
[docs] def extract(self, model: femorph_solver.Model, result: Any, name: str) -> float: u = np.asarray(result.displacement).reshape(-1, 3) if name == "mid_span_deflection": mid = model._bench_midspan_top_nodes # type: ignore[attr-defined] return float(-u[mid, 2].mean()) raise KeyError(f"unknown quantity {name!r}")