Skip to content

visualdynamics.gui.shock_panel

shock_panel

The shocks found in a time history, as a table beside it.

There is one row per event: when its window starts and how long it is. Both can be edited, because detection can miss or misplace an event, and they are the same two numbers that dragging a region on the plot changes, so the table and the plot show one list. Detect finds the events again, Add adds a window the detector missed, Remove deletes the selected one, and Same length for all gives every window one length.

Below the table are the SRS settings (Q, which peak, and lines per octave), the band the windows support, and the velocity check of every acceleration channel over every window (core.shock_check). Compute SRS computes the spectra with whatever is set at that moment, through the project verb a script calls, as the averaging panel does for a PSD.

Classes:

Name Description
ShockPanel

The events, and what can be done to them.

Classes

ShockPanel

ShockPanel(parent: Any = None)

Bases: QWidget

The events, and what can be done to them.

changed carries the whole list instead of the row that moved: the list is short, the receiver redraws the overlay from all of it either way, and a difference nobody uses is one more thing that can be wrong.

Methods:

Name Description
set_shocks

Show these shocks without emitting changed. With locked

srs_settings

The SRS settings as compute_srs keyword arguments: q,

show_check

Show the velocity check's words, core.shock_check.summarize,

show_band

Show the frequency band, in Hz, the current windows support.

Source code in src/visualdynamics/gui/shock_panel.py
def __init__(self, parent: Any = None) -> None:
    super().__init__(parent)
    #: the record's first instant: the table speaks the record's
    #: clock, a shock's start counts from the record's beginning
    self.origin: float = 0.0
    self.setFixedWidth(WIDTH)
    self.setSizePolicy(QSizePolicy.Policy.Fixed,
                       QSizePolicy.Policy.Expanding)
    self._shocks = ()
    self._writing = False

    layout = QVBoxLayout(self)
    layout.setContentsMargins(8, 8, 8, 8)
    layout.setSpacing(6)

    self.heading: QLabel = QLabel('Shocks')
    self.heading.setStyleSheet('font-weight: 600;')
    layout.addWidget(self.heading)

    self.table: QTableWidget = QTableWidget(0, 2, self)
    self.table.setHorizontalHeaderLabels(['Start [s]', 'Length [s]'])
    self.table.verticalHeader().setVisible(True)
    self.table.setSelectionBehavior(
        QAbstractItemView.SelectionBehavior.SelectRows)
    self.table.setEditTriggers(
        QAbstractItemView.EditTrigger.DoubleClicked
        | QAbstractItemView.EditTrigger.SelectedClicked
        | QAbstractItemView.EditTrigger.EditKeyPressed)
    self.table.horizontalHeader().setSectionResizeMode(
        QHeaderView.ResizeMode.Stretch)
    self.table.setFrameShape(QFrame.Shape.NoFrame)
    self.table.itemChanged.connect(self._edited)
    layout.addWidget(self.table, 1)

    self.same_length: QCheckBox = QCheckBox('Same length for all')
    self.same_length.setToolTip(
        'Analyze every event in a window of one length, so what '
        'differs between their spectra is the events.\n'
        'Dragging any edge then sets the length for all of them.')
    self.same_length.toggled.connect(self._length_mode)
    layout.addWidget(self.same_length)

    self.summary: QLabel = QLabel('')
    self.summary.setWordWrap(True)
    layout.addWidget(self.summary)

    buttons = QHBoxLayout()
    self.detect_button: QPushButton = QPushButton('Detect')
    self.detect_button.setToolTip(
        'Find the shocks in this record and replace the list')
    self.detect_button.clicked.connect(self.detect_asked.emit)
    buttons.addWidget(self.detect_button)
    self.add_button: QPushButton = QPushButton('Add')
    self.add_button.setToolTip(
        'Add a window the detector missed, in the largest open '
        'stretch — drag or type it into place')
    self.add_button.clicked.connect(self.add_asked.emit)
    buttons.addWidget(self.add_button)
    self.remove_button: QPushButton = QPushButton('Remove')
    self.remove_button.setToolTip('Drop the selected shock')
    self.remove_button.clicked.connect(self._remove)
    buttons.addWidget(self.remove_button)
    layout.addLayout(buttons)
    # the analysis choices the SRS is computed with (Brandon,
    # 2026-08-29): parameters of the act, recorded in its recipe —
    # a refresh after the windows move keeps them. There is no
    # mass setting because the standard SRS has none: the SDOF's
    # mass cancels for base-input response, so Q, spacing and
    # which peak are the whole parameter space.
    from ..core.srs import DEFAULT_Q, KINDS, PER_OCTAVE
    from .editors import DoubleSpinBox, SpinBox, commit_on_enter

    grid = QGridLayout()
    grid.setHorizontalSpacing(8)
    grid.setVerticalSpacing(4)
    self.q_box: DoubleSpinBox = DoubleSpinBox()
    self.q_box.setDecimals(1)
    self.q_box.setRange(1.0, 500.0)
    self.q_box.setValue(DEFAULT_Q)
    self.q_box.setToolTip(
        'The oscillator amplification, Q = 1/(2ζ): 10 is 5% '
        'damping, the shock-test convention')
    self.kind_box: QComboBox = QComboBox()
    # every peak `core.srs` keeps, maximax first as the default
    for key in sorted(KINDS, key=lambda kind: kind != 'maximax'):
        self.kind_box.addItem(key.capitalize(), key)
    self.kind_box.setToolTip(
        'Which peak each oscillator reports: the largest magnitude '
        'of either sign (maximax), or the positive or negative '
        'excursion alone')
    self.per_octave_box: SpinBox = SpinBox()
    self.per_octave_box.setRange(1, 96)
    self.per_octave_box.setValue(PER_OCTAVE)
    self.per_octave_box.setToolTip(
        'Natural-frequency lines per octave')
    for row, (label, editor) in enumerate((
            ('Q', self.q_box), ('Peak', self.kind_box),
            ('Per octave', self.per_octave_box))):
        grid.addWidget(QLabel(label), row, 0)
        grid.addWidget(editor, row, 1)
    band_name = QLabel('Band')
    band_name.setEnabled(False)
    self.band_label: QLabel = QLabel('—')
    self.band_label.setAlignment(Qt.AlignmentFlag.AlignRight
                                 | Qt.AlignmentFlag.AlignVCenter)
    self.band_label.setToolTip(
        'What the windows can support: low enough that the '
        'shortest window still holds a cycle, up to a fifth of '
        'the sample rate')
    grid.addWidget(band_name, 3, 0)
    grid.addWidget(self.band_label, 3, 1)
    layout.addLayout(grid)
    commit_on_enter(self.q_box, self.per_octave_box)

    # the velocity check, read before the spectrum is computed: a
    # record whose velocity does not come back to rest carries a
    # zero shift or an offset, and its spectrum is false at the low
    # end. Read only — the windows above are what it is read over.
    self.check_label: QLabel = QLabel('')
    self.check_label.setWordWrap(True)
    self.check_label.setToolTip(
        'Each acceleration channel integrated to velocity with '
        'nothing removed, read over each window from rest.\n'
        'A velocity that does not come back to rest is flagged.')
    self.check_label.hide()
    layout.addWidget(self.check_label)

    # the act these windows parameterize, right where they are set
    self.srs_button: QPushButton = QPushButton('Compute SRS')
    self.srs_button.setToolTip(
        'A shock response spectrum over each of these windows, '
        'every channel')
    self.srs_button.clicked.connect(self.srs_asked.emit)
    layout.addWidget(self.srs_button)
Methods:
set_shocks
set_shocks(shocks: Sequence[Shock], locked: bool = False) -> None

Show these shocks without emitting changed. With locked (a file that already cut the record into captures), the table and its editing buttons are disabled.

Source code in src/visualdynamics/gui/shock_panel.py
def set_shocks(self, shocks: Sequence[Shock],
               locked: bool = False) -> None:
    """Show these shocks without emitting `changed`. With `locked`
    (a file that already cut the record into captures), the table
    and its editing buttons are disabled."""
    shocks = tuple(shocks)
    self._shocks = shocks
    self._writing = True
    try:
        self.table.setRowCount(len(shocks))
        for row, shock in enumerate(shocks):
            for column, value in ((0, self.origin + shock.start),
                                  (1, shock.duration)):
                item = QTableWidgetItem(f'{value:.{PLACES}f}')
                item.setTextAlignment(
                    Qt.AlignmentFlag.AlignRight
                    | Qt.AlignmentFlag.AlignVCenter)
                if locked:
                    item.setFlags(item.flags()
                                  & ~Qt.ItemFlag.ItemIsEditable)
                self.table.setItem(row, column, item)
    finally:
        self._writing = False
    self.table.setEnabled(not locked)
    self.detect_button.setEnabled(not locked)
    # unlike Remove, Add serves the empty list too — that is the
    # list most in need of a first window
    self.add_button.setEnabled(not locked)
    self.remove_button.setEnabled(not locked and bool(shocks))
    # derived, never stored: whether the series is at one length is
    # already written in the lengths, and a second copy of it in
    # the project file is a copy that can disagree with them
    self._writing = True
    try:
        self.same_length.setChecked(uniform(shocks))
    finally:
        self._writing = False
    self.same_length.setEnabled(not locked and len(shocks) > 1)
    self._summarize(shocks, locked)
srs_settings
srs_settings() -> dict

The SRS settings as compute_srs keyword arguments: q, kind and per_octave.

Source code in src/visualdynamics/gui/shock_panel.py
def srs_settings(self) -> dict:
    """The SRS settings as `compute_srs` keyword arguments: `q`,
    `kind` and `per_octave`."""
    return {'q': float(self.q_box.value()),
            'kind': str(self.kind_box.currentData()),
            'per_octave': int(self.per_octave_box.value())}
show_check
show_check(text: str, flagged: bool = False) -> None

Show the velocity check's words, core.shock_check.summarize, in bold when a window is flagged; hidden when there are none.

Source code in src/visualdynamics/gui/shock_panel.py
def show_check(self, text: str, flagged: bool = False) -> None:
    """Show the velocity check's words, `core.shock_check.summarize`,
    in bold when a window is flagged; hidden when there are none."""
    self.check_label.setText(text)
    self.check_label.setStyleSheet('font-weight: 600;' if flagged
                                   else '')
    self.check_label.setVisible(bool(text))
show_band
show_band(low: float, high: float) -> None

Show the frequency band, in Hz, the current windows support.

Source code in src/visualdynamics/gui/shock_panel.py
def show_band(self, low: float, high: float) -> None:
    """Show the frequency band, in Hz, the current windows support."""
    self.band_label.setText(f'{low:.4g}–{high:.4g} Hz')

Functions: