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: