Source code for femorph_solver.validation._problems.clamped_plate_static

"""Clamped square plate under uniform pressure — NAFEMS LE5.

Companion to :class:`SSPlateStatic` with all four edges
**clamped** (zero deflection AND zero rotation along each edge),
loaded by a uniform transverse pressure :math:`q`.

The maximum centre deflection of a clamped square plate has no
elementary closed form but is published from the converged
double-Fourier solution as

.. math::

    w_{\\text{max}} \\approx 0.00126 \\, \\frac{q\\, a^{4}}{D},
    \\qquad D = \\frac{E\\, h^{3}}{12 (1 - \\nu^{2})}.

The 0.00126 factor is the NAFEMS R0015 reference value (also in
Timoshenko & Woinowsky-Krieger §32 Table 12).  This is a
factor of ~3.2 stiffer than the simply-supported plate (whose
0.00406 factor sits in :class:`SSPlateStatic`).

References
----------
* Timoshenko, S. P. and Woinowsky-Krieger, S. (1959) *Theory of
  Plates and Shells*, 2nd ed., McGraw-Hill, §32 Table 12 —
  clamped-plate deflection coefficients.
* NAFEMS, *Standard Benchmark Tests for Linear Elastic
  Analysis*, R0015 (1990), §2.5 LE5 "Clamped square plate".
* Cook, R. D., Malkus, D. S., Plesha, M. E., Witt, R. J. (2002)
  *Concepts and Applications of Finite Element Analysis*, 4th
  ed., Wiley, §12.5.

Cross-references (public verification manuals — fair-use
problem-ID citations only; no vendor content vendored):

* **Abaqus AVM 1.4.1** clamped-plate-pressure family.

Implementation notes
--------------------

Uses a HEX8 enhanced-strain hex slab — same kernel +
through-thickness convention as :class:`SSPlateStatic`.  All
four edge faces are fully clamped (every DOF pinned), so the
plate models the textbook fully-clamped boundary exactly.
"""

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 ClampedPlateStatic(BenchmarkProblem): """Clamped square plate under uniform pressure (NAFEMS LE5).""" name: str = "clamped_plate_static" description: str = ( "Clamped square Kirchhoff plate under uniform pressure — " "NAFEMS LE5 / Timoshenko & Woinowsky-Krieger 1959 §32." ) analysis: str = "static" a: float = 1.0 # edge length [m] h: float = 0.02 # thickness [m]; L/h = 50 E: float = 2.0e11 nu: float = 0.3 rho: float = 7850.0 q: float = 1.0e5 # uniform pressure [Pa] @property def published_values(self) -> tuple[PublishedValue, ...]: D = self.E * self.h**3 / (12.0 * (1.0 - self.nu**2)) # Timoshenko Table 12 (clamped-edge square plate, ν = 0.3): # w_max coefficient ≈ 0.00126. w_max = 0.00126 * self.q * self.a**4 / D return ( PublishedValue( name="w_max", value=w_max, unit="m", source="Timoshenko & Woinowsky-Krieger 1959 §32 Table 12", formula="w_max = 0.00126 q a^4 / D, D = E h^3 / (12(1-ν²))", # 60 × 60 × 2 HEX8-EAS recovers the Kirchhoff w_max to # within ~3 % on a/h = 50. Setting the tolerance to 5 % # gives one full doubling of headroom over the observed # error — dense enough for the verification table to # mean something while still fitting the # NAFEMS-LE5-style engineering bound. Tighter # convergence (~1 %) needs a dedicated shell kernel # (QUAD4_SHELL). tolerance=0.05, ), )
[docs] def build_model(self, **mesh_params: Any) -> femorph_solver.Model: # 60 × 60 in-plane drops the single-element discretisation error # at a/h = 50 from ~10 % (20 × 20) to ~3 %. Through-thickness # stays at 2 layers — adding more does not improve accuracy # because the Mindlin shear correction at this aspect ratio is # well below the in-plane discretisation residual. nx = int(mesh_params.get("nx", 60)) ny = int(mesh_params.get("ny", 60)) nz = int(mesh_params.get("nz", 2)) xs = np.linspace(0.0, self.a, nx + 1) ys = np.linspace(0.0, self.a, ny + 1) zs = np.linspace(0.0, self.h, 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) tol = 1e-9 # Clamp all four edge faces — every DOF pinned. edge = ( (np.abs(pts[:, 0]) < tol) | (np.abs(pts[:, 0] - self.a) < tol) | (np.abs(pts[:, 1]) < tol) | (np.abs(pts[:, 1] - self.a) < tol) ) m.fix(nodes=(np.where(edge)[0] + 1).tolist(), dof="ALL") # Uniform pressure on top face — same lumped-load # convention as SSPlateStatic. top = np.where(np.abs(pts[:, 2] - self.h) < tol)[0] f_per_node = -self.q * (self.a * self.a) / len(top) for n_idx in top: m.apply_force(int(n_idx + 1), fz=f_per_node) m._bench_pts = pts # type: ignore[attr-defined] return m
[docs] def extract(self, model: femorph_solver.Model, result: Any, name: str) -> float: if name != "w_max": raise KeyError(f"unknown quantity {name!r}") u = np.asarray(result.displacement).reshape(-1, 3) pts = model._bench_pts # type: ignore[attr-defined] centre = np.array([self.a / 2, self.a / 2, self.h / 2]) idx = int(np.argmin(np.linalg.norm(pts - centre, axis=1))) return float(-u[idx, 2])