Skip to content

visualdynamics.gui.sine_panel

sine_panel

The sine extraction's settings, as a table beside the time history.

How a sweep's levels are read out of the recording — the smoothing, automatic or set, and whether the sweep clock is refined against the recording — with what follows from it read beside the numbers: the cycles the automatic chose, the smoothing window in seconds at the sweep's ends, the scatter a reading is predicted to carry and how much of the sweep sits under the noise floor, and a preview of the readings themselves, sampled along each tone at the smoothing shown (core.sine.sample_levels: a few short solves, so it follows every edit). Beside it the Extract Sine Levels button, the project verb with whatever is set at that moment — the same division of labor as the filter panel, because it is the same kind of thing: parameters that ride the record (Brandon, 2026-09-30: a 0.5 g sweep under a 2.3 g random environment read with a 17 dB spread at the one smoothing the code allowed, and the lever had no control).

Classes:

Name Description
SamplePlot

The sampled readings against frequency: dots per control

SinePanel

The parameter table. Edits arrive as a whole SineExtraction.

Functions:

Name Description
with_chosen

The setting with what the automatic chose written on it.

Classes

SamplePlot

SamplePlot(parent: QWidget | None = None)

Bases: QWidget

The sampled readings against frequency: dots per control channel at the smoothing shown, the noise floor beside each as a hollow ring where the tone sits under it.

Small on purpose, like the filter panel's response: it answers "how clean does this smoothing read" at a glance, and the extracted levels answer the rest.

Methods:

Name Description
show_samples

Draw the sampled readings: one color per tone, a dot per

Source code in src/visualdynamics/gui/sine_panel.py
def __init__(self, parent: QWidget | None = None) -> None:
    super().__init__(parent)
    import pyqtgraph as pg

    from ..theme import theme as resolve_theme

    layout = QVBoxLayout(self)
    layout.setContentsMargins(0, 0, 0, 0)
    self.colors: Mapping[str, str] = resolve_theme(None)
    self.widget: Any = pg.PlotWidget()
    self.widget.setFixedHeight(self.HEIGHT)
    self.plot: Any = self.widget.getPlotItem()
    self.plot.setLogMode(x=True, y=True)
    self.plot.showGrid(x=True, y=True, alpha=0.3)
    self.plot.setMouseEnabled(x=False, y=False)
    self.plot.hideButtons()
    self.plot.setMenuEnabled(False)
    for edge in ('left', 'bottom'):
        axis = self.plot.getAxis(edge)
        axis.setStyle(tickTextOffset=2, tickLength=-3)
        axis.setTextPen(self.colors['plot_foreground'])
        axis.setPen(self.colors['plot_foreground'])
    self.plot.getAxis('left').setWidth(34)
    self.items: list[Any] = []
    layout.addWidget(self.widget)
Methods:
show_samples
show_samples(sampled: list[dict[str, Any]]) -> None

Draw the sampled readings: one color per tone, a dot per channel and point, rings where the tone was under its floor.

Source code in src/visualdynamics/gui/sine_panel.py
def show_samples(self, sampled: list[dict[str, Any]]) -> None:
    """Draw the sampled readings: one color per tone, a dot per
    channel and point, rings where the tone was under its floor."""
    import numpy as np
    import pyqtgraph as pg

    from ..plot import curve_color

    for item in self.items:
        self.plot.removeItem(item)
    self.items = []
    for k, reading in enumerate(sampled):
        color = curve_color(k, self.colors)
        amplitude = np.asarray(reading['amplitude'])
        floor = np.asarray(reading['floor'])
        frequency = np.asarray(reading['frequency'])
        under = amplitude <= floor
        shown = np.where(under, floor, amplitude)
        for row in range(amplitude.shape[0]):
            x = frequency[~under[row]]
            if x.size:
                self.items.append(self.plot.plot(
                    x, shown[row][~under[row]], pen=None, symbol='o',
                    symbolSize=5, symbolPen=None,
                    symbolBrush=pg.mkBrush(color)))
            x = frequency[under[row]]
            if x.size:
                self.items.append(self.plot.plot(
                    x, shown[row][under[row]], pen=None, symbol='o',
                    symbolSize=6, symbolBrush=None,
                    symbolPen=pg.mkPen(color, width=1)))

SinePanel

SinePanel(parent: QWidget | None = None)

Bases: QWidget

The parameter table. Edits arrive as a whole SineExtraction.

Methods:

Name Description
show_history

Point the panel at a record, the sweep it is read against

set_setting

Restate the panel from a setting, without re-emitting.

setting

The setting the editors currently describe, carrying what

Source code in src/visualdynamics/gui/sine_panel.py
def __init__(self, parent: QWidget | None = None) -> None:
    super().__init__(parent)
    #: the record and the sweep the parameters describe
    self._history: Any = None
    self._specification: Any = None
    #: what the automatic chose for this record, from the last
    #: sampling — carried on the setting as `chosen`
    self._chosen: float | None = None
    #: the last sampled readings, for the derived rows
    self._sampled: list[dict[str, Any]] = []
    #: set while the panel is writing to its own editors
    self._loading = False
    self.title: QLabel = QLabel('Sine Levels')
    grid = panel_grid(self, self.title)
    self.mode_box: QComboBox = QComboBox()
    self.mode_box.addItems(MODE_LABELS)
    self.mode_box.setToolTip(
        'Automatic: the smoothing climbs until a reading is '
        'predicted to scatter less than the target, measured from '
        'this recording. Set: the number of cycles you type')
    self.cycles_box: DoubleSpinBox = DoubleSpinBox()
    self.cycles_box.setRange(CYCLES_LADDER[0], CYCLES_LADDER[-1])
    self.cycles_box.setDecimals(0)
    self.cycles_box.setSingleStep(10.0)
    self.cycles_box.setValue(CYCLES_LADDER[0])
    self.cycles_box.setToolTip(
        'How many cycles of the tone’s own frequency each reading '
        'averages over. Fewer follow a resonance closely; more hold '
        'the random environment out of the reading')
    self.target_box: DoubleSpinBox = DoubleSpinBox()
    self.target_box.setRange(0.1, 6.0)
    self.target_box.setDecimals(1)
    self.target_box.setSingleStep(0.5)
    self.target_box.setSuffix(' dB')
    self.target_box.setValue(1.0)
    self.target_box.setToolTip(
        'The scatter the automatic smoothing aims to hold a '
        'reading under, one standard deviation')
    self.refine_box: QCheckBox = QCheckBox()
    self.refine_box.setChecked(True)
    self.refine_box.setToolTip(
        'Fit the sweep clock to the recording and solve again when '
        'the recorded sweep drifted from the one commanded — a long '
        'run whose rate was not quite the specification’s would '
        'otherwise read low toward its end')
    self._labels: dict[str, QLabel] = {}
    self._editors: dict[str, QWidget] = {}
    rows = (('mode', 'Smoothing', self.mode_box),
            ('cycles', 'Cycles', self.cycles_box),
            ('target', 'Scatter under', self.target_box),
            ('refine', 'Follow the clock', self.refine_box))
    for row, (key, label, editor) in enumerate(rows, start=1):
        name = QLabel(label)
        grid.addWidget(name, row, 0)
        grid.addWidget(editor, row, 1)
        self._labels[key] = name
        self._editors[key] = editor
    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 settings, read where they are set
    self.derived: dict[str, QLabel] = {}
    self.derived_names: dict[str, QLabel] = {}
    derived = (('chosen', 'Cycles used'),
               ('window', 'Window, start → end'),
               ('scatter', 'Predicted scatter'),
               ('under', 'Under the floor'))
    for key, (name, value) in add_derived(grid, derived,
                                          len(rows) + 2).items():
        self.derived[key] = value
        self.derived_names[key] = name
    self.preview: SamplePlot = SamplePlot()
    grid.addWidget(self.preview, len(rows) + 2 + len(derived), 0, 1, 2)
    self.extract_button: QPushButton = QPushButton('Extract Sine Levels')
    self.extract_button.setToolTip(
        'Read every tone’s level out of the recording with exactly '
        'the smoothing shown, one object for the set')
    self.extract_button.clicked.connect(self.apply_asked.emit)
    grid.addWidget(self.extract_button, len(rows) + 3 + len(derived),
                   0, 1, 2)
    grid.setRowStretch(len(rows) + 4 + len(derived), 1)
    commit_on_enter(self.cycles_box, self.target_box)
    self.mode_box.currentIndexChanged.connect(self._edited)
    self.cycles_box.valueChanged.connect(self._edited)
    self.target_box.valueChanged.connect(self._edited)
    self.refine_box.toggled.connect(self._edited)
    self._show_mode('automatic')
Methods:
show_history
show_history(history: TimeHistory, specification: SineSweepSpecification, setting: SineExtraction) -> None

Point the panel at a record, the sweep it is read against and the setting on it; sample the readings at that setting.

Source code in src/visualdynamics/gui/sine_panel.py
def show_history(self, history: TimeHistory,
                 specification: SineSweepSpecification,
                 setting: SineExtraction) -> None:
    """Point the panel at a record, the sweep it is read against
    and the setting on it; sample the readings at that setting."""
    self._history = history
    self._specification = specification
    self._chosen = setting.chosen
    self.set_setting(setting)
set_setting
set_setting(setting: SineExtraction) -> None

Restate the panel from a setting, without re-emitting.

Source code in src/visualdynamics/gui/sine_panel.py
def set_setting(self, setting: SineExtraction) -> None:
    """Restate the panel from a setting, without re-emitting."""
    self._loading = True
    try:
        self.mode_box.setCurrentIndex(
            MODES.index('automatic' if setting.automatic else 'set'))
        if setting.cycles is not None:
            self.cycles_box.setValue(float(setting.cycles))
        elif setting.chosen is not None:
            self.cycles_box.setValue(float(setting.chosen))
        self.target_box.setValue(float(setting.target_db))
        self.refine_box.setChecked(bool(setting.refine))
    finally:
        self._loading = False
    self._show_mode('automatic' if setting.automatic else 'set')
    self._restate()
setting
setting() -> SineExtraction

The setting the editors currently describe, carrying what the automatic chose for this record.

Source code in src/visualdynamics/gui/sine_panel.py
def setting(self) -> SineExtraction:
    """The setting the editors currently describe, carrying what
    the automatic chose for this record."""
    automatic = MODES[self.mode_box.currentIndex()] == 'automatic'
    return SineExtraction(
        cycles=None if automatic else float(self.cycles_box.value()),
        target_db=float(self.target_box.value()),
        refine=self.refine_box.isChecked(),
        chosen=self._chosen if automatic else None)

Functions:

with_chosen

with_chosen(setting: SineExtraction, chosen: float | None) -> SineExtraction

The setting with what the automatic chose written on it.

Source code in src/visualdynamics/gui/sine_panel.py
def with_chosen(setting: SineExtraction, chosen: float | None) -> SineExtraction:
    """The setting with what the automatic chose written on it."""
    return replace(setting, chosen=chosen)