Classical-lamination-theory ABD assembly

This is an algebraic verification of Hermit’s laminate preprocessing, rather than a finite-element benchmark. It independently forms each ply’s plane-stress reduced stiffness and transformed stiffness, then sums the classical A, B and D integrals through the thickness. Both an unsymmetric stack (which has nonzero extension–bending coupling B) and a symmetric stack are checked. The reference is the NumPy calculation below; hermit._laminate is intentionally not used to form it.

"""Classical-lamination-theory ABD assembly

This is an algebraic verification of Hermit's laminate preprocessing, rather than a
finite-element benchmark.  It independently forms each ply's plane-stress reduced
stiffness and transformed stiffness, then sums the classical ``A``, ``B`` and ``D``
integrals through the thickness.  Both an unsymmetric stack (which has nonzero
extension--bending coupling ``B``) and a symmetric stack are checked.  The reference
is the NumPy calculation below; ``hermit._laminate`` is intentionally not used to
form it.
"""

import pathlib
import sys

import numpy as np
import csdl_alpha as csdl
from caddee_materials import TransverseMaterial

import hermit as hm

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


E1, E2, NU12, G12 = 138e9, 10e9, 0.31, 7e9
STACKS = {"unsymmetric [0/45/90]": (0.0, 45.0, 90.0),
          "symmetric [0/45/90]s": (0.0, 45.0, 90.0, 90.0, 45.0, 0.0)}


def qbar(theta):
    """Plane-stress ``Qbar`` from first principles in engineering Voigt order."""
    nu21 = NU12 * E2 / E1
    d = 1.0 - NU12 * nu21
    q = np.array([[E1 / d, NU12 * E2 / d, 0.0],
                  [NU12 * E2 / d, E2 / d, 0.0],
                  [0.0, 0.0, G12]])
    c, s = np.cos(theta), np.sin(theta)
    teps = np.array([[c * c, s * s, s * c],
                     [s * s, c * c, -s * c],
                     [-2 * s * c, 2 * s * c, c * c - s * s]])
    tsig_inv = np.array([[c * c, s * s, -2 * s * c],
                         [s * s, c * c, 2 * s * c],
                         [s * c, -s * c, c * c - s * s]])
    return tsig_inv @ q @ teps


def hand_abd(angles, h=0.006):
    """Direct definitions of the CLT thickness integrals, bottom ply first."""
    z = np.linspace(-h / 2, h / 2, len(angles) + 1)
    a = np.zeros((3, 3)); b = np.zeros((3, 3)); d = np.zeros((3, 3))
    for theta, z0, z1 in zip(np.radians(angles), z[:-1], z[1:]):
        qb = qbar(theta)
        a += qb * (z1 - z0)
        b += qb * (z1**2 - z0**2) / 2
        d += qb * (z1**3 - z0**3) / 3
    return a, b, d


def solve_at(_):
    """Maximum normalized difference of Hermit's and independently assembled ABD."""
    rec = csdl.Recorder(inline=True); rec.start()
    domain = hm.ShellDomain(rect_plate(1.0, 1.0, nx=1, ny=1))
    ply = TransverseMaterial(name="ud", EA=E1, ET=E2, vA=NU12, vT=0.4,
                              GA=G12, density=1600.0)
    errors = []
    for name, angles in STACKS.items():
        layup = hm.Layup(ply, np.radians(angles), np.full(len(angles), 0.006 / len(angles)),
                          num_plies=len(angles))
        material = hm.laminate(domain, layup=layup, density=1600.0)
        want = hand_abd(angles)
        got = tuple(np.asarray(field.cell_values().value).reshape(3, 3) for field in
                    (material.A, material.B, material.D))
        errors.extend(np.max(np.abs(x - y)) / np.max(np.abs(y)) for x, y in zip(got, want))
        print(f"    {name:24s}  ||B||/||A||={np.linalg.norm(want[1]) / np.linalg.norm(want[0]):.3e}")
    rec.stop()
    return max(errors)


CASE = Case(
    name="CLT ABD assembly",
    quantity="maximum normalized A/B/D assembly discrepancy",
    reference=0.0,
    tolerance=1e-10,
    citation="independent NumPy evaluation of the CLT thickness integrals in this file",
    levels=(1,), quick_level=1, solve=solve_at, monotone=False,
    notes="one algebraic level; the unsymmetric stack deliberately exercises B",
)

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