Skip to content

visualdynamics.core.shells

shells

Shells: solids meshed into plates (Mesh Solids, Shells).

A flat solid, a plate or a disk with any holes subtracted from it, is meshed on its own plane. A solid with a volume is meshed on its outer surface. Either way the elements are four-node plates or three-node triangles, in an element group named for the part, and their thickness is set on that group in the Element Groups table.

A flat part is meshed by the brick mesher, so its holes come out round as a brick mesh's do: the part is laid out in its own frame as a slab one element thick (a plate a block, a disk a cylinder), each solid cut from it run through the slab, and the bottom face of each brick, moved onto the plane, is a plate. An outer surface is the faces of the part's bricks, or of its tetrahedra for triangles, that no second element shares; the tetrahedra keep their slivers for it, so the surface has no dimple where one would have been removed.

Functions:

Name Description
shell_mesh

The parts as plates, ready to join to a geometry (mesh.join).

mesh_size

The element size the parts are meshed into shells at by default:

flat_scratch

The flat part laid out in its own frame as a slab thickness

Functions:

shell_mesh

shell_mesh(geometry: Any, ids, size: float, triangles: bool = False, progress: Any = None) -> tuple[Any, dict]

The parts as plates, ready to join to a geometry (mesh.join).

A flat part (solids.is_flat) is meshed on its plane, and a part with a volume on its outer surface. Each part gets one element group, named for it. Parts that meet share their nodes where their meshes' nodes coincide.

Parameters:

Name Type Description Default
geometry Geometry

Whose solids.

required
ids sequence of int

The parts, by id; each a solid nothing else uses.

required
size float

The element size aimed at, as the geometry holds its coordinates.

required
triangles bool

Three-node triangles rather than four-node plates. A flat part's quads are each split on the shorter diagonal; a part with a volume takes the surface of a tetrahedral mesh.

False
progress callable

Told (done, total) as an outer surface's volume mesh goes.

None

Returns:

Type Description
(Geometry, dict)

The plates, and what was made: 'method' ('shells'), 'elements', 'nodes', 'flat' (how many parts were meshed on their plane) and 'surfaces' (how many on their outer surface).

Source code in src/visualdynamics/core/shells.py
def shell_mesh(geometry: Any, ids, size: float, triangles: bool = False,
               progress: Any = None) -> tuple[Any, dict]:
    """The parts as plates, ready to join to a geometry (`mesh.join`).

    A flat part (`solids.is_flat`) is meshed on its plane, and a part
    with a volume on its outer surface. Each part gets one element
    group, named for it. Parts that meet share their nodes where their
    meshes' nodes coincide.

    Parameters
    ----------
    geometry : Geometry
        Whose solids.
    ids : sequence of int
        The parts, by id; each a solid nothing else uses.
    size : float
        The element size aimed at, as the geometry holds its
        coordinates.
    triangles : bool, default False
        Three-node triangles rather than four-node plates. A flat part's
        quads are each split on the shorter diagonal; a part with a
        volume takes the surface of a tetrahedral mesh.
    progress : callable, optional
        Told ``(done, total)`` as an outer surface's volume mesh goes.

    Returns
    -------
    (Geometry, dict)
        The plates, and what was made: 'method' ('shells'), 'elements',
        'nodes', 'flat' (how many parts were meshed on their plane) and
        'surfaces' (how many on their outer surface).
    """
    from .mesh import join
    from .solids import is_flat, refuse_parts_of_another

    ids = [int(i) for i in ids]
    refuse_parts_of_another(geometry, ids)
    if not float(size) > 0.0:
        raise ValueError('the element size must be positive')
    flat = [i for i in ids if is_flat(geometry, i)]
    solid = [i for i in ids if i not in flat]
    pieces = [_flat_part(geometry, i, float(size), triangles) for i in flat]
    if solid:
        pieces.append(_outer_surface(geometry, solid, float(size), triangles, progress))
    part = pieces[0]
    for piece in pieces[1:]:
        join(part, piece)
    return part, {'method': 'shells', 'elements': len(part.elem_conn),
                  'nodes': int(part.num_nodes), 'flat': len(flat),
                  'surfaces': len(solid)}

mesh_size

mesh_size(geometry: Any, ids) -> float

The element size the parts are meshed into shells at by default: solids.mesh_size, with a flat part measured across its plane, its own lack of thickness left out.

Parameters:

Name Type Description Default
geometry Geometry

Whose solids.

required
ids sequence of int

The parts, by id.

required

Returns:

Type Description
float
Source code in src/visualdynamics/core/shells.py
def mesh_size(geometry: Any, ids) -> float:
    """The element size the parts are meshed into shells at by default:
    `solids.mesh_size`, with a flat part measured across its plane, its
    own lack of thickness left out.

    Parameters
    ----------
    geometry : Geometry
        Whose solids.
    ids : sequence of int
        The parts, by id.

    Returns
    -------
    float
    """
    from .solids import is_flat
    from .solids import mesh_size as solid_size

    ids = [int(i) for i in ids]
    sizes = []
    for i in ids:
        if is_flat(geometry, i):
            # a slab far thicker than the part is wide, so the thinnest
            # feature found is one across the plane
            scratch, _r, _o = flat_scratch(geometry, i, 1e3 * _reach(geometry, i))
            sizes.append(solid_size(scratch, [i]))
    rest = [i for i in ids if not is_flat(geometry, i)]
    if rest:
        sizes.append(solid_size(geometry, rest))
    return min(sizes)

flat_scratch

flat_scratch(geometry: Any, part: int, thickness: float) -> tuple[Any, ndarray, ndarray]

The flat part laid out in its own frame as a slab thickness through: a scratch geometry holding it, with the rotation (its axes as rows) and the origin that put it back.

The frame is the first plate's or disk's. Every plate becomes a block and every disk a cylinder, thickness along Z about Z = 0, and every solid cut from them runs through the slab, so that the brick mesher sees an extrusion one element thick. Refused: a plate or a disk off the first one's plane, and a solid cut from it that is a cone or a sphere, or that crosses the plane at an angle, since the cut is then not the same through the slab.

Parameters:

Name Type Description Default
geometry Geometry

Whose solids.

required
part int

The flat part, by id.

required
thickness float

How thick the slab is, as the geometry holds its coordinates.

required

Returns:

Type Description
(Geometry, ndarray, ndarray)
Source code in src/visualdynamics/core/shells.py
def flat_scratch(geometry: Any, part: int, thickness: float) -> tuple[Any, np.ndarray, np.ndarray]:
    """The flat part laid out in its own frame as a slab `thickness`
    through: a scratch geometry holding it, with the rotation (its axes
    as rows) and the origin that put it back.

    The frame is the first plate's or disk's. Every plate becomes a
    block and every disk a cylinder, `thickness` along Z about Z = 0,
    and every solid cut from them runs through the slab, so that the
    brick mesher sees an extrusion one element thick. Refused: a plate
    or a disk off the first one's plane, and a solid cut from it that is
    a cone or a sphere, or that crosses the plane at an angle, since the
    cut is then not the same through the slab.

    Parameters
    ----------
    geometry : Geometry
        Whose solids.
    part : int
        The flat part, by id.
    thickness : float
        How thick the slab is, as the geometry holds its coordinates.

    Returns
    -------
    (Geometry, numpy.ndarray, numpy.ndarray)
    """
    from ..rotate import angles_of
    from .geometry import Geometry
    from .solids import (
        FLAT,
        HOLE,
        OPERATIONS,
        PATTERN,
        SECTIONS,
        _frame,
        _offsets,
        piece_name,
        solid,
    )

    tree = _tree(geometry, part)
    items = [solid(geometry, i) for i in tree]
    first = next(item for item in items if item.kind in FLAT)
    rotation = np.array(_frame(first))
    origin = np.asarray(first.center, dtype=float)
    reach = max(max(first.widths), first.radius)
    snug = 1e-9 * reach

    def local(item):
        return (np.array(_frame(item)) @ rotation.T,
                (np.asarray(item.center, dtype=float) - origin) @ rotation.T)

    def square(frame, item):
        if not np.isclose(abs(frame[2, 2]), 1.0, atol=1e-9):
            raise ValueError(f'{piece_name(geometry, item.id)} crosses the plane of '
                             f'{piece_name(geometry, part)} at an angle: a cut from a '
                             'plate or a disk runs square to it')

    thick = 3.0 * thickness
    out = []
    for item in items:
        if item.kind in OPERATIONS:
            out.append(item)
            continue
        if item.kind == PATTERN:
            frame = np.array(_frame(item)) @ rotation.T
            if np.abs(_offsets(item) @ rotation.T[:, 2]).max() > snug:
                raise ValueError(f'{piece_name(geometry, item.id)} puts copies off the '
                                 f'plane of {piece_name(geometry, part)}')
            out.append(replace(item, angles=angles_of(frame)))
            continue
        frame, center = local(item)
        at = (float(center[0]), float(center[1]), 0.0)
        if item.kind in FLAT:
            if not np.isclose(abs(frame[2, 2]), 1.0, atol=1e-9) or abs(center[2]) > snug:
                raise ValueError(f'{piece_name(geometry, item.id)} is not on the plane of '
                                 f'{piece_name(geometry, part)}: a flat part meshes on '
                                 'one plane')
            if item.kind == 'plate':
                out.append(replace(item, kind='block', center=at, angles=angles_of(frame),
                                   widths=(*item.widths[:2], thickness)))
            else:
                out.append(replace(item, kind='cylinder', center=at,
                                   angles=(0.0, 0.0, 0.0), length=thickness))
            continue
        if item.kind in ('cone', 'sphere'):
            raise ValueError(f'{piece_name(geometry, item.id)} is a {item.kind}: a plate '
                             'or a disk is cut by blocks, cylinders, sections and holes')
        square(frame, item)
        if item.kind == 'cylinder':
            out.append(replace(item, center=at, angles=(0.0, 0.0, 0.0), length=thick))
        elif item.kind == 'block':
            out.append(replace(item, center=at, angles=angles_of(frame),
                               widths=(*item.widths[:2], thick)))
        elif item.kind in SECTIONS:
            out.append(replace(item, center=at, angles=angles_of(frame), length=thick))
        elif item.kind == HOLE:
            # it runs along its own -Z from its center: started above the
            # slab, through it, the counterbore of no use in a plate
            side = float(np.sign(frame[2, 2]))
            out.append(replace(item, center=(at[0], at[1], side * thick / 2),
                               angles=angles_of(frame),
                               dimensions=(item.dimensions[0], 0.0, 0.0, 0.0)))
        else:
            out.append(item)
    scratch = Geometry(node_id=[], node_xyz=np.empty((0, 3)),
                       length_unit=geometry.length_unit)
    kept = set(tree)
    scratch.solids = out + [s for s in geometry.solids if s.id not in kept]
    return scratch, rotation, origin