Skip to content

visualdynamics.gui.author_panel

author_panel

The specification being edited, as tables beside the plot.

The panel lays out what an author states. One grid holds the breakpoints, with a level per channel, and another holds each pair's coherence and phase, with a row under it that sets every pair at once. Above them are the form (Breakpoints, or Interpolated onto frequency lines at a spacing), the two band constraints, Symmetric and Uniform, and how the levels are entered: Levels, a level at every breakpoint, or Slopes, a level at the first breakpoint and a slope in decibels per octave into each one after it, with the level the slope reaches shown beside it. Under Slopes an edit holds every slope it does not change, as the draft's with_slope and with_level do. Areas and running rms adds the reading of the breakpoint table under the grid (each segment's slope, area, share and the rms accumulated to its end, for the channel on the plot), and the window writes the areas on the plot and draws the running rms under it. The bands themselves are dragged on the plot beside the panel, which draws the autospectra with their bands. The same panel is used for every way in: a shape set's modal coordinates, a channel table's control channels, or a specification opened to edit.

The two grids are the application's ordinary tables, CopyPasteTableView over TableModel columns. So a column header selects the column, a row header the row, Cmd-click adds a cell, and copy, paste, Delete to clear, Batch Edit and the fill handle come from the one implementation every table uses. A frequency typed between two others re-sorts the breakpoints instead of being refused.

Nothing here computes anything. The panel hands back a SpecificationDraft in SI and reports when it changes, and the window applies every edit to the specification through Project.author_specification.

Classes:

Name Description
Sheet

The draft's fields in SI, as the two grids edit them.

AuthorPanel

The specification editor: the breakpoints and pairs grids, the

Functions:

Name Description
sheet_models

(breakpoints, pairs): the sheet's two grids as ordinary table

Classes

Sheet

Sheet(draft: SpecificationDraft, unit_system: UnitSystem)

The draft's fields in SI, as the two grids edit them.

The table models write here one cell at a time, and the panel reads a whole draft back. The display unit is applied when a cell is shown and undone when one is typed, so a cell nobody edited keeps its exact SI value.

Methods:

Name Description
draft

The sheet as a SpecificationDraft. The draft's own checks

sort

Put the breakpoints in frequency order, with their levels, so a

level_channel

The channel whose level the breakpoints grid shows in

Attributes:

Name Type Description
by_slope bool

Whether the levels are typed by slope here: the draft's entry

Source code in src/visualdynamics/gui/author_panel.py
def __init__(self, draft: SpecificationDraft,
             unit_system: UnitSystem) -> None:
    self.channels: list[str] = list(draft.channels)
    self.dims: list[str] = list(draft.dims)
    self.densities: list[str] = [f'{dim}**2/frequency'
                                 for dim in draft.dims]
    self.frequencies: list[float] = list(draft.frequencies)
    self.levels: list[list[float]] = [list(row) for row in draft.levels]
    self.pairs: dict[tuple[int, int], tuple[float, float] | None] = dict(
        draft.pairs)
    self.pair_keys: list[tuple[int, int]] = draft.pairable()
    self.sources: list[str] = list(draft.sources)
    self.spanning: bool = len(set(draft.sources)) > 1
    self.bands: list[dict] = [dict(entry) for entry in draft.bands]
    self.notes: list[str] = list(draft.notes)
    self.form: str = draft.form
    self.spacing: float | None = draft.spacing
    #: how the levels are typed, the draft's own; slopes are offered
    #: on the breakpoints form only (`by_slope`)
    self.entry: str = draft.entry
    self.unit_system: UnitSystem = unit_system
    #: set by a frequency edit: the rows have moved and the view
    #: has to be restated rather than repainted
    self.reordered: bool = False
    #: set by an edit under slope entry that moved the levels of
    #: other rows (every slope after it held): the view is restated
    self.spread: bool = False
Attributes
by_slope property
by_slope: bool

Whether the levels are typed by slope here: the draft's entry is 'slopes' and the sheet is on its breakpoints. The lines form is a controller's reading of the breakpoints, and a slope into each of four thousand lines is nothing anyone types.

Methods:
draft
draft() -> SpecificationDraft

The sheet as a SpecificationDraft. The draft's own checks raise ValueError for anything no specification could hold.

Source code in src/visualdynamics/gui/author_panel.py
def draft(self) -> SpecificationDraft:
    """The sheet as a `SpecificationDraft`. The draft's own checks
    raise ValueError for anything no specification could hold."""
    return SpecificationDraft(
        list(self.channels), list(self.dims), list(self.frequencies),
        [list(row) for row in self.levels], dict(self.pairs),
        self.bands, list(self.notes),
        list(self.sources), self.form, self.spacing, self.entry)
sort
sort() -> None

Put the breakpoints in frequency order, with their levels, so a frequency typed between two others moves to its place.

Source code in src/visualdynamics/gui/author_panel.py
def sort(self) -> None:
    """Put the breakpoints in frequency order, with their levels, so a
    frequency typed between two others moves to its place."""
    # Brandon, 2026-09-06: typed out of order, the sheet stalled.
    order = np.argsort(self.frequencies, kind='stable')
    self.frequencies = [self.frequencies[k] for k in order]
    self.levels = [[row[k] for k in order] for row in self.levels]
level_channel
level_channel(column: int) -> int | None

The channel whose level the breakpoints grid shows in column, or None for the frequency column and, by slope, a slope column (the grid then holds a level and a slope a channel).

Source code in src/visualdynamics/gui/author_panel.py
def level_channel(self, column: int) -> int | None:
    """The channel whose level the breakpoints grid shows in
    `column`, or None for the frequency column and, by slope, a
    slope column (the grid then holds a level and a slope a
    channel)."""
    if column < 1:
        return None
    if not self.by_slope:
        return column - 1
    k, slope = divmod(column - 1, 2)
    return None if slope else k

AuthorPanel

AuthorPanel(parent: QWidget | None = None)

Bases: QWidget

The specification editor: the breakpoints and pairs grids, the form buttons and the band constraints.

Each edit is reported on changed as a whole SpecificationDraft. The form buttons report on form_asked and the constraints on constraint_asked, both for the whole specification. There is no apply button, because the window applies every edit to the specification as it is made.

Methods:

Name Description
show_draft

Show a draft, with each channel's levels in the display units

draft

The draft the sheet holds, in SI. Raises ValueError when no

show_reading

Read the sheet's breakpoint table for channel (the one on

Attributes:

Name Type Description
areas_wanted bool

Whether the segment areas and the running rms are asked for,

Source code in src/visualdynamics/gui/author_panel.py
def __init__(self, parent: QWidget | None = None) -> None:
    super().__init__(parent)
    self._unit_system = None
    self._sheet: Sheet | None = None
    self._loading = False
    # a cell's edit is handed on a tick later, so a paste, a fill or
    # a Batch Edit is handed on once, after its last cell. Handed on
    # at once, the first cell's edit re-rendered the window, which
    # put fresh models on the grids, and the rest of a pasted block
    # went into the model just taken down: two cells pasted, one
    # landed (Brandon, 2026-10-30)
    self._settle: QTimer = QTimer(self)
    self._settle.setSingleShot(True)
    self._settle.setInterval(0)
    self._settle.timeout.connect(self._edits_settled)

    grid = QGridLayout(self)
    grid.setContentsMargins(8, 8, 8, 8)
    grid.setHorizontalSpacing(8)
    grid.setVerticalSpacing(4)
    # a minimum, not a fixed width: the sheet sits in a splitter
    # beside the plot and the user drags it as wide as the grids
    # need (Brandon, 2026-09-06)
    self.setSizePolicy(QSizePolicy.Policy.Preferred,
                       QSizePolicy.Policy.Preferred)
    self.setMinimumWidth(PANEL_WIDTH)

    self.title: QLabel = QLabel('Specification')
    font = self.title.font()
    font.setBold(True)
    self.title.setFont(font)
    # the two forms of one requirement (Brandon, 2026-09-06): the
    # breakpoints it is written from, or the same read onto a
    # controller's frequency lines. Editing is done on the few
    # points; the lines are for the controller's file. Which is
    # showing is the button that is down; the other converts
    head = QHBoxLayout()
    head.addWidget(self.title)
    head.addStretch(1)
    self.form_group: QButtonGroup = QButtonGroup(self)
    self.form_group.setExclusive(True)
    self.breakpoints_button: QPushButton = QPushButton('Breakpoints')
    self.breakpoints_button.setCheckable(True)
    self.breakpoints_button.setToolTip(
        'The few points the requirement is written from — the lines '
        'where its power laws bend')
    self.breakpoints_button.clicked.connect(self._to_breakpoints)
    self.interpolated_button: QPushButton = QPushButton('Interpolated')
    self.interpolated_button.setCheckable(True)
    self.interpolated_button.setToolTip(
        'The same requirement read onto evenly spaced frequency '
        'lines at the spacing beside, as a controller writes its target')
    self.interpolated_button.clicked.connect(self._to_interpolated)
    for button in (self.breakpoints_button, self.interpolated_button):
        self.form_group.addButton(button)
        head.addWidget(button)
    self.spacing_box: DoubleSpinBox = DoubleSpinBox()
    self.spacing_box.setRange(0.0, 100000.0)
    self.spacing_box.setDecimals(4)
    self.spacing_box.setSuffix(' Hz')
    self.spacing_box.setSpecialValueText('spacing?')
    self.spacing_box.setToolTip(
        'The frequency spacing of the interpolated lines — the '
        'specification\'s own, or a time history\'s averaging, '
        'or typed here')
    head.addWidget(self.spacing_box)
    grid.addLayout(head, 0, 0, 1, 4)
    # the bands' two constraints, for every channel of the object:
    # the bands themselves are on the plot, and these say how a
    # drag there moves them
    constraints = QHBoxLayout()
    constraints.addWidget(QLabel('Bands:'))
    self.symmetric_box: QCheckBox = QCheckBox('Symmetric')
    self.symmetric_box.setToolTip(
        'The band above the target is the band below, mirrored; a '
        'drag on one edge moves both')
    self.symmetric_box.toggled.connect(
        lambda on: self._constraint_toggled('symmetric', on))
    self.uniform_box: QCheckBox = QCheckBox('Uniform')
    self.uniform_box.setToolTip(
        'One band over the whole frequency range; a drag anywhere '
        'moves it everywhere')
    self.uniform_box.toggled.connect(
        lambda on: self._constraint_toggled('uniform', on))
    constraints.addWidget(self.symmetric_box)
    constraints.addWidget(self.uniform_box)
    constraints.addStretch(1)
    grid.addLayout(constraints, 1, 0, 1, 4)
    # how the levels are typed: a level at every breakpoint, or a
    # level at the first and a slope into each one after it, as
    # many written tables give them. The two are the same
    # requirement; which one is down decides what an edit holds
    entry = QHBoxLayout()
    self.entry_label: QLabel = QLabel('Enter:')
    entry.addWidget(self.entry_label)
    self.entry_group: QButtonGroup = QButtonGroup(self)
    self.entry_group.setExclusive(True)
    self.levels_button: QPushButton = QPushButton('Levels')
    self.levels_button.setCheckable(True)
    self.levels_button.setToolTip(
        'Type a level at every breakpoint; the slopes between them '
        'follow')
    self.levels_button.clicked.connect(lambda: self._entry_clicked('levels'))
    self.slopes_button: QPushButton = QPushButton('Slopes')
    self.slopes_button.setCheckable(True)
    self.slopes_button.setToolTip(
        'Type a level at the first breakpoint and a slope in dB per '
        'octave into each one after it; the level the slope reaches is '
        'shown beside it, and an edit holds every slope it does not '
        'change')
    self.slopes_button.clicked.connect(lambda: self._entry_clicked('slopes'))
    for button in (self.levels_button, self.slopes_button):
        self.entry_group.addButton(button)
        entry.addWidget(button)
    entry.addStretch(1)
    self.areas_box: QCheckBox = QCheckBox('Areas and running rms')
    self.areas_box.setToolTip(
        "Each segment's slope and area, its share of the whole, and the "
        'rms accumulated from the lowest breakpoint, for the channel on '
        'the plot: written on the plot, the running rms drawn dashed '
        'under it, and read out under the breakpoints')
    self.areas_box.toggled.connect(self._areas_toggled)
    entry.addWidget(self.areas_box)
    grid.addLayout(entry, 2, 0, 1, 4)
    # where the sheet came from, and what a door could not carry
    self.origin: QLabel = QLabel('')
    self.origin.setWordWrap(True)
    self.origin.setEnabled(False)
    grid.addWidget(self.origin, 3, 0, 1, 4)

    # the breakpoints: a row per frequency, a column per channel —
    # the ordinary table, with everything that brings
    self.points: CopyPasteTableView = CopyPasteTableView()
    self.points.setToolTip(
        'The target at each breakpoint, a power law between them — '
        'type a level per channel in the display units shown. Click '
        'a column or row header to select it, Cmd-click to add cells; '
        'copy, paste, Delete to clear, right-click for Batch Edit, '
        'drag the corner handle to fill down')
    self.points.setMinimumHeight(120)
    self.points.edits_applied.connect(self._edits_applied)
    grid.addWidget(self.points, 4, 0, 1, 4)
    buttons = QHBoxLayout()
    self.add_button: QPushButton = QPushButton('Add breakpoint')
    self.add_button.setToolTip('A new breakpoint after the selected one, '
                               'or at the end')
    self.add_button.clicked.connect(self._add_point)
    self.remove_button: QPushButton = QPushButton('Remove breakpoint')
    self.remove_button.setToolTip('Take the selected breakpoints out')
    self.remove_button.clicked.connect(self._remove_point)
    buttons.addWidget(self.add_button)
    buttons.addWidget(self.remove_button)
    buttons.addStretch(1)
    self.scale_box: DoubleSpinBox = DoubleSpinBox()
    self.scale_box.setRange(-60.0, 60.0)
    self.scale_box.setDecimals(2)
    self.scale_box.setSuffix(' dB')
    self.scale_box.setToolTip('Raise or lower the selected levels by '
                              'this much — select a column header for '
                              'a channel, a row header for a breakpoint')
    self.scale_button: QPushButton = QPushButton('Scale selected')
    self.scale_button.setToolTip('Raise or lower the selected levels by '
                                 'the decibels in the box beside')
    self.scale_button.clicked.connect(self._scale_selected)
    buttons.addWidget(self.scale_box)
    buttons.addWidget(self.scale_button)
    grid.addLayout(buttons, 5, 0, 1, 4)
    # the breakpoint table read, for the channel on the plot: shown
    # while Areas and running rms is ticked (`show_reading`)
    self.reading: QLabel = QLabel('')
    self.reading.setWordWrap(True)
    self.reading.setTextFormat(Qt.TextFormat.RichText)
    self.reading.setVisible(False)
    self._reading_channel: str | None = None
    grid.addWidget(self.reading, 6, 0, 1, 4)

    # the pairs: coherence and phase between every two channels,
    # unstated until the author says — independent is a statement
    pairs_label = QLabel('Cross terms')
    pairs_label.setEnabled(False)
    grid.addWidget(pairs_label, 7, 0, 1, 4)
    self.pairs: CopyPasteTableView = CopyPasteTableView()
    self.pairs.setToolTip(
        'The cross term of each pair from its coherence (0 '
        'independent, 1 fully coherent) and phase. A pair left blank '
        'is absent from the specification; Delete clears one')
    self.pairs.setMinimumHeight(110)
    self.pairs.edits_applied.connect(self._edits_applied)
    grid.addWidget(self.pairs, 8, 0, 1, 4)
    all_row = QHBoxLayout()
    all_row.addWidget(QLabel('All pairs:'))
    self.all_coherence: DoubleSpinBox = DoubleSpinBox()
    self.all_coherence.setRange(0.0, 1.0)
    self.all_coherence.setDecimals(3)
    self.all_coherence.setSingleStep(0.1)
    self.all_coherence.setToolTip('Coherence to state for every pair')
    self.all_phase: DoubleSpinBox = DoubleSpinBox()
    self.all_phase.setRange(-180.0, 180.0)
    self.all_phase.setDecimals(1)
    self.all_phase.setSuffix('°')
    self.all_phase.setToolTip('Phase to state for every pair')
    self.all_button: QPushButton = QPushButton('Set')
    self.all_button.setToolTip('State this coherence and phase for '
                               'every pair at once')
    self.all_button.clicked.connect(self._set_all_pairs)
    for widget in (self.all_coherence, self.all_phase, self.all_button):
        all_row.addWidget(widget)
    all_row.addStretch(1)
    grid.addLayout(all_row, 9, 0, 1, 4)

    # the bands are not here: they are dragged on the plot, per
    # section, for every selected channel at once (Brandon,
    # 2026-09-06 — the only place the bands are shown is the plot)
    self.problem: QLabel = QLabel('')
    self.problem.setWordWrap(True)
    grid.addWidget(self.problem, 10, 0, 1, 4)

    # no button: every edit lands on the specification the sheet
    # is open on as it is made (Brandon, 2026-09-06), so switching
    # to another object never leaves an edit behind
    grid.setRowStretch(11, 1)
Attributes
areas_wanted property
areas_wanted: bool

Whether the segment areas and the running rms are asked for, and the sheet is on its breakpoints, where they are offered.

Methods:
show_draft
show_draft(draft: SpecificationDraft, unit_system: UnitSystem, origin: str = '', spacing_hint: float | None = None) -> None

Show a draft, with each channel's levels in the display units of its density. origin says where the sheet came from and is shown with the draft's notes. spacing_hint is the line spacing to offer when the draft has none, such as a time history's averaging.

Source code in src/visualdynamics/gui/author_panel.py
def show_draft(self, draft: SpecificationDraft, unit_system: UnitSystem,
               origin: str = '',
               spacing_hint: float | None = None) -> None:
    """Show a draft, with each channel's levels in the display units
    of its density. `origin` says where the sheet came from and is
    shown with the draft's notes. `spacing_hint` is the line spacing
    to offer when the draft has none, such as a time history's
    averaging."""
    self._unit_system = unit_system
    self._loading = True
    try:
        (self.interpolated_button if draft.form == 'lines'
         else self.breakpoints_button).setChecked(True)
        # slopes are typed on the breakpoints; on the lines form
        # the choice does not apply and is not shown
        (self.slopes_button if draft.entry == 'slopes'
         else self.levels_button).setChecked(True)
        # and so are the areas, which read a breakpoint table; a
        # controller's lines are a density per line, integrated as
        # bins (`SpecificationDraft.reading`)
        for widget in (self.entry_label, self.levels_button,
                       self.slopes_button, self.areas_box):
            widget.setVisible(draft.form == 'breakpoints')
        # the constraints read off the channels: on when every
        # channel of the sheet has them
        self.symmetric_box.setChecked(
            all(entry['symmetric'] for entry in draft.bands))
        self.uniform_box.setChecked(
            all(entry['uniform'] for entry in draft.bands))
        spacing = draft.spacing if draft.spacing is not None else spacing_hint
        self.spacing_box.setValue(spacing if spacing is not None else 0.0)
        said = origin
        if draft.notes:
            said = (said + ' — ' if said else '') + '; '.join(draft.notes)
        self.origin.setText(said)
        self.origin.setVisible(bool(said))
        self._sheet = Sheet(draft, unit_system)
        self._install_models()
    finally:
        self._loading = False
    self._restate()
draft
draft() -> SpecificationDraft

The draft the sheet holds, in SI. Raises ValueError when no sheet is open, or for anything no specification could hold.

Source code in src/visualdynamics/gui/author_panel.py
def draft(self) -> SpecificationDraft:
    """The draft the sheet holds, in SI. Raises ValueError when no
    sheet is open, or for anything no specification could hold."""
    if self._sheet is None:
        raise ValueError('no sheet is open')
    return self._sheet.draft()
show_reading
show_reading(channel: str | None = None) -> None

Read the sheet's breakpoint table for channel (the one on the plot; the first when it is not on the sheet) under the grid: each segment's slope, area, share of the whole and the rms accumulated to its end, then the total, in the display units. Hidden while Areas and running rms is off, and on the lines form, where it is not offered. A table of more segments than are labeled on the plot is read as its total alone.

Source code in src/visualdynamics/gui/author_panel.py
def show_reading(self, channel: str | None = None) -> None:
    """Read the sheet's breakpoint table for `channel` (the one on
    the plot; the first when it is not on the sheet) under the
    grid: each segment's slope, area, share of the whole and the
    rms accumulated to its end, then the total, in the display
    units. Hidden while Areas and running rms is off, and on the
    lines form, where it is not offered. A table of more segments
    than are labeled on the plot is read as its total alone."""
    from ..plot.breakpoints import MOST_LABELS, readout

    self._reading_channel = channel
    if not self.areas_wanted or self._sheet is None:
        self.reading.setVisible(False)
        return
    try:
        draft = self.draft()
    except ValueError:
        self.reading.setVisible(False)
        return
    k = draft.channels.index(channel) if channel in draft.channels else 0
    rows = readout(draft.reading(k), draft.dims[k], self._unit_system)
    shown = rows if len(rows) - 1 <= MOST_LABELS else rows[-1:]
    right = ' align="right"'
    cells = ''.join(
        '<tr>' + ''.join(f'<td{align}>{row[key]}</td>' for key, align in (
            ('segment', ''), ('slope', ''), ('area', right),
            ('share', right), ('running', right))) + '</tr>'
        for row in shown)
    head = ('<tr><td><i>segment</i></td><td><i>slope</i></td>'
            f'<td{right}><i>area</i></td><td{right}><i>share</i></td>'
            f'<td{right}><i>rms to its end</i></td></tr>')
    said = ('' if shown is rows else
            f' ({len(rows) - 1} segments, too many to list: the total '
            'alone)')
    self.reading.setText(
        f'<b>{draft.channels[k]}</b>, the channel on the plot{said}'
        f'<table cellspacing="0" cellpadding="2">{head}{cells}</table>')
    self.reading.setVisible(True)

Functions:

sheet_models

sheet_models(sheet: Sheet, parent: Any = None) -> tuple[TableModel, TableModel]

(breakpoints, pairs): the sheet's two grids as ordinary table models, which the table conventions test holds to the same contract as every other table. The window records each edit in the journal through the verb it calls, so each editable column is declared as journaled elsewhere.

Source code in src/visualdynamics/gui/author_panel.py
def sheet_models(sheet: Sheet, parent: Any = None
                 ) -> tuple[TableModel, TableModel]:
    """(breakpoints, pairs): the sheet's two grids as ordinary table
    models, which the table conventions test holds to the same contract
    as every other table. The window records each edit in the journal
    through the verb it calls, so each editable column is declared as
    journaled elsewhere."""
    unit_system = sheet.unit_system
    points = [Column('Hz', lambda s, r: s.frequencies[r], set=_set_frequency,
                     format=lambda v: f'{v:g}', journal=_elsewhere)]
    for k, (name, density) in enumerate(zip(sheet.channels, sheet.densities)):
        source = f'{sheet.sources[k]}\n' if sheet.spanning else ''
        title = f'{source}{name}\n{unit_system.label_text(density)}'
        points.append(Column(title, _level_getter(k), set=_level_setter(k),
                             format=lambda v: f'{v:.6g}', journal=_elsewhere,
                             affects_row=sheet.by_slope))
        if sheet.by_slope:
            # by slope, each channel's level is shown beside the slope
            # into it: the level the slope reaches, which is what a
            # written table leaves to the reader
            points.append(Column(
                f'{source}{name}\ndB/oct', _slope_getter(k),
                set=_slope_setter(k), format=_slope_text,
                journal=_elsewhere, affects_row=True,
                does_not_apply=lambda s, r: NO_SLOPE_IN if r == 0 else ''))
    pairs = [Column('Pair', _pair_label),
             Column('Coherence', _pair_getter(0), set=_pair_setter(0),
                    format=_number_text, journal=_elsewhere),
             Column('Phase °', _pair_getter(1), set=_pair_setter(1),
                    format=_number_text, journal=_elsewhere)]
    return (TableModel(sheet, points, lambda s: len(s.frequencies), parent),
            TableModel(sheet, pairs, lambda s: len(s.pair_keys), parent))