Standard tests for shells and their convergence behavior¶

Open In Colab Binder

In [1]:
import matplotlib.pyplot as plt
import meshio
import numpy as np
import torch
from scipy.interpolate import RegularGridInterpolator

from torchfem import Shell
from torchfem.data import get_data
from torchfem.materials import IsotropicElasticityPlaneStress
from torchfem.mesh import rect_quad, rect_tri

torch.set_default_dtype(torch.float64)

Fully clamped plate problem¶

image.png

P.S. Lee, H.C. Noh, K.J. Bathe: "Insight into 3-node triangular shell finite elements: the effects of element isotropy and mesh patterns", Computers and Structures, 2006, pp. 404-418, 10.1016/j.compstruc.2006.10.006

R.H. MacNeal and R.L. Harder: "A proposed standard set of problems to Test Finite Element Accuracy", Finite Elements in Analysis and Design I, 1985, pp. 3-20.

In [2]:
q = 1.0e-4  # Distributed load per unit area
E = 1.7472e7  # Young's modulus
nu = 0.3  # Poisson's ratio
t = 0.0001  # Thickness of the shell
In [3]:
# Extract reference solution obtained from Abaqus with 48x48 S8R elements
reference = meshio.read(get_data("clamped_plate_uniform_S8R.vtk"))
M = 49
x = reference.points[: M**2, 0]
y = reference.points[: M**2, 1]
w = reference.point_data["U"][: M**2, 2]

# Interpolate reference solution to nodes
interp = RegularGridInterpolator((np.unique(x), np.unique(y)), w.reshape(M, M))
In [4]:
N = [4, 8, 16, 32]
variants = ("up", "down", "center", "quad")
mesh_sizes = {variant: [] for variant in variants}
errors = {variant: [] for variant in variants}

for variant in variants:
    for n in N:
        # Material parameters
        material = IsotropicElasticityPlaneStress(E=E, nu=nu)

        # Define nodes and element of the plate
        if variant == "quad":
            nodes, elements = rect_quad(n + 1, n + 1)
        else:
            nodes, elements = rect_tri(n + 1, n + 1, variant=variant)
        nodes = torch.hstack([nodes, torch.zeros((len(nodes), 1))])

        # Define Shell model
        plate = Shell(nodes, elements, material, thickness=t)

        # Apply a uniform pressure in the negative z-direction
        surface = torch.ones(plate.n_nod, dtype=torch.bool)
        plate.forces[:, 0:3] = plate.integrate_surface_load(surface, -q)

        # Apply displacement boundary conditions
        plate.constraints[nodes[:, 0] < 0.01, :] = True
        plate.constraints[nodes[:, 1] < 0.01, :] = True
        plate.constraints[nodes[:, 0] > 0.99, 0] = True
        plate.constraints[nodes[:, 0] > 0.99, 4] = True
        plate.constraints[nodes[:, 1] > 0.99, 1] = True
        plate.constraints[nodes[:, 1] > 0.99, 3] = True

        # Solve
        u, f, _, _, _ = plate.solve(method="direct")

        # Compute error with respect to reference solutio
        w_i = interp(nodes[:, :2])
        error = np.linalg.norm(w_i - u[:, 2].numpy()) / np.linalg.norm(w_i)

        # Store error and mesh size
        errors[variant].append(error)
        mesh_sizes[variant].append(1 / n)
In [5]:
fig, ax = plt.subplots(1, len(variants), figsize=(12, 3))
for i, variant in enumerate(variants):
    logh = np.log10(mesh_sizes[variant])
    loge = np.log10(errors[variant])
    ax[i].plot(logh, loge, marker="o")
    ax[i].plot(logh, 2 * logh, "k")
    ax[i].grid(True)
    ax[i].set_xlabel("log(h)")
    ax[i].set_ylabel("log(E)")
    ax[i].set_title(f"Variant: {variant}")

plt.tight_layout()
plt.show()
No description has been provided for this image

Cantilever bending plate problem¶

image.png

P.S. Lee, H.C. Noh, K.J. Bathe: "Insight into 3-node triangular shell finite elements: the effects of element isotropy and mesh patterns", Computers and Structures, 2006, pp. 404-418, 10.1016/j.compstruc.2006.10.006

In [6]:
m = 1.0  # Distributed moment per length
E = 1.7472e7  # Young's modulus
nu = 0.0  # Poisson's ratio
t = 0.0001  # Thickness of the shell
In [7]:
# Material parameters
material = IsotropicElasticityPlaneStress(E=E, nu=nu)

# Define nodes and element of the plate
nodes = torch.tensor(
    [[1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
)
connectivities = {
    "Tria1": torch.tensor([[0, 1, 2], [1, 2, 3]]),
    "Quad1": torch.tensor([[0, 1, 3, 2]]),
}

for etype, elements in connectivities.items():
    # Define Shell model
    plate = Shell(nodes, elements, material, thickness=t)

    # Boundary conditions
    tip = nodes[:, 1] > 0.99
    plate.constraints[~tip, :] = True
    plate.forces[tip, 3] = m

    u, f, _, _, _ = plate.solve(method="direct")

    # Analytical solution with M = 2m, D = E t^3 / 12: w = M L^2 / (2D), rot_x = M L / D
    # Both element types reproduce this state of constant curvature exactly.
    assert torch.allclose(u[tip, 2], 686813.19 * torch.ones_like(u[tip, 2]))
    assert torch.allclose(u[tip, 3], 1373626.37 * torch.ones_like(u[tip, 3]))
    assert u[tip, 4].abs().max() < 1e-6 * u[tip, 3].abs().max()

# Plot the quadrilateral solution
plate.plot(node_property={"U": u[:, :3]})

Two-sided clamped plate¶

image.png

P.S. Lee, H.C. Noh, K.J. Bathe: "Insight into 3-node triangular shell finite elements: the effects of element isotropy and mesh patterns", Computers and Structures, 2006, pp. 404-418, 10.1016/j.compstruc.2006.10.006

In [8]:
m = 1.0  # Distributed moment per length
E = 1.7472e7  # Young's modulus
nu = 0.0  # Poisson's ratio
In [9]:
variants = ("up", "down", "center")

# Analytical strain energy is 12 m / (E t^2). The "up" and "down" patterns reproduce
# it, while "center" is 23% too high.
expected = {
    ("up", 0.001): 0.686814,
    ("up", 0.0001): 68.681320,
    ("down", 0.001): 0.686813,
    ("down", 0.0001): 68.681319,
    ("center", 0.001): 0.845311,
    ("center", 0.0001): 84.530856,
}

# Material parameters
material = IsotropicElasticityPlaneStress(E=E, nu=nu)

for variant in variants:
    for t in [0.001, 0.0001]:
        # Define nodes and element of the plate
        nodes, elements = rect_tri(2, 2, variant=variant)
        nodes = torch.hstack([nodes, torch.zeros((len(nodes), 1))])

        # Define Shell model
        plate = Shell(nodes, elements, material, thickness=t)

        # Boundary conditions
        left = nodes[:, 1] < 0.01
        right = nodes[:, 1] > 0.99
        back = nodes[:, 0] < 0.01
        front = nodes[:, 0] > 0.99
        plate.constraints[left, :] = True
        plate.constraints[back, :] = True
        plate.forces[right, 3] = m
        plate.forces[front, 4] = -m

        u, f, _, _, _ = plate.solve(method="direct")

        strain_energy = 0.5 * t * torch.einsum("...i, ...i->", u, f)
        assert torch.allclose(strain_energy, torch.tensor(expected[(variant, t)]))

Scordelis-Lo roof¶

A cylindrical roof under its own weight, carried by rigid diaphragms at its ends and free along its straight edges. Two symmetry planes reduce it to a quarter. Unlike the flat plates above, the element normals vary across the mesh, so the drilling rotation takes part in the response.

The deflection at the midpoint of the free edge is normalized on the classical reference 0.3024. A shell model without shear deformation converges to about 0.3006 instead, so this reference is a slight overestimate for a thin roof.

R.H. MacNeal and R.L. Harder: "A proposed standard set of problems to Test Finite Element Accuracy", Finite Elements in Analysis and Design I, 1985, pp. 3-20.

P. Krysl: "Robust flat-facet triangular shell finite element", International Journal for Numerical Methods in Engineering, 2022, pp. 2399-2423, 10.1002/nme.6944

In [10]:
R = 25.0  # Radius of the cylinder
L = 50.0  # Length of the cylinder
opening = 40.0  # Half opening angle in degrees
g = 90.0  # Gravity load per unit area
E = 4.32e8  # Young's modulus
nu = 0.0  # Poisson's ratio
t = 0.25  # Thickness of the shell
w_ref = 0.3024  # Reference deflection at the midpoint of the free edge
In [11]:
N = [4, 8, 16, 32]
variants = ("up", "down", "center", "quad")
deflections = {variant: [] for variant in variants}
material = IsotropicElasticityPlaneStress(E=E, nu=nu)


def build_roof(n, variant):
    """Quarter roof, meshed by wrapping a unit square onto the cylinder."""
    if variant == "quad":
        nodes, elements = rect_quad(n + 1, n + 1)
    else:
        nodes, elements = rect_tri(n + 1, n + 1, variant=variant)
    angle = torch.deg2rad(opening * nodes[:, 0])
    nodes = torch.stack(
        [R * torch.sin(angle), R * torch.cos(angle), L / 2 * nodes[:, 1]], dim=1
    )
    roof = Shell(nodes, elements, material, thickness=t)

    # Apply gravity in the negative y-direction
    surface = torch.ones(roof.n_nod, dtype=torch.bool)
    load_vec = torch.tensor([0.0, -g, 0.0])
    roof.forces[:, 0:3] = roof.integrate_surface_load(surface, load_vec)

    # Symmetry at midspan, rigid diaphragm at the end, symmetry at the crown
    x, z = nodes[:, 0], nodes[:, 2]
    roof.constraints[z < 1e-6, 2:5] = True
    roof.constraints[z > L / 2 - 1e-6, 0:2] = True
    roof.constraints[x < 1e-6, 0] = True
    roof.constraints[x < 1e-6, 4:6] = True
    return roof


for variant in variants:
    for n in N:
        roof = build_roof(n, variant)
        u, *_ = roof.solve(method="direct")

        # Deflection at the midpoint of the free edge
        x, z = roof.nodes[:, 0], roof.nodes[:, 2]
        edge = (z < 1e-6) & (x > x.max() - 1e-6)
        deflections[variant].append(-u[edge, 1].item() / w_ref)

# Every variant should approach the reference from below.
for variant, w in deflections.items():
    assert w == sorted(w) and 0.98 < w[-1] < 1.0
In [12]:
fig, ax = plt.subplots(figsize=(5, 3.5))
for variant in variants:
    ax.plot(N, deflections[variant], marker="o", label=variant)
ax.axhline(1.0, color="k", linewidth=0.8)
ax.set_xscale("log", base=2)
ax.set_xticks(N, [str(n) for n in N])
ax.grid(True)
ax.set_xlabel("Elements per side")
ax.set_ylabel("Normalized deflection")
ax.legend()

plt.tight_layout()
plt.show()
No description has been provided for this image
In [13]:
roof = build_roof(32, "quad")
u, *_ = roof.solve()
roof.plot(20.0 * u[:, 0:3], node_property={"u": u[:, 1]}, mirror=(True, False, True))