Skip to content

visualdynamics.demo.frame

frame

The four-unit frame and its wings, built from blocks of bricks.

The four-unit frame is a small aluminum ladder the structural dynamics community shares as a common test article for substructuring: a 16 by 6 in frame cut from half-inch plate, two rails and five uprights half an inch wide, with 41 threaded steel inserts for bolting things to, and two rectangular 22 by 4.4 in wings, thin (1/8 in) and thick (1/4 in), that screw across two of its uprights. Its solid models, finite element models and test data are shared on the SEM Dynamic Substructuring Focus Group wiki, https://wiki.sem.org/wiki/Round_Robin_Frame_Structure.

Here it is built the way a person would build a solid model with this package (2026-09-30): each part as the rectangular blocks it is made of, meshed into bricks (visualdynamics.mesh.block), the insert holes cut from the blocks and their bricks given the insert's material, the wing's holes cut through, and the screws as rigid links over the bricks each washer covers, the way the BARC's bolts are. Every dimension below is read off the shared finite element models, in inches:

from visualdynamics.demo import frame

model = frame.build()                    # the frame alone
shapes = model.eigensolution(maximum_frequency=2000)
both = frame.build('thick wing')         # the frame with a wing on it

The solid elements are the model. Beams describe a half-inch bar spanning a hundred-millimeter window poorly and plates not at all, so this example is what the eight-node brick with incompatible modes was written for. The element was checked against the mesh the wiki shares: solved here node for node, its first ten elastic modes land within 0.1 % of the frequencies MSC Nastran gives the same mesh. The model built here is coarser — one brick in twelve of an inch, five thousand nodes for the frame where the shared mesh has ninety thousand — and its holes are stair-stepped, and it lands within a few percent of the frames that were measured, which vary by as much among themselves.

What the frames were measured to is on the wiki; the wings' and the assembly's frequencies here were fitted from the shared test FRFs with Project.fit_modes. None of the wiki's files ship with this package.

Run it directly to print the models and their first modes:

python3 -m visualdynamics.demo.frame

Functions:

Name Description
geometry

The frame, a wing, or the frame with a wing screwed on, as a

washer_patch

The wing's bricks a screw's washer covers: those of the wing's

build

The finite element model (geometry built), the screws' mass

project

A project to open in the app: the frame, each wing alone, and

describe

Print each model and its first elastic modes.

Classes

Functions:

geometry

geometry(wing: str | None = None, size: float = SIZE, wing_size: float = WING_SIZE) -> Any

The frame, a wing, or the frame with a wing screwed on, as a geometry of bricks and rigid links, every element group given what it is made of — ready for fem.Model.from_geometry, or for the app's Solve Modes.

Parameters:

Name Type Description Default
wing (None, 'thin wing', 'thick wing', 'frame')

None or 'frame' for the frame alone; a wing's name for the frame with that wing; 'thin wing alone' or 'thick wing alone' for the wing by itself.

None
size float

The frame's element size aimed at, in inches.

SIZE
wing_size float

The wing's, in inches.

WING_SIZE

Returns:

Type Description
Geometry

Element groups 'frame' (6061-T6), 'inserts' (the insert material), the wing (6061-T6) and 'screws' (rigid links over the bricks each washer covers), opening on VIEW.

Source code in src/visualdynamics/demo/frame.py
def geometry(wing: str | None = None, size: float = SIZE,
             wing_size: float = WING_SIZE) -> Any:
    """The frame, a wing, or the frame with a wing screwed on, as a
    geometry of bricks and rigid links, every element group given what it is
    made of — ready for `fem.Model.from_geometry`, or for the app's
    Solve Modes.

    Parameters
    ----------
    wing : {None, 'thin wing', 'thick wing', 'frame'}, optional
        None or 'frame' for the frame alone; a wing's name for the
        frame with that wing; 'thin wing alone' or 'thick wing alone'
        for the wing by itself.
    size : float
        The frame's element size aimed at, in inches.
    wing_size : float
        The wing's, in inches.

    Returns
    -------
    Geometry
        Element groups 'frame' (6061-T6), 'inserts' (the insert material), the
        wing (6061-T6) and 'screws' (rigid links over the bricks each
        washer covers), opening on `VIEW`.
    """
    choice = (wing or 'frame').strip()
    alone = choice.endswith(' alone')
    part = choice.removesuffix(' alone')
    if part not in ('frame', *WINGS):
        raise ValueError(f'{wing!r}: the frame, {" or ".join(WINGS)}, on '
                         'the frame or alone')
    parts = []
    if part == 'frame' or not alone:
        parts.extend(_frame_blocks(size))
    if part != 'frame':
        parts.append(_wing_block(part, wing_size))
    whole = mesh.assemble(*parts)
    whole.view = VIEW
    materials = {'frame': ALUMINUM, 'inserts': INSERT,
                 **{name: ALUMINUM for name in WINGS}}
    whole.group_properties = {
        int(group): fem.GroupProperties(materials[whole.group_name[i]])
        for i, group in enumerate(whole.group_id)}
    if part != 'frame' and not alone:
        for screw in SCREWS:
            mesh.tie(whole, washer_patch(whole, part, screw), 'frame',
                     group='screws')
    return whole

washer_patch

washer_patch(geometry: Any, wing: str, screw: tuple) -> list[int]

The wing's bricks a screw's washer covers: those of the wing's element group whose centers lie within WASHER_RADIUS of the screw's axis, the full thickness of the wing — a rigid plug where the screw and its washers clamp the plate, tied to the frame under it.

Parameters:

Name Type Description Default
geometry Geometry

The frame and wing, assembled.

required
wing str

The wing's element group name.

required
screw tuple

(x, y) of the screw, inches.

required

Returns:

Type Description
list of int

The element ids.

Source code in src/visualdynamics/demo/frame.py
def washer_patch(geometry: Any, wing: str, screw: tuple) -> list[int]:
    """The wing's bricks a screw's washer covers: those of the wing's
    element group whose centers lie within `WASHER_RADIUS` of the screw's axis,
    the full thickness of the wing — a rigid plug where the screw and
    its washers clamp the plate, tied to the frame under it.

    Parameters
    ----------
    geometry : Geometry
        The frame and wing, assembled.
    wing : str
        The wing's element group name.
    screw : tuple
        (x, y) of the screw, inches.

    Returns
    -------
    list of int
        The element ids.
    """
    xyz = geometry.node_xyz / INCH
    rows = geometry.node_index
    patch = []
    for element in geometry.elements_in(wing):
        row = int(np.flatnonzero(geometry.elem_id == element)[0])
        center = xyz[rows(geometry.elem_conn[row])].mean(axis=0)
        if np.hypot(center[0] - screw[0], center[1] - screw[1]) <= WASHER_RADIUS:
            patch.append(element)
    return patch

build

build(wing: str | None = None, size: float = SIZE, wing_size: float = WING_SIZE) -> Model

The finite element model (geometry built), the screws' mass lumped at the frame under each.

Parameters:

Name Type Description Default
wing str | None

As for geometry.

None
size str | None

As for geometry.

None
wing_size str | None

As for geometry.

None

Returns:

Type Description
Model
Source code in src/visualdynamics/demo/frame.py
def build(wing: str | None = None, size: float = SIZE,
          wing_size: float = WING_SIZE) -> fem.Model:
    """The finite element model (`geometry` built), the screws' mass
    lumped at the frame under each.

    Parameters
    ----------
    wing, size, wing_size
        As for `geometry`.

    Returns
    -------
    fem.Model
    """
    part = geometry(wing, size, wing_size)
    name = (wing or 'frame').strip()
    model = fem.Model.from_geometry(part, name=f'Four-Unit Frame: {name}')
    if name in WINGS:
        xyz = part.node_xyz / INCH
        for screw in SCREWS:
            # the nearest frame node to the screw on the top face
            frame_nodes = np.unique(np.concatenate(
                [part.elem_conn[int(np.flatnonzero(part.elem_id == e)[0])]
                 for e in part.elements_in('inserts')]))
            at = xyz[part.node_index(frame_nodes)]
            nearest = int(frame_nodes[np.argmin(
                np.hypot(at[:, 0] - screw[0], at[:, 1] - screw[1]) + np.abs(at[:, 2]))])
            model.add_mass(nearest, SCREW_MASS, name='screw')
    return model

project

project(solved: bool = False, size: float = SIZE, wing_size: float = WING_SIZE) -> Any

A project to open in the app: the frame, each wing alone, and the frame with each wing, their element groups given their properties — Solve Modes on any of them gives its modes.

frame.project().save('frame.vdyn')

With solved, every model's modes are solved to SOLVE_TO already: the example project the downloads page offers.

Parameters:

Name Type Description Default
solved bool

Solve the modes as well.

False
size float

The element sizes aimed at, in inches.

SIZE
wing_size float

The element sizes aimed at, in inches.

SIZE

Returns:

Type Description
Project
Source code in src/visualdynamics/demo/frame.py
def project(solved: bool = False, size: float = SIZE,
            wing_size: float = WING_SIZE) -> Any:
    """A project to open in the app: the frame, each wing alone, and
    the frame with each wing, their element groups given their properties —
    Solve Modes on any of them gives its modes.

        frame.project().save('frame.vdyn')

    With `solved`, every model's modes are solved to `SOLVE_TO`
    already: the example project the downloads page offers.

    Parameters
    ----------
    solved : bool, default False
        Solve the modes as well.
    size, wing_size : float
        The element sizes aimed at, in inches.

    Returns
    -------
    Project
    """
    from visualdynamics.project import Project

    out = Project('Four-Unit Frame')
    for name in ('frame', *(f'{w} alone' for w in WINGS), *WINGS):
        label = {'frame': 'Frame'}.get(name, name.replace(' alone', '').title()
                                       + ('' if name.endswith('alone') else ' on Frame'))
        out.add(label, geometry(name, size, wing_size))
        if solved:
            out.solve_modes(label, maximum_frequency=SOLVE_TO)
    return out

describe

describe(size: float = SIZE, modes: int = 9) -> None

Print each model and its first elastic modes.

Source code in src/visualdynamics/demo/frame.py
def describe(size: float = SIZE, modes: int = 9) -> None:
    """Print each model and its first elastic modes."""
    for name in ('frame', 'thin wing alone', 'thick wing alone', *WINGS):
        model = build(name, size)
        shapes = model.eigensolution(maximum_frequency=SOLVE_TO)
        print(f'{name}: {model.num_nodes} nodes, {len(model.solids)} bricks, '
              f'{len(model.rigid_links)} rigid links, '
              f'{model.total_mass * 1000:.1f} g')
        elastic = [f for f in shapes.frequency if f > 1.0][:modes]
        print('  elastic modes (Hz): ' + ', '.join(f'{f:.1f}' for f in elastic))