Source code for femorph_solver.validation._problems.propped_cantilever

"""Propped cantilever — fixed end + simple support under central load.

A propped cantilever is a beam clamped at one end
(``x = 0``) and simply supported at the other (``x = L``) —
a common statically-indeterminate case that needs
compatibility to resolve the reactions.

Under a central point load :math:`P` at ``x = L/2``:

* Mid-span deflection:

  .. math::

      \\delta_{\\text{mid}} = \\frac{7 P L^{3}}{768 E I}.

* Reactions:

  .. math::

      R_A &= \\frac{11 P}{16} \\text{ (fixed end)}, \\\\
      R_B &= \\frac{5 P}{16}  \\text{ (simple end)}.

* Fixed-end moment:

  .. math::

      M_A = -\\frac{3 P L}{16}.

References
----------
* Gere, J. M. and Goodno, B. J.  *Mechanics of Materials*,
  9th ed., Cengage, 2018, §10.3 Table 10-1 Case 6 —
  propped cantilever deflection formulas.
* Timoshenko, S. P.  *Strength of Materials, Part I*, 3rd ed.,
  Van Nostrand, 1955, §5.8 — statically-indeterminate beams.

Cross-references (public verification manuals — fair-use
citation of problem IDs only; no vendor content vendored):

* **Abaqus AVM 1.5.x** propped-cantilever family.
"""

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 ProppedCantileverCentralLoad(BenchmarkProblem): """Propped cantilever mid-span deflection under central point load.""" name: str = "propped_cantilever_central_load" description: str = ( "Fixed-pinned beam with central transverse point load — " "Euler-Bernoulli closed-form " "(Gere & Goodno 2018 §10.3 Table 10-1 Case 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 P: float = 1.0e3 @property def published_values(self) -> tuple[PublishedValue, ...]: I = self.width * self.height**3 / 12.0 # noqa: E741 delta = 7.0 * self.P * self.L**3 / (768.0 * self.E * I) return ( PublishedValue( name="mid_span_deflection", value=delta, unit="m", source="Gere & Goodno 2018 §10.3 Table 10-1 Case 6", formula="delta_mid = 7 P L^3 / (768 E I)", tolerance=0.05, ), )
[docs] def build_model(self, **mesh_params: Any) -> femorph_solver.Model: nx = int(mesh_params.get("nx", 40)) if nx % 2 != 0: nx += 1 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) # Fixed end: full clamp on the left face x = 0. left_face = np.where(pts[:, 0] < 1e-9)[0] m.fix(nodes=(left_face + 1).tolist(), dof="ALL") # Simple support at the right end (roller, UZ pinned along # the bottom-line knife-edge); axial UX left free to avoid # locking out axial strain. UY pinned at a single corner # node to kill the lateral rigid-body mode. 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) right_y_pin = np.where( (pts[:, 0] > self.L - 1e-9) & (pts[:, 1] < 1e-9) & (pts[:, 2] < 1e-9) )[0] for n in right_y_pin: m.fix(nodes=int(n + 1), dof="UY", value=0.0) # Central point load: mid-span bottom-line nodes. x_mid = 0.5 * self.L load_nodes = np.where((np.abs(pts[:, 0] - x_mid) < 1e-9) & (pts[:, 2] < 1e-9))[0] fz_per = -self.P / len(load_nodes) for n in load_nodes: m.apply_force(int(n + 1), fz=fz_per) # Extract from the top-face centerline to sidestep the # local-load stress concentration. m._bench_midspan_top_nodes = np.where( (np.abs(pts[:, 0] - x_mid) < 1e-9) & (pts[:, 2] > self.height - 1e-9) )[0] # type: ignore[attr-defined] 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}")