Skip to content

visualdynamics.core.overload

overload

Each channel of a record checked against its range.

Every input measures over a window, its range, and a signal larger than the window is clipped: each sample beyond the edge is written as the edge, and the tops of the waveform come out flat. The averaged spectrum does not show it. Clipping takes a little energy off the few peaks that cross the edge and spreads it across the band as harmonics, and over dozens of frames the result is a spectrum a fraction of a decibel low in band with a slightly raised floor above it. The kurtosis barely moves either: a Gaussian record clipped at 2.8 standard deviations reads about 2.86, inside the kurtosis bars' band of 2 to 4. The overload is in the time history and nowhere else, so this reads it there, channel by channel, against each channel's own range.

The reading of a record against a range is core.time_statistics's, and this does not read it again: the samples on the rail (within RAIL_MARGIN of the range, judged against RAIL_SAMPLES), the crest factor, the peak and the kurtosis are that reading's, over the same span the kurtosis bars read (core.kurtosis.analyzed_span). What this adds is what a range makes of them:

  • The range itself, read from the channel table for every channel at once, where the Statistics reading takes one range typed for one channel.
  • The samples on the rail as a rate per second, beside the count and the share.
  • The headroom of the range over the largest excursion, in dB: 20 log10(range / peak). At or below 0 dB the range was reached.
  • The largest excursion scaled to the next level of the test, a stated step in dB up (6 dB, about twice the amplitude, by default), and its headroom there. A test is run up in steps, and a range that holds at one level is checked against the next level's peak before the step, not found wanting after it. The scaled peak is a starting point: structures at full level often do not behave as they did at the low level, so it is never a ceiling.
  • Samples beyond the range: further out than the range and its margin. A converter cannot write a sample past its own range, so a record that holds one was not taken through the range the table states: the ranges or the Sensitivity in the table are not the ones the record was made with, and every other number here is against the wrong range.

The range comes from the channel table. The level a channel clips at is ChannelTable.clip_ranges(), in volts: the smaller of its DAQ Range (the input's range) and its Sensor Range (the sensor's own full-scale output), whichever is stated. Its Sensitivity column is the chain's sensitivity in mV per the unit beside it, so the range in that unit is volts * 1000 / sensitivity, and in SI it is that times the unit's scale. table_full_scales reads it for each channel of a time history by the same match the role pairing uses (ChannelTable.identities). A channel the table does not name, or names with no sensitivity, or in a unit of some other quantity, has no range, and says why rather than being judged against a guess. Two rows naming one channel give it a range only when they agree, and say so. A range is a size either way, so a unit with an offset (a temperature) converts by its scale alone. A row that states neither range gives its channel none, and the channel says so rather than being read against a range nobody stated.

overload_check reads every channel of a TimeHistory (also TimeHistory.overload_check), channel_overload one plain array; each channel's result is a ChannelOverload, which carries its TimeStatistics, and the whole an OverloadCheck.

What is judged: the samples beyond the range (none), the samples on the rail (the time statistics' own judgment of them), and the peak scaled to the next level against the range. No headroom is asked for beyond that: how much a range should carry over its predicted peak is a choice a test procedure makes, and the procedures put no single number on it, so the headroom is reported and the step is set where the next level is.

References

What a channel is ranged against and what clipping costs are set out in the test standard this check follows.

  1. Department of Defense (2019). MIL-STD-810H, Environmental Engineering Considerations and Laboratory Tests. Method 517.3, paragraph 4.4.1.1c: a channel ranged for the peak it is expected to see, and a clipped signal from which no data can be recovered. Method 516.8, paragraph 4.5: clipping found in a record, and a record that shows it set aside.

Classes:

Name Description
TableRanges

Each channel's range as a channel table gives it.

ChannelOverload

One channel's record against its range.

OverloadCheck

Every channel read against its range, and the judgment.

Functions:

Name Description
range_in_unit

The range in the channel's own unit: a range in volts over a

table_full_scales

Each channel's range in SI from a channel table, and why where

channel_overload

Read one record against its range.

overload_check

Read each channel of a time history against its range.

headroom_rows

[(label, headroom in dB)] for each channel, NaN where there is

notes_for

{row: why} for each channel with no range: the sentence the

Classes

TableRanges

Bases: NamedTuple

Each channel's range as a channel table gives it.

One entry per channel of the history, in order: full_scales in SI, NaN where there is none; reasons, why there is none, '' where the range was read; and notes, anything else to say about the rows the range came from, ''.

ChannelOverload dataclass

ChannelOverload(record: int, label: str, full_scale: float, statistics: TimeStatistics | None, step_db: float = STEP_DB, reason: str = '', beyond: int = 0, note: str = '')

One channel's record against its range.

full_scale and the values read from statistics are in the record's own units (SI for a channel of a TimeHistory). Without a range, full_scale is NaN, reason says why, and nothing is judged; without a reading at all (a channel with fewer than two finite samples), statistics is None as well.

Methods:

Name Description
flags

Each judged item, by OVERLOADS key, and whether it is

describe

The channel's judgment in words.

cautions

What to doubt about the reading, in words: samples left out

Attributes:

Name Type Description
has_range bool

Whether the channel was read against a range.

peak float

The largest excursion either way, a size: NaN without a

rms float

The rms over the span read; NaN without a reading.

crest float

The crest factor: the largest excursion over the rms.

rail int | None

The samples on the rail; None without a range.

rail_share float | None

The samples on the rail as a share of those read; None

rail_rate float | None

The samples on the rail per second of the span read; None

headroom_db float

The range over the largest excursion in dB, 20 log10 of the

next_peak float

The largest excursion scaled up by step_db, a size: what

next_headroom_db float

The headroom at the next level, in dB: headroom_db less the

flagged list[str]

The keys flagged, in OVERLOADS order.

Attributes
has_range property
has_range: bool

Whether the channel was read against a range.

peak property
peak: float

The largest excursion either way, a size: NaN without a reading.

rms property
rms: float

The rms over the span read; NaN without a reading.

crest property
crest: float

The crest factor: the largest excursion over the rms.

rail property
rail: int | None

The samples on the rail; None without a range.

rail_share property
rail_share: float | None

The samples on the rail as a share of those read; None without a range.

rail_rate property
rail_rate: float | None

The samples on the rail per second of the span read; None without a range.

headroom_db property
headroom_db: float

The range over the largest excursion in dB, 20 log10 of the time statistics' headroom ratio. At or below 0 the range was reached; infinite for a record that never leaves zero; NaN without a range.

next_peak property
next_peak: float

The largest excursion scaled up by step_db, a size: what the next level would put on the channel if the structure behaves as it did at this one.

next_headroom_db property
next_headroom_db: float

The headroom at the next level, in dB: headroom_db less the step.

flagged property
flagged: list[str]

The keys flagged, in OVERLOADS order.

Methods:
flags
flags() -> dict[str, bool]

Each judged item, by OVERLOADS key, and whether it is flagged; empty for a channel with no range. The rail is the time statistics' own judgment (TimeStatistics.flags). The next level is judged only where the range has not already been reached, so a clipped channel is flagged for its clip and not again for a scaled peak that predicts nothing.

Source code in src/visualdynamics/core/overload.py
def flags(self) -> dict[str, bool]:
    """Each judged item, by `OVERLOADS` key, and whether it is
    flagged; empty for a channel with no range. The rail is the time
    statistics' own judgment (`TimeStatistics.flags`). The next
    level is judged only where the range has not already been
    reached, so a clipped channel is flagged for its clip and not
    again for a scaled peak that predicts nothing."""
    if not self.has_range:
        return {}
    stats = self.statistics
    reached = bool(stats.rail)
    # the scaled peak within the margin of the range is on the rail
    # at the next level
    edge = (1.0 - stats.margin) * self.full_scale
    return {
        'beyond': self.beyond > 0,
        'rail': stats.flags().get('rail', False),
        'next': not reached and self.next_peak >= edge,
    }
describe
describe(unit: str = '', scale: float = 1.0) -> str

The channel's judgment in words.

scale divides the values (SI for a channel of a time history) into unit for the words, as a unit system's transform scale does: 9.80665 and 'g' state an acceleration in g.

Source code in src/visualdynamics/core/overload.py
def describe(self, unit: str = '', scale: float = 1.0) -> str:
    """The channel's judgment in words.

    `scale` divides the values (SI for a channel of a time history)
    into `unit` for the words, as a unit system's transform scale
    does: 9.80665 and 'g' state an acceleration in g.
    """
    if self.statistics is None:
        return f'{self.label}: not read ({self.reason}).'
    if not np.isfinite(self.full_scale):
        return f'{self.label}: no range ({self.reason}).'
    return self._judged(unit, scale) + ''.join(
        f' {caution[0].upper()}{caution[1:]}.'
        for caution in self.cautions())
cautions
cautions() -> list[str]

What to doubt about the reading, in words: samples left out because they are not finite, and any note on the rows the range came from. Empty when there is nothing.

Source code in src/visualdynamics/core/overload.py
def cautions(self) -> list[str]:
    """What to doubt about the reading, in words: samples left out
    because they are not finite, and any note on the rows the range
    came from. Empty when there is nothing."""
    found = []
    left = 0 if self.statistics is None else self.statistics.nonfinite
    if left:
        found.append(f'{left} sample{"" if left == 1 else "s"} '
                     f'{"is" if left == 1 else "are"} not finite and '
                     'left out')
    if self.note:
        found.append(self.note)
    return found

OverloadCheck dataclass

OverloadCheck(channels: tuple[ChannelOverload, ...], step_db: float = STEP_DB, margin: float = RAIL_MARGIN)

Every channel read against its range, and the judgment.

Methods:

Name Description
with_step

This check with the step to the next level at step_db, the

with_flag

The channels flagged for key (an OVERLOADS key).

describe

The judgment in words: which channels are on the rail,

Attributes:

Name Type Description
checked list[ChannelOverload]

The channels read against a range.

unranged list[ChannelOverload]

The channels with no range, or not read at all, each saying

unread list[ChannelOverload]

The channels not read at all (a dead channel, or one with too

flagged list[ChannelOverload]

The channels flagged for anything.

worst ChannelOverload | None

The channel that most wants looking at: the most samples on

Attributes
checked property
checked: list[ChannelOverload]

The channels read against a range.

unranged property
unranged: list[ChannelOverload]

The channels with no range, or not read at all, each saying why.

unread property
unread: list[ChannelOverload]

The channels not read at all (a dead channel, or one with too few finite samples), each saying why.

flagged property
flagged: list[ChannelOverload]

The channels flagged for anything.

worst property
worst: ChannelOverload | None

The channel that most wants looking at: the most samples on the rail, then the least headroom at the next level. None when no channel has a range.

Methods:
with_step
with_step(step_db: float) -> OverloadCheck

This check with the step to the next level at step_db, the readings kept: nothing read from the record depends on the step, so dragging the headroom floor does not read every channel's statistics again (2.2 s for eight channels of a five-minute record, on every drag).

Source code in src/visualdynamics/core/overload.py
def with_step(self, step_db: float) -> OverloadCheck:
    """This check with the step to the next level at `step_db`, the
    readings kept: nothing read from the record depends on the
    step, so dragging the headroom floor does not read every
    channel's statistics again (2.2 s for eight channels of a
    five-minute record, on every drag)."""
    from dataclasses import replace

    step_db = float(step_db)
    return OverloadCheck(tuple(replace(c, step_db=step_db)
                               for c in self.channels),
                         step_db=step_db, margin=self.margin)
with_flag
with_flag(key: str) -> list[ChannelOverload]

The channels flagged for key (an OVERLOADS key).

Source code in src/visualdynamics/core/overload.py
def with_flag(self, key: str) -> list[ChannelOverload]:
    """The channels flagged for `key` (an `OVERLOADS` key)."""
    if key not in OVERLOADS:
        raise KeyError(key)
    return [channel for channel in self.channels
            if key in channel.flagged]
describe
describe() -> str

The judgment in words: which channels are on the rail, which reach their range at the next level, which hold samples beyond it, and how many had no range.

Source code in src/visualdynamics/core/overload.py
def describe(self) -> str:
    """The judgment in words: which channels are on the rail,
    which reach their range at the next level, which hold samples
    beyond it, and how many had no range."""
    step = f'+{self.step_db:g} dB'
    phrases = {'beyond': ('beyond its range', 'beyond their ranges'),
               'rail': ('on the rail', 'on the rail'),
               'next': (f'reaches its range at {step}',
                        f'reach their ranges at {step}')}
    parts = []
    for key, (one, many) in phrases.items():
        found = self.with_flag(key)
        if found:
            names = ', '.join(channel.label for channel in found)
            parts.append(f'{len(found)} {one if len(found) == 1 else many}'
                         f' ({names})')
    checked = len(self.checked)
    if not parts:
        words = (f'All {checked} channel{"s" * (checked != 1)} inside '
                 f'their ranges, at {step} as well.'
                 if checked else 'No channel was read against a range.')
    else:
        words = (f'Of {checked} channel{"s" * (checked != 1)}: '
                 + '; '.join(parts) + '.')
    unread = self.unread
    if unread:
        words += ' Not read: ' + '; '.join(
            f'{channel.label}, {channel.reason}' for channel in unread) + '.'
    missing = len(self.unranged) - len(unread)
    if missing:
        words += (f' {missing} channel{"s" * (missing != 1)} '
                  f'with no range.')
    holes = [channel for channel in self.channels
             if channel.statistics is not None
             and channel.statistics.nonfinite]
    if holes:
        names = ', '.join(f'{channel.label} {channel.statistics.nonfinite}'
                          for channel in holes)
        words += (f' Samples not finite, left out: {names}.')
    return words

Functions:

range_in_unit

range_in_unit(range_volts: float, sensitivity: float) -> float

The range in the channel's own unit: a range in volts over a sensitivity in mV per unit, volts * 1000 / sensitivity.

A 10 V input on a 100 mV/g chain reads to 100 g. NaN when the sensitivity is not a positive number, since there is then no range to speak of.

Source code in src/visualdynamics/core/overload.py
def range_in_unit(range_volts: float, sensitivity: float) -> float:
    """The range in the channel's own unit: a range in volts over a
    sensitivity in mV per unit, ``volts * 1000 / sensitivity``.

    A 10 V input on a 100 mV/g chain reads to 100 g. NaN when the
    sensitivity is not a positive number, since there is then no range
    to speak of.
    """
    sensitivity = float(sensitivity)
    if not (np.isfinite(sensitivity) and sensitivity > 0.0):
        return float('nan')
    return float(range_volts) * 1000.0 / sensitivity

table_full_scales

table_full_scales(table: ChannelTable, history: TimeHistory) -> TableRanges

Each channel's range in SI from a channel table, and why where there is none.

The table's row for a channel is the one that names it (ChannelTable.identities): its DOF and its channel type, or its DOF alone when the type is blank and the history has one channel there. The range is the level the row's channel clips at (ChannelTable.clip_ranges, the smaller of its DAQ Range and its Sensor Range that are stated) in volts over its Sensitivity in mV per the row's unit (range_in_unit), put into SI by the unit's scale. A row that states neither range gives none, and the reason says so. Where two or more rows name one channel, it has a range only when they all give the same one, and a note says which rows; where they differ it has none, and the reason says so.

Parameters:

Name Type Description Default
table ChannelTable

The channel table describing the recording.

required
history TimeHistory

The recording.

required

Returns:

Type Description
TableRanges

Per channel of history: the range in SI, why there is none, and any note.

Source code in src/visualdynamics/core/overload.py
def table_full_scales(table: ChannelTable, history: TimeHistory
                      ) -> TableRanges:
    """Each channel's range in SI from a channel table, and why where
    there is none.

    The table's row for a channel is the one that names it
    (`ChannelTable.identities`): its DOF and its channel type, or its
    DOF alone when the type is blank and the history has one channel
    there. The range is the level the row's channel clips at
    (`ChannelTable.clip_ranges`, the smaller of its DAQ Range and its
    Sensor Range that are stated) in volts over its Sensitivity in mV
    per the row's unit (`range_in_unit`), put into SI by the unit's
    scale. A row that states neither range gives none, and the reason
    says so. Where two or more rows name one channel, it has a range
    only when they all give the same one, and a note says which rows;
    where they differ it has none, and the reason says so.

    Parameters
    ----------
    table : ChannelTable
        The channel table describing the recording.
    history : TimeHistory
        The recording.

    Returns
    -------
    TableRanges
        Per channel of `history`: the range in SI, why there is none,
        and any note.
    """
    rows = table.identities(history)
    ranges = table.clip_ranges()
    sensitivities = table.sensitivities()
    units = [str(unit).strip() for unit in table.frame['unit']]
    count = history.num_records
    scales = np.full(count, np.nan)
    reasons = [''] * count
    notes = [''] * count
    for i in range(count):
        identity = (history.response_dof[i], history.ordinate_dim[i])
        named = [k for k, row in enumerate(rows) if row == identity]
        if not named:
            reasons[i] = 'not in the channel table'
            continue
        read = [_row_range(ranges[k], sensitivities[k], units[k],
                           history.ordinate_dim[i]) for k in named]
        if len(named) > 1:
            # two rows naming one channel: taking the first would be a
            # guess at which one the record was made through
            which = _row_numbers(named)
            values = np.array([value for value, _reason in read])
            finite = values[np.isfinite(values)]
            if finite.size and (finite.size < values.size
                                or np.ptp(finite) > 1e-12 * finite.max()):
                reasons[i] = (f'rows {which} of the channel table name it '
                              'with different ranges')
                continue
            if finite.size:
                notes[i] = (f'rows {which} of the channel table name it, '
                            'with the same range')
        value, reason = read[0]
        if not np.isfinite(value):
            reasons[i] = reason
            continue
        scales[i] = value
    return TableRanges(scales, reasons, notes)

channel_overload

channel_overload(values: ArrayLike, sample_rate: float, full_scale: float | None, *, step_db: float = STEP_DB, margin: float = RAIL_MARGIN, rail_samples: int = RAIL_SAMPLES, label: str = '', record: int = 0, start: float = 0.0, spans: Sequence[tuple[int, int]] | None = None) -> ChannelOverload

Read one record against its range.

Parameters:

Name Type Description Default
values array_like

The record, one channel, evenly sampled.

required
sample_rate float

Samples per second.

required
full_scale float or None

The range either way, in the units of values. None or NaN reads the record with no range: nothing is judged.

required
step_db float

The step to the next level, in dB.

`STEP_DB`
margin float

How close to the range a sample counts as on it, as a share.

`core.time_statistics.RAIL_MARGIN`
rail_samples int

The most samples allowed on the rail.

`core.time_statistics.RAIL_SAMPLES`
label str

What the channel is called in the words.

''
record int

The channel's index, carried for the caller.

0
start float

As core.time_statistics.time_statistics takes them.

0.0
spans float

As core.time_statistics.time_statistics takes them.

0.0

Returns:

Type Description
ChannelOverload

The numbers and the flags.

Raises:

Type Description
ValueError

If the range is not above zero, or as core.time_statistics.time_statistics raises.

Source code in src/visualdynamics/core/overload.py
def channel_overload(values: ArrayLike, sample_rate: float,
                     full_scale: float | None, *,
                     step_db: float = STEP_DB,
                     margin: float = RAIL_MARGIN,
                     rail_samples: int = RAIL_SAMPLES,
                     label: str = '', record: int = 0,
                     start: float = 0.0,
                     spans: Sequence[tuple[int, int]] | None = None
                     ) -> ChannelOverload:
    """Read one record against its range.

    Parameters
    ----------
    values : array_like
        The record, one channel, evenly sampled.
    sample_rate : float
        Samples per second.
    full_scale : float or None
        The range either way, in the units of `values`. None or NaN
        reads the record with no range: nothing is judged.
    step_db : float, default `STEP_DB`
        The step to the next level, in dB.
    margin : float, default `core.time_statistics.RAIL_MARGIN`
        How close to the range a sample counts as on it, as a share.
    rail_samples : int, default `core.time_statistics.RAIL_SAMPLES`
        The most samples allowed on the rail.
    label : str, optional
        What the channel is called in the words.
    record : int, default 0
        The channel's index, carried for the caller.
    start, spans
        As `core.time_statistics.time_statistics` takes them.

    Returns
    -------
    ChannelOverload
        The numbers and the flags.

    Raises
    ------
    ValueError
        If the range is not above zero, or as
        `core.time_statistics.time_statistics` raises.
    """
    scale = (float('nan') if full_scale is None else float(full_scale))
    if np.isfinite(scale) and not scale > 0.0:
        raise ValueError('the range is how far either way the channel '
                         'reads, and has to be above zero')
    data = np.real(np.asarray(values)).astype(float, copy=False).ravel()
    stats = time_statistics(data, sample_rate, start=start, spans=spans,
                            full_scale=scale if np.isfinite(scale) else None,
                            margin=margin, rail_samples=rail_samples)
    if stats.peak == 0.0:
        return ChannelOverload(record=int(record), label=str(label),
                               full_scale=scale, statistics=None,
                               step_db=float(step_db), reason=DEAD)
    return ChannelOverload(record=int(record), label=str(label),
                           full_scale=scale, statistics=stats,
                           step_db=float(step_db),
                           reason='' if np.isfinite(scale) else 'no range given',
                           beyond=_beyond(data, spans, scale, float(margin)))

overload_check

overload_check(history: TimeHistory, ranges: ChannelTable | ArrayLike | float, *, records: Sequence[int] | None = None, step_db: float = STEP_DB, margin: float = RAIL_MARGIN, rail_samples: int = RAIL_SAMPLES) -> OverloadCheck

Read each channel of a time history against its range.

Over the span core.kurtosis.analyzed_span names, as the kurtosis bars and the Statistics reading read it, so a channel's rail count here is that reading's for the same range and margin.

Parameters:

Name Type Description Default
history TimeHistory

The record.

required
ranges (ChannelTable, array_like or float)

Where the ranges come from: the channel table describing the record (table_full_scales reads its clip_ranges), or the ranges themselves in SI, one per channel (NaN or None for a channel with none) or one for every channel. A 25 g range on an acceleration channel is 25 * 9.80665.

required
records sequence of int

Which channels, counting from 0; every channel by default.

None
step_db float

The step to the next level, in dB.

`STEP_DB`
margin float

How close to the range a sample counts as on it, as a share.

`core.time_statistics.RAIL_MARGIN`
rail_samples int

The most samples allowed on the rail.

`core.time_statistics.RAIL_SAMPLES`

Returns:

Type Description
OverloadCheck

Every channel's numbers and flags, and the judgment.

Raises:

Type Description
ValueError

If a channel named is not in the record, none is named, the ranges given are not one per channel, a range is zero or negative, the step is not finite, or the margin is not a share.

Source code in src/visualdynamics/core/overload.py
def overload_check(history: TimeHistory,
                   ranges: ChannelTable | ArrayLike | float, *,
                   records: Sequence[int] | None = None,
                   step_db: float = STEP_DB,
                   margin: float = RAIL_MARGIN,
                   rail_samples: int = RAIL_SAMPLES) -> OverloadCheck:
    """Read each channel of a time history against its range.

    Over the span `core.kurtosis.analyzed_span` names, as the kurtosis
    bars and the Statistics reading read it, so a channel's rail count
    here is that reading's for the same range and margin.

    Parameters
    ----------
    history : TimeHistory
        The record.
    ranges : ChannelTable, array_like or float
        Where the ranges come from: the channel table describing the
        record (`table_full_scales` reads its `clip_ranges`), or the ranges themselves
        in SI, one per channel (NaN or None for a channel with none) or
        one for every channel. A 25 g range on an acceleration channel
        is ``25 * 9.80665``.
    records : sequence of int, optional
        Which channels, counting from 0; every channel by default.
    step_db : float, default `STEP_DB`
        The step to the next level, in dB.
    margin : float, default `core.time_statistics.RAIL_MARGIN`
        How close to the range a sample counts as on it, as a share.
    rail_samples : int, default `core.time_statistics.RAIL_SAMPLES`
        The most samples allowed on the rail.

    Returns
    -------
    OverloadCheck
        Every channel's numbers and flags, and the judgment.

    Raises
    ------
    ValueError
        If a channel named is not in the record, none is named, the
        ranges given are not one per channel, a range is zero or
        negative, the step is not finite, or the margin is not a share.
    """
    count = history.num_records
    if records is not None and not len(records):
        raise ValueError('no channel asked for: name one or more, or pass '
                         'none for every channel')
    if not np.isfinite(float(step_db)):
        raise ValueError('the step to the next level is a number of dB')
    if not 0.0 <= float(margin) < 1.0:
        raise ValueError('the margin is a share of the range, from 0 up '
                         'to 1')
    if isinstance(ranges, ChannelTable):
        scales, reasons, notes = table_full_scales(ranges, history)
    else:
        given = np.asarray(
            [np.nan if value is None else value
             for value in np.atleast_1d(np.asarray(ranges, dtype=object))],
            dtype=float)
        if given.size == 1:
            given = np.full(count, given[0])
        if given.size != count:
            raise ValueError(f'{given.size} ranges for {count} channel'
                             f'{"s" * (count != 1)}: one each, or one for '
                             'all')
        if np.any(np.isfinite(given) & ~(given > 0.0)):
            raise ValueError('a range is how far either way a channel '
                             'reads, and has to be above zero')
        scales = given
        reasons = ['' if np.isfinite(value) else 'no range given'
                   for value in given]
        notes = [''] * count
    wanted = range(count) if records is None else [int(i) for i in records]
    spans, _phrase = analyzed_span(history)
    ordinate = np.asarray(history.ordinate)
    channels = []
    for i in wanted:
        if not 0 <= i < count:
            raise ValueError(f'the record has {count} channel'
                             f'{"s" * (count != 1)}; there is no channel {i}')
        scale = float(scales[i])
        known = np.isfinite(scale)
        try:
            stats = record_statistics(
                history, i, full_scale=scale if known else None,
                margin=margin, rail_samples=rail_samples)
        except ValueError as error:
            # an empty channel: said, and left out of the verdict
            channels.append(ChannelOverload(
                record=i, label=history.record_label(i), full_scale=scale,
                statistics=None, step_db=float(step_db),
                reason=str(error)))
            continue
        if stats.peak == 0.0:
            # zero throughout: infinite headroom would read as a pass
            channels.append(ChannelOverload(
                record=i, label=history.record_label(i), full_scale=scale,
                statistics=None, step_db=float(step_db), reason=DEAD))
            continue
        channels.append(ChannelOverload(
            record=i, label=history.record_label(i), full_scale=scale,
            statistics=stats, step_db=float(step_db), reason=reasons[i],
            beyond=_beyond(np.real(ordinate[i]), spans, scale,
                           float(margin)),
            note=notes[i]))
    return OverloadCheck(channels=tuple(channels), step_db=float(step_db),
                         margin=float(margin))

headroom_rows

headroom_rows(check: OverloadCheck) -> list[tuple[str, float]]

[(label, headroom in dB)] for each channel, NaN where there is no range: the rows the headroom bars draw.

Source code in src/visualdynamics/core/overload.py
def headroom_rows(check: OverloadCheck) -> list[tuple[str, float]]:
    """[(label, headroom in dB)] for each channel, NaN where there is
    no range: the rows the headroom bars draw."""
    return [(channel.label, channel.headroom_db)
            for channel in check.channels]

notes_for

notes_for(check: OverloadCheck) -> dict[int, str]

{row: why} for each channel with no range: the sentence the headroom bars write along its row in place of a bar.

Source code in src/visualdynamics/core/overload.py
def notes_for(check: OverloadCheck) -> dict[int, str]:
    """{row: why} for each channel with no range: the sentence the
    headroom bars write along its row in place of a bar."""
    return {row: (f'not read: {channel.reason}'
                  if channel.statistics is None
                  else f'no range: {channel.reason}')
            for row, channel in enumerate(check.channels)
            if not channel.has_range}