Skip to content

visualdynamics.gui.wavelet_panel

wavelet_panel

What the scalogram is computed over, as a table beside it.

Three things are set here — the frequency range to listen across, how finely, and how the wavelet trades time against frequency — and three more are shown that follow from them: what the record can actually resolve at the bottom of the range, how wide the cone of influence is there, and how many transforms that adds up to.

Which record is not set here. It was, briefly, as a combo box — and that put the same choice in two places, because the project tree already selects records for every other view. The tree is the one selection everywhere (Brandon, 2026-08-29), so the scalogram reads its record off the selection and this panel keeps only what the tree cannot say.

The derived three are not decoration. A scalogram is the one reading in this toolset where the settings decide whether the picture is mostly artefact: ask for 1 Hz on a two-second record and the bottom of the axis is nothing but cone, and the picture will look perfectly plausible while saying nothing about the measurement. So the panel says it before the transform runs.

Nothing here computes. The panel says what the parameters are and reports when they move, the way the averaging panel does.

Classes:

Name Description
WaveletPanel

The scalogram's parameters. Edits arrive as a whole settings dict.

Classes

WaveletPanel

WaveletPanel(parent: QWidget | None = None)

Bases: QWidget

The scalogram's parameters. Edits arrive as a whole settings dict.

Methods:

Name Description
show_history

Point the panel at a time history, and at the settings on it.

suggested

The range this record can honestly carry.

settings

What the editors currently describe.

Source code in src/visualdynamics/gui/wavelet_panel.py
def __init__(self, parent: QWidget | None = None) -> None:
    super().__init__(parent)
    self.sample_rate: float = 1.0
    self.samples: int = 0
    self.duration: float = 0.0
    #: set while the panel writes to its own editors, so restating a
    #: clamped value does not read as a fresh edit
    self._loading = False

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

    self.low_box: DoubleSpinBox = DoubleSpinBox()
    self.low_box.setDecimals(2)
    self.low_box.setSuffix(' Hz')
    self.low_box.setToolTip(
        'The bottom of the frequency axis. Low frequencies use long '
        'wavelets, so this is what decides how much of the picture '
        'is cone of influence')

    self.high_box: DoubleSpinBox = DoubleSpinBox()
    self.high_box.setDecimals(2)
    self.high_box.setSuffix(' Hz')
    self.high_box.setToolTip(
        'The top of the frequency axis. Defaults to everything the '
        'record carries; the reading stays true to a fraction of a '
        'percent right up against Nyquist')

    self.per_octave_box: SpinBox = SpinBox()
    self.per_octave_box.setRange(1, 96)
    self.per_octave_box.setToolTip(
        'Lines per octave. The axis is logarithmic because a '
        "wavelet's bandwidth is a constant fraction of its "
        'frequency — evenly spaced lines would crowd at the top')

    self.omega_box: DoubleSpinBox = DoubleSpinBox()
    self.omega_box.setDecimals(1)
    self.omega_box.setRange(3.0, 30.0)
    self.omega_box.setSingleStep(1.0)
    self.omega_box.setToolTip(
        'Cycles under the wavelet, which is the trade itself: low '
        'resolves when and blurs what, high resolves what and '
        'blurs when. Six is conventional, and near the lowest value '
        'for which the transform is admissible at all')

    rows = (('From', self.low_box),
            ('To', self.high_box),
            ('Per octave', self.per_octave_box),
            ('Cycles', self.omega_box))
    for row, (label, editor) in enumerate(rows, start=1):
        grid.addWidget(QLabel(label), row, 0)
        grid.addWidget(editor, row, 1)

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

    self.derived: dict[str, QLabel] = {}
    derived = (('cone', 'Cone at bottom'), ('resolves', 'Resolves'),
               ('transforms', 'Lines'))
    for key, (_name, value) in add_derived(grid, derived,
                                           len(rows) + 2).items():
        self.derived[key] = value

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

    commit_on_enter(self.low_box, self.high_box, self.per_octave_box,
                    self.omega_box)
    for editor in (self.low_box, self.high_box, self.per_octave_box,
                   self.omega_box):
        editor.valueChanged.connect(self._edited)
Methods:
show_history
show_history(
    history: TimeHistory,
    settings: dict[str, Any] | None = None,
) -> None

Point the panel at a time history, and at the settings on it.

The range the record can carry is what every clamp is against, so it comes from the history rather than from whatever numbers the boxes are holding from the last one.

Source code in src/visualdynamics/gui/wavelet_panel.py
def show_history(self, history: TimeHistory,
                 settings: dict[str, Any] | None = None) -> None:
    """Point the panel at a time history, and at the settings on it.

    The range the record can carry is what every clamp is against,
    so it comes from the history rather than from whatever numbers
    the boxes are holding from the last one.
    """
    self.sample_rate = history.sample_rate
    self.samples = len(history.abscissa)
    self.duration = self.samples / self.sample_rate if self.sample_rate \
        else 0.0
    settled = dict(self.suggested()) if settings is None else dict(settings)
    # through the clamp, not straight into the boxes: a range tuned
    # on one record and carried to a slower one arrives above this
    # Nyquist, and each box clamping on its own would settle both
    # ends onto the same number — a range of no width, which is not
    # a range the derived rows can be computed from
    settled = self._clamped_against(settled, history.sample_rate)

    self._loading = True
    try:
        self._apply_limits()
        self.low_box.setValue(settled['low'])
        self.high_box.setValue(settled['high'])
        self.per_octave_box.setValue(settled['per_octave'])
        self.omega_box.setValue(settled['omega0'])
    finally:
        self._loading = False
    self._restate(self.settings())
suggested
suggested() -> dict[str, Any]

The range this record can honestly carry.

The top is a fraction of Nyquist — a wavelet up there is a couple of samples long — and the bottom is where a handful of the longest wavelets still fit inside the record rather than hanging off both ends of it. Opening on a range that is mostly cone would be opening on an artefact.

Source code in src/visualdynamics/gui/wavelet_panel.py
def suggested(self) -> dict[str, Any]:
    """The range this record can honestly carry.

    The top is a fraction of Nyquist — a wavelet up there is a
    couple of samples long — and the bottom is where a handful of
    the longest wavelets still fit inside the record rather than
    hanging off both ends of it. Opening on a range that is mostly
    cone would be opening on an artefact.
    """
    low, high = wavelet.default_range(self.sample_rate or 1.0,
                                      self.duration)
    return {'low': low, 'high': high,
            'per_octave': wavelet.PER_OCTAVE, 'omega0': wavelet.OMEGA0}
settings
settings() -> dict[str, Any]

What the editors currently describe.

Source code in src/visualdynamics/gui/wavelet_panel.py
def settings(self) -> dict[str, Any]:
    """What the editors currently describe."""
    return {'low': self.low_box.value(),
            'high': self.high_box.value(),
            'per_octave': self.per_octave_box.value(),
            'omega0': self.omega_box.value()}

Functions: