Skip to content

visualdynamics.gui.averaging_panel

averaging_panel

The averaging parameters, as a table beside the time history.

Five numbers are set here — start, frame length, overlap, window, frame count — and four more are shown that follow from them: where the analysis ends, how fine the frequency resolution will be, how long each frame lasts, and how many averages the PSD will actually be built from. The derived four are never edited, because a value that can be typed in two places disagrees with itself by the second edit.

The panel says what the parameters are, reports when they move, and computes with its own buttons (Compute PSDs, CPSDs, FRFs, Multiple Coherence, Spectra) — each calling the project verb a script calls, with whatever is set at that moment (principle 13).

A capture the controller already cut into frames arrives with its frame length and count settled by the file. The panel shows them, greyed, and leaves only the window live — there is no second frame inside a record to overlap with, and nowhere for the start to go.

Classes:

Name Description
AveragingPanel

The parameter table. Edits arrive as a whole Averaging.

Classes

AveragingPanel

AveragingPanel(parent: QWidget | None = None)

Bases: QWidget

The parameter table. Edits arrive as a whole Averaging.

Methods:

Name Description
chosen_window

Which window shapes each frame.

show_history

Point the panel at a time history and the averaging set on it.

set_averaging

Restate the panel from an averaging — what a drag arrives as.

averaging

The averaging the editors currently describe, unclamped.

Source code in src/visualdynamics/gui/averaging_panel.py
def __init__(self, parent: QWidget | None = None) -> None:
    super().__init__(parent)
    self.sample_rate: float = 1.0
    self.samples: int = 0
    self.records: int = 1
    #: the file already cut this record into one frame per
    #: average, so there is no stream to search for the test —
    #: every other parameter is still the user's to choose
    self.precut: bool = False
    #: the record the parameters describe, for working them out again
    self._history = None
    #: which window the parameter row is currently fitted to
    self._was_window: str | None = None
    #: set while the panel is writing to its own editors, so that
    #: restating a clamped value does not read as a fresh edit
    self._loading = False

    self.title: QLabel = QLabel('Averaging')
    grid = panel_grid(self, self.title)

    self.start_box: DoubleSpinBox = DoubleSpinBox()
    self.start_box.setDecimals(4)
    self.start_box.setSuffix(' s')
    self.start_box.setToolTip(
        'Where the analysis begins, from the start of the record')

    self.length_box: SpinBox = SpinBox()
    self.length_box.setRange(2, 2 ** 24)
    self.length_box.setToolTip(
        'Samples in each frame. The frequency resolution follows '
        'from it: a longer frame is a finer one')

    self.overlap_box: DoubleSpinBox = DoubleSpinBox()
    self.overlap_box.setRange(0.0, 99.0)
    self.overlap_box.setDecimals(1)
    self.overlap_box.setSuffix(' %')
    self.overlap_box.setToolTip(
        'How much of a frame the next one repeats')

    self.window_box: QComboBox = QComboBox()
    for name in WINDOWS:
        self.window_box.addItem(name.capitalize(), name)
    self.window_box.setToolTip(
        'The shape each frame is multiplied by before its FFT')

    # the one box a parameterised window brings with it — tukey's
    # alpha, kaiser's beta — shown only while such a window is
    # chosen (principle 3), its label restated to the parameter's
    # own name so the row never needs a second glance
    self.parameter_box: DoubleSpinBox = DoubleSpinBox()
    self.parameter_box.setDecimals(2)
    self.parameter_box.setToolTip(
        'The window’s own number. Tukey’s alpha is the tapered '
        'fraction — 0 is a boxcar, 1 is a hann; Kaiser’s beta '
        'trades main-lobe width for sidelobes, 14 sitting in the '
        'blackman-harris class')

    self.detrend_box: QComboBox = QComboBox()
    for key in DETRENDS:
        self.detrend_box.addItem(key.capitalize(), key)
    self.detrend_box.setToolTip(
        'How each frame is levelled before its window: nothing '
        '(the convention here and in sdynpy), its own mean '
        'removed (what scipy’s welch does by default), or a '
        'least-squares line taken out — for a record whose drift '
        'the frames should not carry into the low bins')

    self.frames_box: SpinBox = SpinBox()
    self.frames_box.setRange(1, 2 ** 20)
    self.frames_box.setToolTip(
        'Frames averaged per record. Drag the edge of the shaded '
        'region on the plot to change it there')

    rows = (('Start', self.start_box),
            ('Frame length', self.length_box),
            ('Overlap', self.overlap_box),
            ('Window', self.window_box),
            ('Alpha', self.parameter_box),
            ('Detrend', self.detrend_box),
            ('Frames', self.frames_box))
    for row, (label, editor) in enumerate(rows, start=1):
        name = QLabel(label)
        grid.addWidget(name, row, 0)
        grid.addWidget(editor, row, 1)
        if editor is self.parameter_box:
            self._parameter_label = name

    rule = QFrame()
    rule.setFrameShape(QFrame.Shape.HLine)
    rule.setFrameShadow(QFrame.Shadow.Sunken)
    grid.addWidget(rule, len(rows) + 1, 0, 1, 2)

    # what follows from the five, shown so the trade is visible while
    # it is being made rather than after the PSD comes out
    self.derived: dict[str, QLabel] = {}
    derived = (('averages', 'Averages'), ('resolution', 'Δf'),
               ('duration', 'Frame'), ('stop', 'Ends'))
    for key, (_name, value) in add_derived(grid, derived,
                                           len(rows) + 2).items():
        self.derived[key] = value

    self.detect_button: QPushButton = QPushButton('Detect')
    self.detect_button.setToolTip(
        'Work the parameters out from the record: find the settled '
        'stretch at the highest level it holds for long enough, and '
        'take as many frames as it will carry')
    grid.addWidget(self.detect_button, len(rows) + 2 + len(derived),
                   0, 1, 2)

    # the acts these frames parameterise, right where they are
    # set — deciding the framing and computing from it are one
    # intent, not a trip through the tree (Brandon, 2026-08-28)
    self.psds_button: QPushButton = QPushButton('Compute PSDs')
    self.psds_button.setToolTip(
        'Auto-power spectral density per channel, averaged over '
        'exactly these frames')
    self.psds_button.clicked.connect(self.psds_asked.emit)
    grid.addWidget(self.psds_button, len(rows) + 3 + len(derived),
                   0, 1, 2)
    self.cpsds_button: QPushButton = QPushButton('Compute CPSDs')
    self.cpsds_button.setToolTip(
        'The full cross-power matrix, phase kept, over these '
        'same frames')
    self.cpsds_button.clicked.connect(self.cpsds_asked.emit)
    grid.addWidget(self.cpsds_button, len(rows) + 4 + len(derived),
                   0, 1, 2)
    self.frfs_button: QPushButton = QPushButton('Compute FRFs…')
    self.frfs_button.setToolTip(
        'The transfer functions from the excitation channels, over '
        'these frames — the ellipsis asks which estimator first. '
        'Offered when the history holds excitation channels')
    self.frfs_button.clicked.connect(self.frfs_asked.emit)
    grid.addWidget(self.frfs_button, len(rows) + 5 + len(derived),
                   0, 1, 2)
    self.coherence_button: QPushButton = QPushButton(
        'Multiple Coherence')
    self.coherence_button.setToolTip(
        'How much of each response all the excitation channels '
        'together account for, line by line, over these same '
        'frames. Offered when the history holds excitation '
        'channels')
    self.coherence_button.clicked.connect(self.coherence_asked.emit)
    grid.addWidget(self.coherence_button,
                   len(rows) + 6 + len(derived), 0, 1, 2)
    self.spectra_button: QPushButton = QPushButton('Compute Spectra')
    self.spectra_button.setToolTip(
        'Averaged linear spectra per channel, over these frames')
    self.spectra_button.clicked.connect(self.spectra_asked.emit)
    grid.addWidget(self.spectra_button,
                   len(rows) + 7 + len(derived), 0, 1, 2)

    self.note: QLabel = QLabel()
    self.note.setWordWrap(True)
    self.note.setEnabled(False)
    grid.addWidget(self.note, len(rows) + 8 + len(derived), 0, 1, 2)
    grid.setRowStretch(len(rows) + 9 + len(derived), 1)

    commit_on_enter(self.start_box, self.overlap_box,
                    self.length_box, self.frames_box,
                    self.parameter_box)
    for editor in (self.start_box, self.overlap_box):
        editor.valueChanged.connect(self._edited)
    for editor in (self.length_box, self.frames_box):
        editor.valueChanged.connect(self._edited)
    self.window_box.currentIndexChanged.connect(self._edited)
    self.detrend_box.currentIndexChanged.connect(self._edited)
    self.parameter_box.valueChanged.connect(self._edited)
    self.detect_button.clicked.connect(self._detect)
Methods:
chosen_window
chosen_window() -> str

Which window shapes each frame.

Not window(): that is Qt's own verb on every widget, for the top-level window this one sits in, and taking the name would hand a string to the next caller that meant Qt's.

Source code in src/visualdynamics/gui/averaging_panel.py
def chosen_window(self) -> str:
    """Which window shapes each frame.

    Not `window()`: that is Qt's own verb on every widget, for the
    top-level window this one sits in, and taking the name would
    hand a string to the next caller that meant Qt's.
    """
    return self.window_box.currentData()
show_history
show_history(
    history: TimeHistory, averaging: Averaging
) -> None

Point the panel at a time history and the averaging set on it.

The record's length and rate are what every clamp is against, so they come from the history rather than being guessed from the numbers already in the boxes.

Source code in src/visualdynamics/gui/averaging_panel.py
def show_history(self, history: TimeHistory,
                 averaging: Averaging) -> None:
    """Point the panel at a time history and the averaging set on it.

    The record's length and rate are what every clamp is against, so
    they come from the history rather than being guessed from the
    numbers already in the boxes.
    """
    self._history = history
    self.sample_rate = history.sample_rate
    self.samples = len(history.abscissa)
    self.precut = history.split_into_frames
    self.records = max(history.records_per_channel.values(), default=1)
    # only what applies (principle 3): the reference-driven
    # estimates need excitation channels, and a button that can
    # only refuse is a question, not a control
    driven = bool(history.drive_dofs())
    self.frfs_button.setVisible(driven)
    self.coherence_button.setVisible(driven)
    self.set_averaging(averaging)
set_averaging
set_averaging(averaging: Averaging) -> None

Restate the panel from an averaging — what a drag arrives as.

Source code in src/visualdynamics/gui/averaging_panel.py
def set_averaging(self, averaging: Averaging) -> None:
    """Restate the panel from an averaging — what a drag arrives as."""
    self._loading = True
    try:
        # the limits first: a spin box clamps to the range it has at
        # the moment it is written, so setting the values against the
        # *previous* averaging's limits silently truncates them — a
        # detected 57 frames came out as the 22 the last frame length
        # allowed
        self._apply_limits(averaging)
        self.start_box.setValue(averaging.start)
        self.length_box.setValue(averaging.frame_length)
        self.overlap_box.setValue(averaging.overlap * 100.0)
        self.frames_box.setValue(averaging.frames)
        at = self.detrend_box.findData(averaging.detrend)
        if at >= 0:
            self.detrend_box.setCurrentIndex(at)
        index = self.window_box.findData(averaging.window)
        if index >= 0:
            self.window_box.setCurrentIndex(index)
        self._restate_parameter()
        self._was_window = averaging.window
        if averaging.window_parameter is not None:
            self.parameter_box.setValue(averaging.window_parameter)
    finally:
        self._loading = False
    self._restate(averaging)
averaging
averaging() -> Averaging

The averaging the editors currently describe, unclamped.

Source code in src/visualdynamics/gui/averaging_panel.py
def averaging(self) -> Averaging:
    """The averaging the editors currently describe, unclamped."""
    offered = self.window_box.currentData() in WINDOW_PARAMETERS
    return Averaging(frame_length=self.length_box.value(),
                     overlap=self.overlap_box.value() / 100.0,
                     window=self.window_box.currentData(),
                     window_parameter=(self.parameter_box.value()
                                       if offered else None),
                     detrend=self.detrend_box.currentData(),
                     frames=self.frames_box.value(),
                     start=self.start_box.value())

Functions: