Skip to content

visualdynamics.gui.modes_panel

modes_panel

The Modes pane beside the 3-D view: a geometry's mode shapes, rigid body or finite element.

The kind is chosen at the top. Kinematic is offered for every geometry; finite element only once its element groups have properties to build a model from, and with only one kind there is no choice shown. Both kinds are named for how the shapes are made: a finite element solve returns rigid-body modes too, at 0 Hz, so 'Rigid body' did not tell the two apart.

Kinematic: the six rigid-body shapes from the geometry alone. The settings are the reference point the rotations are about and, when the shape set is to be mass-normalized, the mass and the six terms of the inertia tensor about that point. The view previews the six shapes on the geometry as they are set, and the button at the bottom makes the shape set (generate_rigid_body_modes).

Finite element: the settings are how far to solve, either a number of modes (rigid-body modes included) or every mode up to a frequency, and the damping to give every mode. A solve is too slow to preview as the numbers change, so the readings say what will be solved (the nodes, elements, element groups and DOFs) and refuse a group with no properties before the solve does; the button solves (solve_modes).

This is select, see, set, apply (principle 13), laid out as in the truncate panel but with the preview in the 3-D view instead of on a plot. The panel makes nothing itself: it shows the settings in the display units, emits the rigid ones in SI as a MassProperties when they change, and the window runs the verb when a button is pressed.

Classes:

Name Description
ModesPanel

The settings table. Rigid-body edits are emitted as a whole

Classes

ModesPanel

ModesPanel(parent: QWidget | None = None)

Bases: QWidget

The settings table. Rigid-body edits are emitted as a whole MassProperties; the solve's two numbers are read when it runs.

Methods:

Name Description
offer_kinds

Offer the finite element kind beside the kinematic one, or

show_model

Say what a solve of geometry would build: its nodes,

limit

How far to solve, as the solve's keyword: num_modes, how

damping

The damping to give every mode, as a fraction of critical.

show_geometry

Point the panel at a geometry and the settings on it, in the

set_properties

Show the settings in the panel without emitting changed.

properties

The settings the editors currently describe, in SI.

Attributes:

Name Type Description
kind str

'fem' when the finite element kind is offered and chosen,

Source code in src/visualdynamics/gui/modes_panel.py
def __init__(self, parent: QWidget | None = None) -> None:
    super().__init__(parent)
    #: the geometry the settings describe, for counting DOFs and
    #: for knowing whether its coordinates mean anything yet
    self._geometry = None
    self._unit_system = None
    #: set while the panel is writing to its own editors, so that
    #: restating a value does not read as a fresh edit
    self._loading = False

    # One pane for both kinds of modes (Brandon, 2026-10-22): the
    # rigid-body reading and Solve Modes were two acts on one
    # geometry, the solve's settings in two dialogs.
    self.title: QLabel = QLabel('Modes')
    panel = panel_grid(self, self.title)
    self._fem_offered = False
    self.kind_label: QLabel = QLabel('Kind')
    self.kind_box: QComboBox = QComboBox()
    # 'Kinematic' and not 'Rigid body' (Brandon, 2026-10-22): the
    # solve's six 0 Hz modes are rigid-body modes as well
    self.kind_box.addItem('Kinematic', 'rigid')
    self.kind_box.addItem('Finite element', 'fem')
    # a model whose groups have properties is there to be solved
    self.kind_box.setCurrentIndex(1)
    self.kind_box.setToolTip(
        'Kinematic: the six rigid-body shapes about a point, from the '
        'geometry alone. Finite element: the normal modes solved from '
        "the element groups' properties")
    self.kind_box.currentIndexChanged.connect(self._kind_chosen)
    panel.addWidget(self.kind_label, 1, 0)
    panel.addWidget(self.kind_box, 1, 1)
    self.rigid_section: QWidget = QWidget()
    self.fem_section: QWidget = self._build_fem_section()
    panel.addWidget(self.rigid_section, 2, 0, 1, 2)
    panel.addWidget(self.fem_section, 3, 0, 1, 2)
    panel.setRowStretch(4, 1)
    grid = _section_grid(self.rigid_section)

    point = QLabel('Reference point')
    point.setEnabled(False)
    grid.addWidget(point, 1, 0, 1, 2)
    self.point_boxes: list[DoubleSpinBox] = []
    for row, axis in enumerate(('X', 'Y', 'Z'), start=2):
        box = DoubleSpinBox()
        box.setDecimals(LENGTH_DECIMALS)
        box.setRange(-1e9, 1e9)
        box.setToolTip(
            f'{axis} of the point the rotations pivot on — the CG, '
            'or wherever the virtual point is wanted. Starts at the '
            'centroid of the nodes')
        grid.addWidget(QLabel(axis), row, 0)
        grid.addWidget(box, row, 1)
        self.point_boxes.append(box)

    # mass and inertia are one optional group: both or neither
    # (`core.rigid` says why), so one checkbox reveals them. Named
    # for what it reveals, not for the result: 'Mass-normalize' did
    # not read as the place to enter a mass (Brandon, 2026-09-04)
    self.scale_check: QCheckBox = QCheckBox('Mass properties')
    self.scale_check.setToolTip(
        'Scale the shapes by the mass and the inertia about the '
        'point, so the set is mass-normalized like a solved one. '
        'Off, the shapes are unit translations and unit rotations')
    grid.addWidget(self.scale_check, 5, 0, 1, 2)
    self.scale_note: QLabel = QLabel(
        'Declare the length unit to mass-normalize')
    self.scale_note.setWordWrap(True)
    self.scale_note.setEnabled(False)
    grid.addWidget(self.scale_note, 6, 0, 1, 2)

    self.scale_panel: QWidget = QWidget()
    scale = QGridLayout(self.scale_panel)
    scale.setContentsMargins(0, 0, 0, 0)
    scale.setHorizontalSpacing(8)
    scale.setVerticalSpacing(4)
    self.mass_box: DoubleSpinBox = DoubleSpinBox()
    self.mass_box.setDecimals(4)
    self.mass_box.setRange(1e-9, 1e12)
    self.mass_box.setToolTip('The mass of the body')
    scale.addWidget(QLabel('Mass'), 0, 0)
    scale.addWidget(self.mass_box, 0, 1)
    self.inertia_boxes: dict[str, DoubleSpinBox] = {}
    for row, term in enumerate(INERTIA_TERMS, start=1):
        box = DoubleSpinBox()
        box.setDecimals(4)
        box.setRange(-1e12, 1e12)
        box.setToolTip(
            f'{term} of the inertia tensor about the point, as a '
            'CAD mass-properties report states it')
        scale.addWidget(QLabel(term), row, 0)
        scale.addWidget(box, row, 1)
        self.inertia_boxes[term] = box
    grid.addWidget(self.scale_panel, 7, 0, 1, 2)
    self.scale_panel.hide()

    add_rule(grid, 8)

    # what follows from the settings, shown so the set is known
    # before it is made
    self.derived: dict[str, QLabel] = {}
    derived = (('modes', 'Modes'), ('dofs', 'DOFs'), ('axes', 'Axes'),
               ('scaling', 'Scaling'))
    for key, (_name, value) in add_derived(grid, derived,
                                           9).items():
        self.derived[key] = value

    # the one refusal the boxes' ranges cannot state at entry: a
    # tensor no body could have, which only the six together say
    self.problem: QLabel = QLabel('')
    self.problem.setWordWrap(True)
    grid.addWidget(self.problem, 13, 0, 1, 2)

    # the act itself, where its settings are set
    self.apply_button: QPushButton = QPushButton(
        'Generate Rigid Body Mode Shapes')
    self.apply_button.setToolTip(
        'Make the six rigid-body shapes a shape set in the '
        "geometry's group")
    self.apply_button.clicked.connect(self.apply_asked.emit)
    grid.addWidget(self.apply_button, 14, 0, 1, 2)

    boxes = (*self.point_boxes, self.mass_box, *self.inertia_boxes.values())
    commit_on_enter(*boxes)
    for box in boxes:
        box.valueChanged.connect(self._edited)
    self.scale_check.toggled.connect(self._scale_toggled)
    self.offer_kinds(False)
Attributes
kind property
kind: str

'fem' when the finite element kind is offered and chosen, else 'rigid'.

Methods:
offer_kinds
offer_kinds(fem: bool) -> None

Offer the finite element kind beside the kinematic one, or not. With kinematic the only kind, no choice is shown.

Source code in src/visualdynamics/gui/modes_panel.py
def offer_kinds(self, fem: bool) -> None:
    """Offer the finite element kind beside the kinematic one, or
    not. With kinematic the only kind, no choice is shown."""
    self._fem_offered = bool(fem)
    self.kind_label.setVisible(self._fem_offered)
    self.kind_box.setVisible(self._fem_offered)
    self._show_kind()
show_model
show_model(geometry: Geometry) -> None

Say what a solve of geometry would build: its nodes, elements, element groups and DOFs. An element group holding elements but no properties is named, and the solve refused until it has them, as the model build would refuse it.

Source code in src/visualdynamics/gui/modes_panel.py
def show_model(self, geometry: Geometry) -> None:
    """Say what a solve of `geometry` would build: its nodes,
    elements, element groups and DOFs. An element group holding
    elements but no properties is named, and the solve refused until
    it has them, as the model build would refuse it."""
    self._geometry = geometry
    groups = sorted({int(group) for group in geometry.elem_group})
    missing = [group for group in groups
               if group not in geometry.group_properties]
    nodes = geometry.num_nodes
    self.fem_derived['nodes'].setText(f'{nodes:,}')
    self.fem_derived['elements'].setText(f'{len(geometry.elem_conn):,}')
    self.fem_derived['groups'].setText(
        f'{len(groups) - len(missing)} of {len(groups)} with properties'
        if missing else f'{len(groups)}')
    self.fem_derived['dofs'].setText(f'6 × {nodes:,}')
    if missing:
        ids = ', '.join(str(group) for group in missing)
        plural = len(missing) > 1
        self.fem_problem.setText(
            f'Element group{"s" if plural else ""} {ids} '
            f'{"have" if plural else "has"} no properties: give '
            f'{"each" if plural else "it"} a material in the Element '
            'Groups table to solve')
    else:
        self.fem_problem.setText('')
    self.solve_button.setEnabled(not missing and nodes > 0)
limit
limit() -> dict[str, float | int]

How far to solve, as the solve's keyword: num_modes, how many modes, rigid-body modes included, or maximum_frequency, the highest frequency in Hz.

Source code in src/visualdynamics/gui/modes_panel.py
def limit(self) -> dict[str, float | int]:
    """How far to solve, as the solve's keyword: ``num_modes``, how
    many modes, rigid-body modes included, or ``maximum_frequency``,
    the highest frequency in Hz."""
    if self.limit_box.currentData() == 'count':
        return {'num_modes': int(self.count_box.value())}
    return {'maximum_frequency': float(self.top_box.value())}
damping
damping() -> float

The damping to give every mode, as a fraction of critical.

Source code in src/visualdynamics/gui/modes_panel.py
def damping(self) -> float:
    """The damping to give every mode, as a fraction of critical."""
    return float(self.damping_box.value()) / 100.0
show_geometry
show_geometry(geometry: Geometry, properties: MassProperties, unit_system: UnitSystem) -> None

Point the panel at a geometry and the settings on it, in the display units of unit_system.

A geometry whose length unit is not declared shows its raw coordinates with no unit and cannot be mass-normalized, since an inertia in kg·m² means nothing against coordinates with no unit. For such a geometry the checkbox is hidden and a note says to declare the length unit.

Source code in src/visualdynamics/gui/modes_panel.py
def show_geometry(self, geometry: Geometry, properties: MassProperties,
                  unit_system: UnitSystem) -> None:
    """Point the panel at a geometry and the settings on it, in the
    display units of `unit_system`.

    A geometry whose length unit is not declared shows its raw
    coordinates with no unit and cannot be mass-normalized, since an
    inertia in kg·m² means nothing against coordinates with no unit.
    For such a geometry the checkbox is hidden and a note says to
    declare the length unit.
    """
    self._geometry = geometry
    self._unit_system = unit_system
    self._loading = True
    try:
        defined = geometry.units_defined
        length = f' {unit_system.label_text("length")}' if defined else ''
        for box in self.point_boxes:
            box.setSuffix(length)
        self.mass_box.setSuffix(f' {unit_system.label_text("mass")}')
        inertia = unit_system.label_text(INERTIA_DIMENSION)
        for box in self.inertia_boxes.values():
            box.setSuffix(f' {inertia}')
        self.scale_check.setVisible(defined)
        self.scale_note.setVisible(not defined)
    finally:
        self._loading = False
    self.set_properties(properties)
set_properties
set_properties(properties: MassProperties) -> None

Show the settings in the panel without emitting changed.

Source code in src/visualdynamics/gui/modes_panel.py
def set_properties(self, properties: MassProperties) -> None:
    """Show the settings in the panel without emitting `changed`."""
    self._loading = True
    try:
        for box, value in zip(self.point_boxes,
                              self._display(properties.point, 'length')):
            box.setValue(float(value))
        self.scale_check.setChecked(properties.scaled)
        self.scale_panel.setVisible(properties.scaled)
        if properties.scaled:
            self.mass_box.setValue(
                float(self._display(properties.mass, 'mass')))
            terms = self._display(properties.inertia, INERTIA_DIMENSION)
            for box, value in zip(self.inertia_boxes.values(), terms):
                box.setValue(float(value))
    finally:
        self._loading = False
    self._restate()
properties
properties() -> MassProperties

The settings the editors currently describe, in SI.

Raises ValueError for an inertia tensor no body could have, the one problem the boxes cannot catch one value at a time.

Source code in src/visualdynamics/gui/modes_panel.py
def properties(self) -> MassProperties:
    """The settings the editors currently describe, in SI.

    Raises `ValueError` for an inertia tensor no body could have,
    the one problem the boxes cannot catch one value at a time.
    """
    point = self._si([box.value() for box in self.point_boxes], 'length')
    if not (self.scale_check.isVisible() and self.scale_check.isChecked()):
        return MassProperties(tuple(point))
    return MassProperties(
        tuple(point), mass=float(self._si(self.mass_box.value(), 'mass')),
        inertia=tuple(self._si([box.value() for box in
                                self.inertia_boxes.values()],
                               INERTIA_DIMENSION)))

Functions: