Skip to content

visualdynamics.gui.averaging_panel

averaging_panel

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

The settings are the start, the frame length, the overlap, the window (with its parameter, for the Tukey and Kaiser windows), the detrend and the frame count. The values that follow from them are shown below and cannot be edited: how many averages the spectra are built from, what those averages are worth (the statistical degrees of freedom, nominal and discounted for overlap, and the 90 percent confidence band they give), the frequency resolution, how long each frame lasts, and where the analysis ends. They cannot be edited because a value that could be typed in two places would disagree with itself after the second edit. Detect fills the settings in from the record itself.

The panel says what the settings are, reports when they change, and computes with its own buttons (Compute PSDs, Compute CPSDs, Compute FRFs, Multiple Coherence and Compute Spectra), each calling the project verb a script calls, with whatever is set at that moment (principle 13). The FRF and coherence buttons are shown only when the history has excitation and response channels.

A capture the controller already cut into one record per average keeps every setting editable, with the file's frames as the default. Only Detect is disabled, because there is no stream to search for the test.

Classes:

Name Description
AveragingPanel

The settings table. Edits are emitted as a whole Averaging.

Classes

AveragingPanel

AveragingPanel(parent: QWidget | None = None)

Bases: QWidget

The settings table. Edits are emitted as a whole Averaging.

Methods:

Name Description
chosen_window

The name of the window applied to each frame.

show_history

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

set_averaging

Show an averaging in the panel without emitting changed. A

averaging

The averaging the editors currently describe, before it is

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
    #: the record's first instant, what the start box counts from
    self.origin: float = 0.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, on the record’s own clock')

    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 parameterized 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 leveled 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))
    names = add_settings(grid, rows)
    self._parameter_label = names[[editor for _label, editor in rows]
                                  .index(self.parameter_box)]

    add_rule(grid, len(rows) + 1)

    # 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] = {}
    # what the averages are worth beside how many there are: the
    # count alone hides that overlapped frames are not independent,
    # and the band is what the count means on the plot
    level = CONFIDENCE_LEVELS[0]
    # named statistical, since a DOF elsewhere in the app is a
    # node and direction (2026-10-16); abbreviated to fit the
    # panel's name column, which cut the full words off (2026-10-17),
    # the tooltips saying it in full
    derived = (('averages', 'Averages'),
               ('dof', 'Stat. DOF (2n)'),
               ('equivalent', 'Equivalent DOF'),
               ('band', f'{round(level * 100)} % band'),
               ('resolution', 'Δf'),
               ('duration', 'Frame'), ('stop', 'Ends'))
    tips = {
        'dof': 'Statistical degrees of freedom of the averaged '
               'spectrum: two per frame averaged. Not the '
               'measurement DOFs the channels are taken at',
        'equivalent': 'The degrees of freedom discounted for '
                      'overlap (Welch): overlapped frames share '
                      'samples, so they steady the average less '
                      'than as many independent frames would',
        'band': 'Where the true spectrum lies, at this confidence, '
                'about the averaged one: decibels below and above '
                'it, from the equivalent degrees of freedom '
                '(chi-square)'}
    for key, (name, value) in add_derived(grid, derived,
                                          len(rows) + 2).items():
        self.derived[key] = value
        if key in tips:
            name.setToolTip(tips[key])
            value.setToolTip(tips[key])

    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 parameterize, 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

The name of the window applied to each frame.

It is not called window() because that is Qt's own method on every widget, returning the top-level window this one is in, and a string in its place would break any caller that meant Qt's.

Source code in src/visualdynamics/gui/averaging_panel.py
def chosen_window(self) -> str:
    """The name of the window applied to each frame.

    It is not called `window()` because that is Qt's own method on
    every widget, returning the top-level window this one is in, and
    a string in its place would break any 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.

Every limit on the boxes comes from the record's length and sample rate, taken from the history instead of 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.

    Every limit on the boxes comes from the record's length and
    sample rate, taken from the history instead of guessed from the
    numbers already in the boxes.
    """
    self._history = history
    self.sample_rate = history.sample_rate
    self.samples = len(history.abscissa)
    # the box speaks the record's clock (a truncation keeps its
    # instants; an import window reads the run's); the averaging
    # counts from the record's beginning — `Averaging.stop`
    self.origin = float(history.abscissa[0]) if self.samples else 0.0
    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.reference_channels())
              and bool(history.response_channels()))
    self.frfs_button.setVisible(driven)
    self.coherence_button.setVisible(driven)
    self.set_averaging(averaging)
set_averaging
set_averaging(averaging: Averaging) -> None

Show an averaging in the panel without emitting changed. A drag on the plot arrives this way.

Source code in src/visualdynamics/gui/averaging_panel.py
def set_averaging(self, averaging: Averaging) -> None:
    """Show an averaging in the panel without emitting `changed`. A
    drag on the plot arrives this way."""
    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(self.origin + 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, before it is clamped to the record.

Source code in src/visualdynamics/gui/averaging_panel.py
def averaging(self) -> Averaging:
    """The averaging the editors currently describe, before it is
    clamped to the record."""
    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=max(self.start_box.value() - self.origin, 0.0),
                     start_set=True)

Functions: