Files
OneCAD-Simulation/tests/oracle/scordelis-lo-roof-shell
Erkin Alp Güney 18be60672b verification: activate the two shell benchmark cases (#106)
The Scordelis-Lo roof and the pinched cylinder had their published
references cited but no recipe, because mesh.block's 2D form is planar
and cannot build a curved patch. Both are now active: the mesh is
written node by node (mesh.node.add / mesh.element.add, the route
docs/verification.md allows), the same cylinder patch the solver's own
benchmarks.rs builds in code, with the quarter/eighth symmetry planes,
the rigid diaphragm and the published load.

- scordelis-lo-roof-shell: 16x16 flat quad4 (MITC4) shells, R = 25,
  t = 0.25, self-weight 90 per area; u_z at the free-edge mid-span
  -0.3024 within 2% (ours -0.29901009815961804, 1.12% low).
- pinched-cylinder: 32x32 flat quad4 shells, R = 300, t = 3, a quarter
  of the unit pinch; u under the load -1.8248e-5 within 2% (ours
  -1.803845342860719e-05, 1.15% low).

Each README carries the generator (so the recipe is reproducible), the
refinement study the tolerance reason cites, and how the reference was
cited before. pinched-hemisphere and mh-twisted-beam now say what they
actually wait for: a doubly curved or helical shell mesh written node
by node, not missing shell elements.
2026-10-17 07:25:44 -04:00
..

Scordelis–Lo roof with shell elements

Our model of the published problem: a cylindrical roof of radius 25, length 50 and thickness 0.25, spanning ±40°, E = 4.32 × 10⁸, ν = 0, carrying its own weight (90 per area, a body force 90/t = 360 per volume), supported by rigid diaphragms at its curved ends and free along its straight edges. This is scordelis-lo-roof-solid with flat quad4 (MITC4) shells in place of the hex20 layer, so the same reference is checked by two element routes; the reference value, 0.3024 for the vertical deflection at the middle of a free edge, is the one the case already cited (MacNeal and Harder, A proposed standard set of problems to test finite element accuracy, FEAD 1 (1985) 3–20, Fig. 8, Table 5(a); Belytschko et al., CMAME 51 (1985) 221–258).

The quarter model: x from the mid-span (x = 0, symmetry in x) to the diaphragm (x = 25, u_y = u_z = rot_x = 0), φ from the crown (φ = 0, symmetry in y) to the free edge at 40°, with R sin φ = y and R cos φ = z on the mid-surface R = 25. The measured node is the free-edge mid-span node (0, 16.0697, 19.1511), the node the solid case measures too.

The mesh

mesh.block's 2D form takes its four corners at z = 0, so it cannot mesh a patch on a cylindrical surface (it can be curved in its own plane, but it is planar): the recipe writes the 289 nodes and 256 quad4 elements one by one with mesh.node.add and mesh.element.add, the route docs/verification.md allows ("nodes and elements written one by one"). Node 1 + i + 17 j lies at (25 i/16, 25 sin(40° j/16), 25 cos(40° j/16)); element 1 + i + 16 j is [node(i, j), node(i + 1, j), node(i + 1, j + 1), node(i, j + 1)], counter-clockwise seen from outside, so its normal points away from the axis — the same mesh the solver's own Scordelis–Lo benchmark builds in code (crates/solver/tests/benchmarks.rs), so the command route and the in-code route are comparable.

The recipe was generated with this script (python3 cylinder_recipe.py scordelis-lo 16):

import json, math

def scordelis_lo(n):
    label = lambda i, j: 1 + i + (n + 1) * j
    commands = []
    for j in range(n + 1):
        for i in range(n + 1):
            x, phi = 25.0 * i / n, math.radians(40.0) * j / n
            commands.append({"command": "mesh.node.add",
                             "params": {"label": label(i, j),
                                        "xyz": [x, 25.0 * math.sin(phi), 25.0 * math.cos(phi)]}})
    for j in range(n):
        for i in range(n):
            nodes = [label(i, j), label(i + 1, j), label(i + 1, j + 1), label(i, j + 1)]
            commands.append({"command": "mesh.element.add",
                             "params": {"label": 1 + i + n * j, "kind": "quad4", "nodes": nodes}})
    return commands + [
        {"command": "model.set.create", "params": {"name": "shell", "elements": list(range(1, n * n + 1))}},
        {"command": "model.set.create", "params": {"name": "diaphragm", "nodes": [label(n, j) for j in range(n + 1)]}},
        {"command": "model.set.create", "params": {"name": "midspan", "nodes": [label(0, j) for j in range(n + 1)]}},
        {"command": "model.set.create", "params": {"name": "crown", "nodes": [label(i, 0) for i in range(n + 1)]}},
        {"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 4.32e8, "poisson": 0.0}}},
        {"command": "model.section.create", "params": {"name": "shell", "set": "shell", "material": "Mat", "kind": {"type": "shell", "thickness": 0.25}}},
        {"command": "model.boundary_condition.create", "params": {"name": "diaphragm", "type": "displacement", "target": {"set": "diaphragm"}, "values": [None, 0, 0, 0, None, None]}},
        {"command": "model.boundary_condition.create", "params": {"name": "midspan", "type": "symmetry", "target": {"set": "midspan"}, "plane": "x"}},
        {"command": "model.boundary_condition.create", "params": {"name": "crown", "type": "symmetry", "target": {"set": "crown"}, "plane": "y"}},
        {"command": "model.load.create", "params": {"name": "weight", "type": "body_force", "set": "shell", "vector": [0, 0, -360.0]}},
        {"command": "model.step.create", "params": {"name": "Load", "procedure": {"type": "static"}, "boundary_conditions": ["diaphragm", "midspan", "crown"], "loads": ["weight"]}},
        {"command": "solve.static", "params": {}, "id": "solve"},
    ]

print("[\n " + ",\n ".join(json.dumps(c) for c in scordelis_lo(16)) + "\n]")

Refinement

The flat MITC4 shell approaches the reference from below and keeps closing the gap, so the mesh error is inside the tolerance and shrinking. Every row is the listing above with the 16 in its last line replaced by the row's divisions and run through cargo xtask oracle as a scratch case; the case mesh is 16 × 16:

cells u_z error
4 × 4 −0.28459 −5.89%
8 × 8 −0.29401 −2.77%
16 × 16 (this case) −0.29901009815961804 −1.12%
32 × 32 −0.30060 −0.60%
64 × 64 −0.30118 −0.40%
128 × 128 −0.30143 −0.32%

The solid case of the same roof reports −0.3014 on its 16 × 16 × 1 hex20 mesh, i.e. 0.33% low: two element formulations, two meshes, one reference.

Tolerance

2%, the grading the sibling solid case already records for this problem from MacNeal and Harder (an element is graded within 2% of the theoretical value on a prescribed coarse mesh, and their normalised target is 0.3024). 16 × 16 is our choice of mesh — the one the solver's in-code gate test uses — and it is 1.12% low; its own next refinement (32 × 32) moves the value by 0.53% of the reference, a quarter of the tolerance, on a mesh error that falls monotonically. The tolerance is the source's grading, not a cover for the discretisation error.

fixture.json is the accepted value at 16 × 16 (−0.29901009815961804) with the runner's default band: the mesh is written by the recipe, not generated from geometry, so it is bit-identical on every platform and the result is frozen.

Running it

cargo xtask oracle --case scordelis-lo-roof-shell
cargo xtask oracle --bless scordelis-lo-roof-shell   # only after the case passes