Skip to content

visualdynamics.gui.object_tables

object_tables

Table models for visualdynamics objects.

Each is a list of columns over an existing object — reading straight from it and, where editing makes sense, writing straight back. Results that a fit produced (frequency, damping) are read-only; the free-text description beside them is not.

Classes:

Name Description
ComplianceRows

The comparison of a specification with a measurement, per channel.

ReplicationRows

Waveform error as a grid: a control channel per row, a playing of

ComplianceGrid

A control channel per row: how far it is from its specification.

Functions:

Name Description
shape_table_model

Modes with their frequency, damping and a description to fill in.

matched_modes_model

A MatchedModes object as a table, in the mode table's format.

channel_table_model

A channel table: every column editable, every column typed.

modal_fit_table_model

The modes fitted so far, plus the one the cursor is placing.

units_table_model

One row per thing that needs a unit, whatever kind of object it is.

frf_units_models

Two tables for an FRF: response channels and reference channels.

id_runs_text

Sorted ids as runs: [1,2,3,7,9,10] -> '1-3 7 9-10'.

parse_id_runs

The inverse: '1-3, 7 9-10' -> [1, 2, 3, 7, 9, 10].

block_label

How a block reads in a list: its name, or its id when unnamed.

block_table_model

The element blocks: what the mesh is divided into.

specification_model

A row per channel a specification carries: its name and its level.

replication_model

A grid of channels down and repeats across.

compliance_grid_model

Channel, how far its level is out, and how much of its band is.

Classes

ComplianceRows

ComplianceRows(
    rows: Sequence[tuple[str, Any]],
    unit_label: str = "",
    plotted: int | None = None,
)

The comparison of a specification with a measurement, per channel.

A plain holder so the table model has something to read: the rows are computed once, when the selection settles, rather than per cell.

plotted is the row the plot is currently drawing, so the table can say which of its channels is the one on screen.

Methods:

Name Description
row_of

Which row is this channel, or None.

Source code in src/visualdynamics/gui/object_tables.py
def __init__(self, rows: Sequence[tuple[str, Any]], unit_label: str = '',
             plotted: int | None = None) -> None:
    self.rows: list[tuple[str, Any]] = list(rows)
    self.unit_label: str = unit_label
    self.plotted: int | None = plotted
Methods:
row_of
row_of(label: str) -> int | None

Which row is this channel, or None.

Source code in src/visualdynamics/gui/object_tables.py
def row_of(self, label: str) -> int | None:
    """Which row is this channel, or None."""
    for row, (name, _result) in enumerate(self.rows):
        if name == label:
            return row
    return None

ReplicationRows

ReplicationRows(
    channels: Sequence[str],
    events: Sequence[int],
    values: Mapping[tuple[str, int], float],
    plotted: Sequence[tuple[str, int]] | None = None,
)

Waveform error as a grid: a control channel per row, a playing of the waveform per column.

A transient record is many attempts at one target, so the numbers are two-dimensional and the table is the only place that shows them that way — the plot draws what is picked and the bar chart reduces to one reading. Laid out this way a channel that is bad everywhere and a repeat that was bad for everything look different at a glance, which is the comparison a single column cannot make.

plotted is the set of (channel, event) pairs on screen, because picking a cell picks both at once.

Methods:

Name Description
value

The cell at (row, column-of-events), or None where the

pairs_at

The (channel, event) pairs one selected cell stands for.

Source code in src/visualdynamics/gui/object_tables.py
def __init__(self, channels: Sequence[str], events: Sequence[int],
             values: Mapping[tuple[str, int], float],
             plotted: Sequence[tuple[str, int]] | None = None) -> None:
    self.channels: list[str] = list(channels)
    self.events: list[int] = list(events)
    self.values: dict[tuple[str, int], float] = dict(values)
    self.plotted: set[tuple[str, int]] = set(plotted or ())
Methods:
value
value(row: int, column: int) -> float | None

The cell at (row, column-of-events), or None where the channel cannot be scored against its target.

Source code in src/visualdynamics/gui/object_tables.py
def value(self, row: int, column: int) -> float | None:
    """The cell at (row, column-of-events), or None where the
    channel cannot be scored against its target."""
    if not 0 <= row < len(self.channels):
        return None
    if not 0 <= column < len(self.events):
        return None
    return self.values.get((self.channels[row], self.events[column]))
pairs_at
pairs_at(row: int, column: int) -> list[tuple[str, int]]

The (channel, event) pairs one selected cell stands for.

Column zero is the channel's name rather than a reading of it, so picking there asks for that channel across every playing — the row as a whole, which is what clicking a name should mean.

Source code in src/visualdynamics/gui/object_tables.py
def pairs_at(self, row: int,
             column: int) -> list[tuple[str, int]]:
    """The (channel, event) pairs one selected cell stands for.

    Column zero is the channel's name rather than a reading of it,
    so picking there asks for that channel across every playing —
    the row as a whole, which is what clicking a name should mean.
    """
    if not 0 <= row < len(self.channels):
        return []
    channel = self.channels[row]
    if column <= 0:
        return [(channel, event) for event in self.events]
    if column - 1 >= len(self.events):
        return []
    return [(channel, self.events[column - 1])]

ComplianceGrid

ComplianceGrid(
    rows: Sequence[tuple[str, Any]],
    plotted: Sequence[str] | None = None,
)

A control channel per row: how far it is from its specification.

The same job the transient's grid does, for a comparison that has no repeats to spread across — a PSD is already an average over them, so there is one column of numbers and not a grid of them.

plotted is the set of DOF pairs on screen.

Methods:

Name Description
pair_at

The DOF pair a row names, or None.

Source code in src/visualdynamics/gui/object_tables.py
def __init__(self, rows: Sequence[tuple[str, Any]],
             plotted: Sequence[str] | None = None) -> None:
    self.rows: list[tuple[str, Any]] = list(rows)
    self.plotted: set[str] = set(plotted or ())
Methods:
pair_at
pair_at(row: int) -> str | None

The DOF pair a row names, or None.

Source code in src/visualdynamics/gui/object_tables.py
def pair_at(self, row: int) -> str | None:
    """The DOF pair a row names, or None."""
    if 0 <= row < len(self.rows):
        return self.rows[row][0]
    return None

Functions:

shape_table_model

shape_table_model(
    shapes: ShapeSet, parent: QObject | None = None
) -> TableModel

Modes with their frequency, damping and a description to fill in.

Source code in src/visualdynamics/gui/object_tables.py
def shape_table_model(shapes: ShapeSet,
                      parent: QObject | None = None) -> TableModel:
    """Modes with their frequency, damping and a description to fill in."""
    columns = [
        Column('Mode', lambda s, r: r + 1),
        Column('Frequency [Hz]', lambda s, r: float(s.frequency[r]),
               format=lambda v: f'{v:.4f}'),
        Column('Damping [%]', lambda s, r: float(s.damping[r]) * 100.0,
               format=lambda v: f'{v:.3f}'),
        Column('Modal mass (unscaled)'
               if getattr(shapes, 'unscaled', False) else 'Modal mass',
               lambda s, r: float(s.modal_mass[r]),
               format=lambda v: f'{v:.4g}'),
        Column('Description', lambda s, r: s.description[r],
               set=_set_description,
               journal=lambda s, r, t: f'.description[{r}] = {t.strip()!r}',
               alignment=LEFT),
    ]
    return TableModel(shapes, columns, lambda s: s.num_shapes, parent)

matched_modes_model

matched_modes_model(
    matched: MatchedModes,
    objects: Mapping[str, Any],
    parent: QObject | None = None,
) -> TableModel

A MatchedModes object as a table, in the mode table's format.

The referenced sets are read live by name — frequencies and damping restate themselves as the sets change — while the MAC is the object's own stored value (it came from the comparison as displayed, possibly projected). A set gone from the project, or a mode index past its end, shows dashes rather than guessing.

Source code in src/visualdynamics/gui/object_tables.py
def matched_modes_model(matched: MatchedModes, objects: Mapping[str, Any],
                        parent: QObject | None = None) -> TableModel:
    """A MatchedModes object as a table, in the mode table's format.

    The referenced sets are read live by name — frequencies and
    damping restate themselves as the sets change — while the MAC is
    the object's own stored value (it came from the comparison as
    displayed, possibly projected). A set gone from the project, or a
    mode index past its end, shows dashes rather than guessing.
    """
    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)

    # the scale each pair was normalized by, which the overlay
    # animation cannot show: it draws both shapes to their own peak, so
    # a set thirty times the other looks identical to one that agrees
    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]))
    def number(decimals: int) -> Callable[[Any], str]:
        return lambda v: '—' if v is None else f'{v:.{decimals}f}'

    columns = [
        Column(f'{a_name} Mode', lambda s, r: s[r][0]),
        Column('Frequency [Hz]', lambda s, r: s[r][1],
               format=number(4)),
        Column('Damping [%]', lambda s, r: s[r][2], format=number(3)),
        Column(f'{b_name} Mode', lambda s, r: s[r][3]),
        Column('Frequency [Hz]', lambda s, r: s[r][4],
               format=number(4)),
        Column('Damping [%]', lambda s, r: s[r][5], format=number(3)),
        Column('Δf [%]', lambda s, r: s[r][6]),
        Column('MAC', lambda s, r: s[r][7], format=number(3)),
        Column(f'{b_name}/{a_name}', lambda s, r: s[r][8],
               format=number(2)),
    ]
    return TableModel(rows, columns, len, parent)

channel_table_model

channel_table_model(
    table: ChannelTable,
    parent: QObject | None = None,
    rows: Sequence[int] | None = None,
) -> TableModel

A channel table: every column editable, every column typed.

The columns come from the table's own COLUMNS spec rather than being listed again here, so a schema change reaches the interface without a second edit — a choice column gets its drop-down, a date gets a calendar, a flag gets a check box, and the unit column is narrowed by what the channel says it measures.

rows shows only those channels — picking cells in the tree's grid is picking rows here too, so the table beside the model shows what the selection says and nothing else.

Source code in src/visualdynamics/gui/object_tables.py
def channel_table_model(table: ChannelTable, parent: QObject | None = None,
                        rows: Sequence[int] | None = None) -> TableModel:
    """A channel table: every column editable, every column typed.

    The columns come from the table's own `COLUMNS` spec rather than
    being listed again here, so a schema change reaches the interface
    without a second edit — a choice column gets its drop-down, a date
    gets a calendar, a flag gets a check box, and the unit column is
    narrowed by what the channel says it measures.

    `rows` shows only those channels — picking cells in the tree's grid
    is picking rows here too, so the table beside the model shows what
    the selection says and nothing else.
    """
    rows = None if rows is None else [int(r) for r in rows]
    index = (lambda row: row) if rows is None else rows.__getitem__
    columns = []
    for name, spec in table.COLUMNS.items():
        title = title_of(name)
        if spec.kind == 'flag':
            columns.append(Column(
                title,
                lambda t, r, n=name, i=index: bool(t.controls()[i(r)])
                if n == 'control' else bool(t[n][i(r)]),
                set=_column_setter(name, index),
                journal=_column_journal(name, index), checkbox=True))
            continue
        if spec.kind == 'choice':
            shown = ([shown_dimension(c) for c in spec.choices]
                     if name == 'channel_type' else list(spec.choices))
            columns.append(Column(
                title, _shown_getter(name, index),
                set=_column_setter(name, index),
                journal=_column_journal(name, index),
                choices=shown, affects_row=name == 'channel_type',
                alignment=LEFT))
            continue
        if spec.kind == 'unit':
            # what this channel could be in, given what it says it is
            columns.append(Column(
                title, _column_getter(name, index),
                set=_column_setter(name, index),
                journal=_column_journal(name, index),
                choices=ALL_ORDINATE_UNITS,
                row_choices=lambda t, r, i=index: t.units_for(i(r)),
                alignment=LEFT))
            continue
        columns.append(Column(
            title, _column_getter(name, index),
            set=_column_setter(name, index),
            journal=_column_journal(name, index),
            date=spec.kind == 'date', alignment=LEFT))
    count = ((lambda t: t.num_channels) if rows is None
             else (lambda t: len(rows)))
    return TableModel(table, columns, count, parent)

modal_fit_table_model

modal_fit_table_model(
    session: ModalFitSession, parent: QObject | None = None
) -> TableModel

The modes fitted so far, plus the one the cursor is placing.

The last row is the pending mode: its frequency mirrors the cursor on the CMIF, its damping is the half-power estimate until the user types one, and Confirm Mode turns it into a fitted row. A fitted mode's damping stays editable — the synthesis restates itself — and its description is free text.

Source code in src/visualdynamics/gui/object_tables.py
def modal_fit_table_model(session: ModalFitSession,
                          parent: QObject | None = None) -> TableModel:
    """The modes fitted so far, plus the one the cursor is placing.

    The last row is the pending mode: its frequency mirrors the cursor on
    the CMIF, its damping is the half-power estimate until the user types
    one, and Confirm Mode turns it into a fitted row. A fitted mode's
    damping stays editable — the synthesis restates itself — and its
    description is free text.
    """
    def pending(row: int) -> bool:
        return row == len(session.modes)

    def entry(row: int, key: str) -> Any:
        return (session.pending[key] if pending(row)
                else session.modes[row][key])

    def set_damping(session: ModalFitSession, row: int, text: str) -> None:
        value = float(text.rstrip('%')) / 100.0
        if not 0.0 < value < 1.0:
            raise ValueError('damping is a fraction of critical, 0-100%')
        if pending(row):
            session.pending['damping'] = value
            session.pending['overridden'] = True
        else:
            session.modes[row]['damping'] = value

    def set_description(session: Any, row: int, text: str) -> None:
        if pending(row):
            session.pending['description'] = text.strip()
        else:
            session.modes[row]['description'] = text.strip()

    columns = [
        Column('Mode', lambda s, r: 'new' if pending(r) else r + 1),
        Column('Frequency [Hz]', lambda s, r: float(entry(r, 'frequency')),
               format=lambda v: f'{v:.4f}'),
        Column('Damping [%]', lambda s, r: float(entry(r, 'damping')) * 100.0,
               format=lambda v: f'{v:.3f}', set=set_damping,
               journal=_elsewhere),
        Column('Description', lambda s, r: entry(r, 'description'),
               set=set_description, journal=_elsewhere,
               alignment=LEFT),
    ]
    return TableModel(session, columns,
                          lambda s: len(s.modes) + 1, parent)

units_table_model

units_table_model(
    obj: Any,
    records: Sequence[int] | None = None,
    parent: QObject | None = None,
) -> TableModel

One row per thing that needs a unit, whatever kind of object it is.

A table rather than a dialog, so the plot or the model stays on screen beside it and units copy, paste and clear like any other column.

Source code in src/visualdynamics/gui/object_tables.py
def units_table_model(obj: Any, records: Sequence[int] | None = None,
                      parent: QObject | None = None) -> TableModel:
    """One row per thing that needs a unit, whatever kind of object it is.

    A table rather than a dialog, so the plot or the model stays on screen
    beside it and units copy, paste and clear like any other column.
    """
    from ..core.data import Psd
    from ..core.geometry import Geometry
    from ..core.shapes import ShapeSet
    from ..core.unit_choices import LENGTH_UNITS, MASS_UNITS

    if isinstance(obj, Psd) and _has_cross_records(obj):
        return _matrix_units_model(obj, records, parent)
    if isinstance(obj, Geometry):
        return _one_unit_model(obj, 'Coordinates', 'length', LENGTH_UNITS,
                               'length_unit', parent)
    if isinstance(obj, ShapeSet):
        # What a mass-normalized shape is *in* is 1/sqrt(mass), and a cell
        # reading 'kg' does not say that — the same reasoning that shows a
        # PSD's unit as g**2/Hz. The mass unit is what is stored; the
        # radical is what is shown and offered.
        return _one_unit_model(obj, 'Mode shapes', 'mass',
                               [f'1/\u221a{unit}' for unit in MASS_UNITS],
                               'mass_unit', parent,
                               shown=lambda unit: f'1/\u221a{unit}',
                               given=_mass_unit_given,
                               type_label='modal mass')
    return _channel_units_model(obj, records, parent)

frf_units_models

frf_units_models(
    data: DataArray,
    records: Sequence[int] | None = None,
    parent: QObject | None = None,
) -> tuple[TableModel, TableModel]

Two tables for an FRF: response channels and reference channels.

An FRF matrix is N x M records but only N + M physical channels, and a channel's unit is the channel's property — so each side is declared once, and every record converts as soon as both of its channels are named. The sides stay separate even where the DOF labels collide: a drive point's accelerometer and its force gauge share a label and are two different channels.

Source code in src/visualdynamics/gui/object_tables.py
def frf_units_models(data: DataArray, records: Sequence[int] | None = None,
                     parent: QObject | None = None
                     ) -> tuple[TableModel, TableModel]:
    """Two tables for an FRF: response channels and reference channels.

    An FRF matrix is N x M records but only N + M physical channels, and a
    channel's unit is the channel's property — so each side is declared
    once, and every record converts as soon as both of its channels are
    named. The sides stay separate even where the DOF labels collide: a
    drive point's accelerometer and its force gauge share a label and are
    two different channels.
    """
    from ..core.unit_choices import (
        ALL_ORDINATE_UNITS,
        ORDINATE_UNITS,
        REFERENCE_UNITS,
        units_for,
    )

    wanted = (list(range(data.num_records)) if records is None
              else [int(i) for i in records])
    sides = {
        'response': (data.response_dof, data.ordinate_unit),
        'reference': (data.reference_dof, data.reference_unit),
    }

    channels = {side: [] for side in sides}
    for i in wanted:
        for side, (dofs, _) in sides.items():
            if dofs[i] not in channels[side]:
                channels[side].append(dofs[i])

    def involving(side: str, dof: str) -> list[int]:
        dofs = sides[side][0]
        return [i for i in range(data.num_records) if dofs[i] == dof]

    def stored_unit(side: str, dof: str) -> str | None:
        """The unit a channel's records already carry, if any do."""
        dofs, units = sides[side]
        for i in range(data.num_records):
            if dofs[i] == dof and units[i]:
                return units[i]
        return None

    declared = {(side, dof): stored_unit(side, dof)
                for side in sides for dof in channels[side]}

    def unit_for(side: str, dof: str) -> str | None:
        """What a channel is declared in — including channels off the pane,
        whose records may carry units from an earlier declaration."""
        key = (side, dof)
        return declared[key] if key in declared else stored_unit(side, dof)

    def apply(side: str, dof: str) -> None:
        for i in involving(side, dof):
            unit = unit_for('response', data.response_dof[i])
            reference = unit_for('reference', data.reference_dof[i])
            if unit and reference:
                data.define_units({i: unit}, {i: reference})

    def withdraw(side: str, dof: str) -> None:
        declared[(side, dof)] = None
        data.undefine_units(involving(side, dof))

    def hinted_type(side: str, dof: str) -> str:
        """The quantity a channel was taken to hold: an FRF's dimension is
        response over reference, and the side says which half is ours."""
        for i in involving(side, dof):
            response, _, reference = data.known_dim(i).partition('/')
            part = response if side == 'response' else reference
            if part and part != UNKNOWN:
                return part
        return ''

    def side_model(side: str,
                   unit_choices: Sequence[str]) -> TableModel:
        dofs = channels[side]
        types = [hinted_type(side, dof) for dof in dofs]

        def set_type(data: Any, row: int, text: str) -> None:
            text = stored_dimension(text)
            if text and text not in ORDINATE_UNITS:
                raise ValueError('expected one of ' + str(sorted(
                    shown_dimension(d) for d in ORDINATE_UNITS)))
            types[row] = text
            unit = unit_for(side, dofs[row])
            # a unit of some other kind is no longer an answer here
            if unit and (not text or dimension_of(unit) != text):
                withdraw(side, dofs[row])

        def set_unit(data: Any, row: int, text: str) -> None:
            text = text.strip().replace('^', '**')
            if not text:
                withdraw(side, dofs[row])
                return
            si_transform(text)    # raises before anything is changed
            declared[(side, dofs[row])] = text
            apply(side, dofs[row])
            # a chosen unit IS a statement of type (Brandon, 2026-08-30)
            types[row] = dimension_of(text) or types[row]

        columns = [
            Column('Channel', lambda d, r: dofs[r], alignment=LEFT),
            Column('Type', lambda d, r: shown_dimension(types[r]),
                   set=set_type, journal=_elsewhere,
                   choices=[shown_dimension(d) for d in ORDINATE_UNITS],
                   affects_row=True, alignment=LEFT),
            Column('Unit', lambda d, r: unit_for(side, dofs[r]) or '',
                   set=set_unit, journal=_elsewhere,
                   choices=unit_choices,
                   row_choices=lambda d, r: units_for(types[r], unit_choices),
                   choices_editable=True, carries=('Type',),
                   affects_row=True, alignment=LEFT),
        ]
        return TableModel(data, columns, lambda d: len(dofs), parent)

    return (side_model('response', ALL_ORDINATE_UNITS),
            side_model('reference', REFERENCE_UNITS))

id_runs_text

id_runs_text(ids) -> str

Sorted ids as runs: [1,2,3,7,9,10] -> '1-3 7 9-10'.

A block's elements are not a handful like an element's nodes — the demonstration drone puts 478 in one — and they come in runs, because a mesh is built a part at a time. Written out one by one the drone's 22 blocks need 21 731 characters; as runs they need 595, which is the difference between a cell you can read and one you cannot.

Source code in src/visualdynamics/gui/object_tables.py
def id_runs_text(ids) -> str:
    """Sorted ids as runs: `[1,2,3,7,9,10]` -> `'1-3 7 9-10'`.

    A block's elements are not a handful like an element's nodes — the
    demonstration drone puts 478 in one — and they come in runs,
    because a mesh is built a part at a time. Written out one by one
    the drone's 22 blocks need 21 731 characters; as runs they need
    595, which is the difference between a cell you can read and one
    you cannot.
    """
    ids = np.unique(np.asarray(list(ids), dtype=np.int64))
    if not len(ids):
        return ''
    runs, start, previous = [], int(ids[0]), int(ids[0])
    for value in ids[1:]:
        value = int(value)
        if value == previous + 1:
            previous = value
            continue
        runs.append((start, previous))
        start = previous = value
    runs.append((start, previous))
    return ' '.join(str(a) if a == z else f'{a}-{z}' for a, z in runs)

parse_id_runs

parse_id_runs(text: str) -> list[int]

The inverse: '1-3, 7 9-10' -> [1, 2, 3, 7, 9, 10].

Commas or spaces, and a run either way round — 10-7 is the same six elements as 7-10, because someone typing a range backwards means the range.

Source code in src/visualdynamics/gui/object_tables.py
def parse_id_runs(text: str) -> list[int]:
    """The inverse: `'1-3, 7 9-10'` -> `[1, 2, 3, 7, 9, 10]`.

    Commas or spaces, and a run either way round — `10-7` is the same
    six elements as `7-10`, because someone typing a range backwards
    means the range.
    """
    found: list[int] = []
    for part in str(text).replace(',', ' ').split():
        if '-' in part[1:]:
            head, _, tail = part[1:].partition('-')
            low, high = int(part[0] + head), int(tail)
            if low > high:
                low, high = high, low
            found.extend(range(low, high + 1))
        else:
            found.append(int(part))
    return found

block_label

block_label(geometry: Geometry, row: int) -> str

How a block reads in a list: its name, or its id when unnamed.

Exodus files often carry unnamed blocks, and 'block 3' is what a person calls that one — inventing a name for it would be putting words in the file's mouth.

Source code in src/visualdynamics/gui/object_tables.py
def block_label(geometry: Geometry, row: int) -> str:
    """How a block reads in a list: its name, or its id when unnamed.

    Exodus files often carry unnamed blocks, and 'block 3' is what a
    person calls that one — inventing a name for it would be putting
    words in the file's mouth.
    """
    name = geometry.block_name[row].strip()
    return name or f'block {int(geometry.block_id[row])}'

block_table_model

block_table_model(
    geometry: Geometry,
    unit_system: UnitSystem | None = None,
    parent: QObject | None = None,
) -> TableModel

The element blocks: what the mesh is divided into.

Three columns, all editable. The elements are listed as runs (1-36 156-245) and naming one here claims it for this block — the same gesture as typing a node into a traceline, with one difference the model forces: a node can be in no traceline at all, while an element is always in exactly one block. So the list adds and moves, and a removal that would orphan an element is refused with the reason (see _set_block_elements). The Elements column in the elements table moves them one at a time; this one moves them by the hundred.

Source code in src/visualdynamics/gui/object_tables.py
def block_table_model(geometry: Geometry,
                      unit_system: UnitSystem | None = None,
                      parent: QObject | None = None) -> TableModel:
    """The element blocks: what the mesh is divided into.

    Three columns, all editable. The elements are listed as runs
    (`1-36 156-245`) and naming one here claims it for this block —
    the same gesture as typing a node into a traceline, with one
    difference the model forces: a node can be in no traceline at all,
    while an element is always in exactly one block. So the list adds
    and moves, and a removal that would orphan an element is refused
    with the reason (see `_set_block_elements`). The Elements column
    in the *elements* table moves them one at a time; this one moves
    them by the hundred.
    """
    def set_name(geometry: Any, row: int, text: str) -> None:
        geometry.block_name[row] = text

    columns = [
        Column('Block', lambda g, r: int(g.block_id[r]),
               set=_renumber('renumber_block'),
               journal=_renumber_journal('renumber_block')),
        Column('Name', lambda g, r: g.block_name[r], set=set_name,
               journal=lambda g, r, t: f'.block_name[{r}] = {t!r}',
               alignment=LEFT),
        # The elements themselves, as runs, and editable: naming an
        # element here claims it for this block. Editing the *count*
        # would have been meaningless — a number is not a thing you
        # can change — and the count is still readable at a glance
        # from the runs.
        Column('Elements', _block_elements_text, set=_set_block_elements,
               # membership moves elements between blocks; the honest
               # replay is the post-state of the whole assignment array
               journal=lambda g, _r, _t: '.elem_block = np.array('
               f'{[int(b) for b in g.elem_block]!r})',
               alignment=LEFT),
    ]
    return TableModel(geometry, columns, lambda g: len(g.block_id), parent)

specification_model

specification_model(
    rows: Any, parent: QObject | None = None
) -> TableModel

A row per channel a specification carries: its name and its level.

What a specification says, in one number per channel. The plot draws one channel at a time — six targets and their limits stacked together are unreadable — so the table is where every channel is still accounted for, and picking a row is how you get to one.

The level is the specification's own, integrated from its own breakpoints over its own band, so it does not move with whatever happens to be measured against it.

Source code in src/visualdynamics/gui/object_tables.py
def specification_model(rows: Any,
                        parent: QObject | None = None) -> TableModel:
    """A row per channel a specification carries: its name and its level.

    What a specification says, in one number per channel. The plot draws
    one channel at a time — six targets and their limits stacked
    together are unreadable — so the table is where every channel is
    still accounted for, and picking a row is how you get to one.

    The level is the specification's own, integrated from its own
    breakpoints over its own band, so it does not move with whatever
    happens to be measured against it.
    """
    level = f' [{rows.unit_label}]' if rows.unit_label else ''
    return TableModel(rows, [
        Column('Channel', lambda h, r: h.rows[r][0], alignment=LEFT),
        Column(f'Specification RMS{level}',
               lambda h, r: h.rows[r][1].get('specification_rms'),
               format=_number),
    ], len, parent)

replication_model

replication_model(
    rows: Any,
    parent: QObject | None = None,
    units: str = "%",
) -> TableModel

A grid of channels down and repeats across.

Two comparisons share it, because they have the same shape: the waveform error against a target time history, in percent, and the RMS dB deviation of a shock spectrum against the one it had to meet. Only the unit in the heading differs.

Blank rather than zero for a channel whose target asks for nothing. A zero would read as a channel that matched perfectly, which is the opposite of what is known about it.

Source code in src/visualdynamics/gui/object_tables.py
def replication_model(rows: Any, parent: QObject | None = None,
                      units: str = '%') -> TableModel:
    """A grid of channels down and repeats across.

    Two comparisons share it, because they have the same shape: the
    waveform error against a target time history, in percent, and the
    RMS dB deviation of a shock spectrum against the one it had to
    meet. Only the unit in the heading differs.

    Blank rather than zero for a channel whose target asks for nothing.
    A zero would read as a channel that matched perfectly, which is the
    opposite of what is known about it.
    """
    columns = [Column('Channel', lambda h, r: h.channels[r], alignment=LEFT)]
    for index, event in enumerate(rows.events):
        columns.append(Column(
            f'Event {event + 1} [{units}]',
            lambda h, r, c=index: h.value(r, c),
            format=_number))
    return TableModel(rows, columns, len, parent)

compliance_grid_model

compliance_grid_model(
    rows: Any, parent: QObject | None = None
) -> TableModel

Channel, how far its level is out, and how much of its band is.

Two readings and not one, because they disagree often enough to be worth seeing together: a channel can sit at exactly the right level and still be out of tolerance across half its band, and one that is 2 dB low everywhere may never cross an abort limit at all.

Source code in src/visualdynamics/gui/object_tables.py
def compliance_grid_model(rows: Any,
                          parent: QObject | None = None) -> TableModel:
    """Channel, how far its level is out, and how much of its band is.

    Two readings and not one, because they disagree often enough to be
    worth seeing together: a channel can sit at exactly the right level
    and still be out of tolerance across half its band, and one that is
    2 dB low everywhere may never cross an abort limit at all.
    """
    return TableModel(rows, [
        Column('Channel', lambda h, r: _pair_text(h.rows[r][0]),
               alignment=LEFT),
        Column('RMS error [dB]', lambda h, r: h.rows[r][1], format=_number),
        Column('Outside abort [%]', lambda h, r: h.rows[r][2],
               format=_number),
    ], len, parent)