MacNeal-Harder curved beam (out-of-plane load)

A 90-degree curved beam with inner radius 4.12, outer radius 4.32, and thickness 0.1, lying in the xy plane. E = 1e7, nu = 0.25. The beam is clamped at one end (theta=0) and loaded by a unit tip load at the free end (theta=90). This is deliberately a very high aspect ratio, single-element-wide strip, providing a severe test for shear and membrane locking.

There are two load cases: in-plane and out-of-plane, with references 0.08734 and 0.5022 respectively. Because the beam lies in the xy-plane (z=0), an out-of-plane load unequivocally acts in the global z-direction. The in-plane load could be applied in various directions (e.g., radial or tangential), making its definition slightly more ambiguous. Therefore, this file uses the out-of-plane (z-direction) load case as it is structurally unambiguous. A load applied uniformly across the free tip’s nodes is used.

conda activate hermit
python examples/verification/ex_curved_beam.py
python examples/verification/ex_curved_beam.py --quick
"""MacNeal-Harder curved beam (out-of-plane load)

A 90-degree curved beam with inner radius 4.12, outer radius 4.32, and thickness 0.1,
lying in the xy plane. E = 1e7, nu = 0.25. The beam is clamped at one end (theta=0)
and loaded by a unit tip load at the free end (theta=90).
This is deliberately a very high aspect ratio, single-element-wide strip, providing 
a severe test for shear and membrane locking.

There are two load cases: in-plane and out-of-plane, with references 0.08734 and 
0.5022 respectively. 
Because the beam lies in the xy-plane (z=0), an out-of-plane load unequivocally acts 
in the global z-direction. The in-plane load could be applied in various directions 
(e.g., radial or tangential), making its definition slightly more ambiguous. 
Therefore, this file uses the out-of-plane (z-direction) load case as it is structurally 
unambiguous. A load applied uniformly across the free tip's nodes is used.

    conda activate hermit
    python examples/verification/ex_curved_beam.py
    python examples/verification/ex_curved_beam.py --quick
"""

import pathlib
import sys

import numpy as np
import csdl_alpha as csdl

import hermit as hm

sys.path.insert(0, str(pathlib.Path(__file__).parent))
from _geometry import curved_beam                        # noqa: E402
from _harness import Case, main, node_nearest            # noqa: E402

R_INNER, R_OUTER = 4.12, 4.32
THICKNESS = 0.1
E, NU, LOAD = 1e7, 0.25, 1.0
REFERENCE = 0.5022


def solve_at(n):
    """Out-of-plane (z-direction) tip deflection on a 1 x ``n`` quad mesh."""
    # curved_beam uses nr for radial elements, ns for tangential (sweep) elements
    mesh = curved_beam(R_INNER, R_OUTER, sweep=90.0, nr=1, ns=n, cell="quad")

    rec = csdl.Recorder(inline=True)
    rec.start()

    domain = hm.ShellDomain(mesh, element="CG2CG1")
    material = hm.isotropic(domain, E=E, nu=NU, thickness=THICKNESS, density=1.0)

    # Clamped at theta=0 (which corresponds to y=0 and x>0)
    bcs = hm.clamp(domain, where=lambda x: np.isclose(x[1], 0.0) & (x[0] > 0))

    # Apply total unit load at the free end (theta=90, corresponds to x=0 and y>0).
    # We spread the load statically equivalent across the nodes at the free tip.
    xyz = domain.node_coords
    tip_nodes = np.flatnonzero(np.isclose(xyz[:, 0], 0.0) & (xyz[:, 1] > 0))
    
    load = None
    for k in tip_nodes:
        p = hm.point_load(domain, at=xyz[k], force=[0.0, 0.0, LOAD / len(tip_nodes)])
        load = p if load is None else load + p
        
    state = hm.solve(domain, material, load, bcs)
    u = hm.nodal_displacement(state).value.reshape(-1, 3)

    rec.stop()

    # The tip deflection is usually measured at the centroid of the free end
    k, dist = node_nearest(domain, [0.0, (R_INNER + R_OUTER) / 2.0, 0.0])
    if dist > 1e-9:
        print(f"    (note: sample node is {dist:.2e} from the nominal point at n={n})")
    
    return abs(u[k, 2])


CASE = Case(
    name="MacNeal-Harder curved beam (out-of-plane tip load)",
    quantity="tip deflection in z",
    reference=REFERENCE,
    tolerance=0.02,
    citation="MacNeal & Harder, 'A proposed standard set of problems to test finite "
             "element accuracy', Finite Elements in Analysis and Design 1(1), 1985",
    levels=(6, 12, 24, 48),
    quick_level=24,
    solve=solve_at,
    notes="single-element-wide strip is a severe test for locking; converges from below to ~0.482 (error ~3.9% at centroid)",
    monotone=True,
    open_finding=(
        "Ruled out a shared cause with Cook's membrane (which was purely a measurement point error). "
        "For this curved beam, measuring at the inner radius instead of the centroid exaggerated the error "
        "(0.476 vs 0.482). The remaining deficit at the centroid (plateaus at ~0.482, 3.9% low) is a genuine "
        "element limitation on this deliberately extreme 1-element-wide strip. Refining the width (nr=4) "
        "closes the gap to 0.492 (1.9% error), and the in-plane load case converges to ~0.0885 (1.3% error). "
        "So this is a specific limitation for out-of-plane twisting on extreme aspect ratios, not a general "
        "membrane offset."
    ),
)

if __name__ == "__main__":
    main(CASE)