Skip to content

visualdynamics.gui.shock_panel

shock_panel

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

One row per event: when its window opens and how long it runs. Both are editable, because detection is a good guess and not an oracle — and the same two numbers are what dragging a region on the plot changes, so the table and the trace are two views of one list.

The panel says where the events are, reports when that changes, and its Compute SRS button turns the history into spectra with whatever is set at that moment — the project verb a script calls. That is exactly how the averaging panel stands to 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 rather than which row moved: the list is short, the receiver rewrites the overlay from it either way, and a diff nobody uses is a thing that can be wrong.

Methods:

Name Description
set_shocks

Show these, without reporting them back as an edit.

srs_settings

The analysis choices as compute_srs keyword arguments.

show_band

State the band the current windows support.

Source code in src/visualdynamics/gui/shock_panel.py
def __init__(self, parent: Any = None) -> None:
    super().__init__(parent)
    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(
        'Analyse 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, 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()
    for key, label in (('maximax', 'Maximax'),
                       ('positive', 'Positive'),
                       ('negative', 'Negative')):
        self.kind_box.addItem(label, 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 act these windows parameterise, 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, without reporting them back as an edit.

Source code in src/visualdynamics/gui/shock_panel.py
def set_shocks(self, shocks: Sequence[Shock],
               locked: bool = False) -> None:
    """Show these, without reporting them back as an edit."""
    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, 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._summarise(shocks, locked)
srs_settings
srs_settings() -> dict

The analysis choices as compute_srs keyword arguments.

Source code in src/visualdynamics/gui/shock_panel.py
def srs_settings(self) -> dict:
    """The analysis choices as `compute_srs` keyword arguments."""
    return {'q': float(self.q_box.value()),
            'kind': str(self.kind_box.currentData()),
            'per_octave': int(self.per_octave_box.value())}
show_band
show_band(low: float, high: float) -> None

State the band the current windows support.

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

Functions: