Skip to content

visualdynamics.core.tables

tables

An object's contents as a table: headers and rows of text.

The app has its own table models, which are editable, colored, aware of units and built on Qt. This module is the read-only version: the table that goes in a report, and the one a script gets when it asks what is in a channel table without opening a window. The report and a script use the same functions here, so they always show the same table.

Functions:

Name Description
matched_rows

The table of a MatchedModes object, as values.

cell_text

One matched-modes value as text, to decimals places; None is a dash.

mode_heading

The column heading of mode mode (from 0) in a table of shape

shape_value_text

One shape value as text, to six significant figures; a complex

shape_values

(headers, rows) of a shape set's values, or of one of its

cell_value_text

shape_value_text, and blank where there is no value.

transformation_table

(headers, rows) of a control loop's transformation

table_of

(headers, rows) for an object that has a table, else None.

Functions:

matched_rows

matched_rows(matched: Any, objects: Mapping[str, Any]) -> tuple[list[str], list[tuple[Any, ...]]]

The table of a MatchedModes object, as values.

Returns the headers and one row per pair: each set's mode number (one-based), frequency [Hz] and damping [%], the frequency error of the second set against the first as signed text in percent, the pair's MAC, and the size of the second set's mode relative to the first's (scale_ratios).

The two sets are looked up by name in objects each time, so the frequencies and damping follow any change to the sets. The MAC is the value stored in the object, because it came from the comparison as it was displayed, possibly after a projection. A set missing from objects, or a mode number past the end of its set, gives None (and a dash for the frequency error). The last column shows how the two shapes of each pair are scaled relative to each other, which an overlay cannot show because it draws each shape to its own peak; a column of 1.00 means the comparison is of shape alone.

The window's table and the report's table are drawn from these rows; table_of gives the same rows as text.

Source code in src/visualdynamics/core/tables.py
def matched_rows(matched: Any, objects: Mapping[str, Any]
                 ) -> tuple[list[str], list[tuple[Any, ...]]]:
    """The table of a `MatchedModes` object, as values.

    Returns the headers and one row per pair: each set's mode number
    (one-based), frequency [Hz] and damping [%], the frequency error of
    the second set against the first as signed text in percent, the
    pair's MAC, and the size of the second set's mode relative to the
    first's (`scale_ratios`).

    The two sets are looked up by name in `objects` each time, so the
    frequencies and damping follow any change to the sets. The MAC is
    the value stored in the object, because it came from the comparison
    as it was displayed, possibly after a projection. A set missing from
    `objects`, or a mode number past the end of its set, gives None (and
    a dash for the frequency error). The last column shows how the two
    shapes of each pair are scaled relative to each other, which an
    overlay cannot show because it draws each shape to its own peak; a
    column of 1.00 means the comparison is of shape alone.

    The window's table and the report's table are drawn from these
    rows; `table_of` gives the same rows as text.
    """
    from .shapes import scale_ratios

    a_name, b_name = matched.first, matched.second
    a, b = objects.get(a_name), objects.get(b_name)

    def parameters(shape_set: Any,
                   mode: int) -> tuple[float | None, float | None]:
        if shape_set is None or not 0 <= mode < shape_set.num_shapes:
            return None, None
        return (float(shape_set.frequency[mode]),
                float(shape_set.damping[mode]) * 100.0)

    ratios = (scale_ratios(a, b, matched.pairs)
              if a is not None and b is not None
              else [None] * len(matched.pairs))
    rows = []
    for index, ((row, column), mac) in enumerate(
            zip(matched.pairs, matched.macs)):
        fa, da = parameters(a, row)
        fb, db = parameters(b, column)
        delta = (f'{(fb - fa) / fa * 100.0:+.2f}'
                 if fa and fb is not None else '—')
        rows.append((row + 1, fa, da, column + 1, fb, db, delta,
                     float(mac), ratios[index]))
    headers = [f'{a_name} Mode', 'Frequency [Hz]', 'Damping [%]',
               f'{b_name} Mode', 'Frequency [Hz]', 'Damping [%]',
               'Δf [%]', 'MAC', f'{b_name}/{a_name}']
    return headers, rows

cell_text

cell_text(value: Any, decimals: int | None) -> str

One matched-modes value as text, to decimals places; None is a dash.

Source code in src/visualdynamics/core/tables.py
def cell_text(value: Any, decimals: int | None) -> str:
    """One matched-modes value as text, to `decimals` places; None is a dash."""
    if value is None:
        return '—'
    return str(value) if decimals is None else f'{value:.{decimals}f}'

mode_heading

mode_heading(mode: int) -> str

The column heading of mode mode (from 0) in a table of shape values: 'M1' for the first.

Source code in src/visualdynamics/core/tables.py
def mode_heading(mode: int) -> str:
    """The column heading of mode `mode` (from 0) in a table of shape
    values: 'M1' for the first."""
    return f'M{mode + 1}'

shape_value_text

shape_value_text(value: Any) -> str

One shape value as text, to six significant figures; a complex one as its real and imaginary parts, '0.5-0.25j'.

Source code in src/visualdynamics/core/tables.py
def shape_value_text(value: Any) -> str:
    """One shape value as text, to six significant figures; a complex
    one as its real and imaginary parts, '0.5-0.25j'."""
    import numpy as np

    # + 0.0 turns a negated zero into zero: '2X-' against a column with
    # no X read '-0'
    if np.iscomplexobj(value):
        return f'{value.real + 0.0:.6g}{value.imag + 0.0:+.6g}j'
    return f'{float(value) + 0.0:.6g}'

shape_values

shape_values(shapes: Any, dofs: Any = None, form: str = 'shapes') -> tuple[list[str], list[list[str]]]

(headers, rows) of a shape set's values, or of one of its transformations, at dofs (transform.matrix_at).

The shapes are a row per DOF, the DOF first, then a column per mode ('M1', 'M2', ...), the way a mode shape matrix is usually written: the transpose of ShapeSet.shape_matrix. The two transformations are a row per mode, then a column per DOF. A DOF the set does not have is labeled so and its values are blank.

Source code in src/visualdynamics/core/tables.py
def shape_values(shapes: Any, dofs: Any = None,
                 form: str = 'shapes') -> tuple[list[str], list[list[str]]]:
    """``(headers, rows)`` of a shape set's values, or of one of its
    transformations, at `dofs` (`transform.matrix_at`).

    The shapes are a row per DOF, the DOF first, then a column per mode
    ('M1', 'M2', ...), the way a mode shape matrix is usually written:
    the transpose of `ShapeSet.shape_matrix`. The two transformations
    are a row per mode, then a column per DOF. A DOF the set does not
    have is labeled so and its values are blank.
    """
    # Brandon, 2026-10-22, with the table of them beside the mode list;
    # at a choice of DOFs and in three forms, 2026-10-23.
    from .transform import matrix_at

    view = matrix_at(shapes, dofs, form)
    missing = set(view.missing)

    def label(dof):
        return f'{dof} (not in the set)' if dof in missing else str(dof)

    rows_are_dofs = form == 'shapes'
    headers = (['DOF' if rows_are_dofs else 'Mode']
               + [label(c) if not rows_are_dofs else c for c in view.columns])
    rows = [[label(r) if rows_are_dofs else r]
            + [cell_value_text(v) for v in view.values[i]]
            for i, r in enumerate(view.rows)]
    return headers, rows

cell_value_text

cell_value_text(value: Any) -> str

shape_value_text, and blank where there is no value.

Source code in src/visualdynamics/core/tables.py
def cell_value_text(value: Any) -> str:
    """`shape_value_text`, and blank where there is no value."""
    import numpy as np

    return '' if np.isnan(value) else shape_value_text(value)

transformation_table

transformation_table(table: Any, environment: str, form: str = 'response') -> tuple[list[str], list[list[str]]]

(headers, rows) of a control loop's transformation (ChannelTable.transformation): a row per transformation row, numbered from 1, then a column per control (or drive) channel headed by its DOF.

Source code in src/visualdynamics/core/tables.py
def transformation_table(table: Any, environment: str,
                         form: str = 'response'
                         ) -> tuple[list[str], list[list[str]]]:
    """``(headers, rows)`` of a control loop's transformation
    (`ChannelTable.transformation`): a row per transformation row,
    numbered from 1, then a column per control (or drive) channel
    headed by its DOF.
    """
    view = table.transformation(environment, form)
    return (['Row', *view.columns],
            [[label, *(cell_value_text(v) for v in view.values[i])]
             for i, label in enumerate(view.rows)])

table_of

table_of(obj: Any, geometry: Any = None, objects: Mapping[str, Any] | None = None, environment: str | None = None) -> tuple[list[str], list[list[str]]] | None

(headers, rows) for an object that has a table, else None.

A shape set gives its modal parameters (mode, frequency, damping, description). A channel table gives its columns, plus the direction columns derived from geometry when one is given, its Control column that of environment when one is named. Matched modes give the pairs beside both sets' parameters (matched_rows), with the sets looked up in objects. Every cell is text, formatted the way it is usually quoted rather than the way it is stored: a damping of 0.0213 is '2.130' percent.

Source code in src/visualdynamics/core/tables.py
def table_of(obj: Any, geometry: Any = None,
             objects: Mapping[str, Any] | None = None,
             environment: str | None = None
             ) -> tuple[list[str], list[list[str]]] | None:
    """``(headers, rows)`` for an object that has a table, else None.

    A shape set gives its modal parameters (mode, frequency, damping,
    description). A channel table gives its columns, plus the direction
    columns derived from `geometry` when one is given, its Control
    column that of `environment` when one is named. Matched modes
    give the pairs beside both sets' parameters (`matched_rows`), with
    the sets looked up in `objects`. Every cell is text, formatted the
    way it is usually quoted rather than the way it is stored: a damping
    of 0.0213 is '2.130' percent.
    """
    from .channel_table import ChannelTable
    from .matches import MatchedModes
    from .shapes import ShapeSet

    if isinstance(obj, MatchedModes):
        headers, rows = matched_rows(obj, objects or {})
        return headers, [[cell_text(value, decimals) for value, decimals
                          in zip(row, MATCHED_DECIMALS)] for row in rows]

    if isinstance(obj, ShapeSet):
        return (['Mode', 'Frequency [Hz]', 'Damping [%]', 'Description'],
                [[str(m + 1), f'{float(obj.frequency[m]):.4f}',
                  f'{float(obj.damping[m]) * 100:.3f}', obj.description[m]]
                 for m in range(obj.num_shapes)])
    if isinstance(obj, ChannelTable):
        # every column, because the schema is already the curated set:
        # a Rattlesnake save's coupling and feedback wiring stop at the
        # importer now, so there is no acquisition plumbing left here to
        # cut and no second list to keep in step with the first
        from .channel_table import DERIVED_COLUMNS, title_of

        names = obj.column_names
        derived = list(DERIVED_COLUMNS) if geometry is not None else []
        controls = obj.controls(environment)

        def cell(name, r):
            if name == 'control':
                return str(bool(controls[r]))
            return str(obj[name][r])
        return ([title_of(name) for name in names + derived],
                [[cell(name, r) for name in names]
                 + (obj.derived_cells(r, geometry) if derived else [])
                 for r in range(obj.num_channels)])
    return None