Skip to content

visualdynamics.core.prediction

prediction

Responses against prediction: each response channel read against the response its analyst predicted, scaled to the level run.

A Prediction is one response channel's predicted response, as the analyst gave it: a list of peaks (frequency, level and Q) or a predicted curve (an ASD, an amplitude or a transmissibility) with the frequencies of the modes it is to be compared at; the input level it was predicted at; its source; and the model's stated uncertainty. A Predictions object holds a test's predictions, one per channel, and is saved in the project.

compare reads a measured object against them. The prediction is scaled to the level run: a level of L dB relative to the level it was predicted at multiplies an ASD by 10^(L/10) and an amplitude by 10^(L/20) (scale_factor). A transmissibility is a ratio of response to input, the same at every level for a linear article, and is not scaled. Near each predicted peak, inside its band, the measured peak is read by the package's half-power reader (core.half_power), and each row gives the frequency, level and Q measured and predicted and their ratio, named as an ASD ratio or an amplitude ratio and given in decibels, so the decibels cannot be read two ways.

What a difference means is not the program's to say. The table states the numbers and the ratio beside the uncertainty the model stated; whether the response is as predicted, different and explained, or different and not explained is the analyst's answer.

What cannot be read one way is refused, with the cause named:

  • a prediction of one quantity against a record of another (an ASD against a transmissibility), or in units of another dimension;
  • an ASD or an amplitude whose input level, or whose run level, is not stated (a transmissibility takes neither: a level would do nothing);
  • a peak with no band to look in, from neither its own band nor the model's stated frequency uncertainty;
  • two peaks of one channel whose bands overlap, since one measured peak inside both could be read as either.

A predicted peak whose band holds no measured peak says so: a peak is never matched to one outside its band. A band runs from the predicted frequency less its stated uncertainty to the frequency plus it, unless the analyst states a band for the peak (to compare a mode the model placed far from where it was found, say).

The level the record was run at is taken in one place, level_run: the level stated for the comparison, or the scaling held on a PSD (Psd.scale_db, the decibels it is scaled up by to meet its specification, so a run at -12 dB holds +12), refused when the two disagree.

References
  1. Bendat, J. S., & Piersol, A. G. (2010). Random Data: Analysis and Measurement Procedures, 4th ed. Wiley. The response of a linear system to a stationary random input: the output spectral density is the input spectral density times the squared gain, so it scales with the input's density, and the half-power bandwidth of a resonance.

Classes:

Name Description
PredictedPeak

One predicted peak of a response channel.

Prediction

One response channel's predicted response.

Predictions

A test's predictions, one per response channel (and quantity, and

PeakRow

One predicted peak against its measured counterpart.

ChannelComparison

One prediction read against its record.

PredictionComparison

What compare found.

Functions:

Name Description
scale_factor

What a prediction is multiplied by to read at level_db decibels

level_run

The level measured was run at, in dB relative to full level, and

compare

Read measured against predictions, each scaled to the level

Classes

PredictedPeak dataclass

PredictedPeak(frequency: float, level: float | None = None, q: float | None = None, band: tuple[float, float] | None = None)

One predicted peak of a response channel.

frequency is in Hz. level is the predicted level at the peak, in the prediction's unit (for an ASD, that unit squared per hertz) and at the prediction's input level, and q its predicted Q (None when the model does not state one). A peak of a predicted curve names its frequency only: its level and Q are read off the curve. band, (low, high) in Hz, is where its measured counterpart is looked for; None takes the band from the model's stated frequency uncertainty.

Prediction dataclass

Prediction(channel: str, quantity: str, peaks: tuple[PredictedPeak, ...] = (), unit: str | None = None, level_db: float | None = None, curve: tuple[ndarray, ndarray] | None = None, reference: str | None = None, source: str = '', customer: bool = False, frequency_uncertainty: float | None = None, level_uncertainty: float | None = None)

One response channel's predicted response.

Parameters:

Name Type Description Default
channel str

The response DOF the prediction is for, as the records name it ('201Z+').

required
quantity str

'asd', 'amplitude' or 'transmissibility'.

required
peaks sequence

The predicted peaks: PredictedPeak, (frequency, level[, q]) tuples or mappings. At least one.

()
unit str

The unit of the quantity the levels are in, the way a PSD declares it: 'g' for an ASD in g²/Hz, 'g' for an amplitude in g. Required for an ASD or an amplitude; a transmissibility has none.

None
level_db float

The input level the prediction is stated at, in dB relative to full level (0 for a full-level prediction). Required for an ASD or an amplitude; a transmissibility takes none.

None
curve (frequencies, values)

The predicted curve, in the same unit; its peaks' levels and Q are read off it, so a peak of a curve names its frequency only.

None
reference str

The input DOF of a transmissibility ('1Z+'); required for one, refused for anything else.

None
source str

Where the prediction comes from: the analysis, its document and revision.

''
customer bool

Whether the customer supplied it.

False
frequency_uncertainty float

The model's stated frequency uncertainty, plus or minus, in percent. It sets each peak's band unless the peak states one.

None
level_uncertainty float

The model's stated level uncertainty, plus or minus, in dB.

None

Raises:

Type Description
ValueError

For what cannot be read one way: see the module's notes.

TypeError

When a field is of the wrong type.

Methods:

Name Description
si

Levels in the prediction's unit, in SI (the unit squared per

own

SI levels in the prediction's unit: the inverse of si.

as_record

The prediction as plain values (the curve left out), what a

from_record

A prediction from as_record's values and its curve.

describe

The prediction in one line, in its own unit: '201Z+, ASD in

Attributes:

Name Type Description
dimension str | None

The dimension of the quantity the levels are in

record_dimension str | None

The ordinate_dim a record compared with this prediction

unit_label str

The unit the levels are written in, as text: 'g²/Hz', 'g',

key tuple[str, str, str | None]

What makes two predictions the same one: the channel, the

Attributes
dimension property
dimension: str | None

The dimension of the quantity the levels are in ('acceleration'), or None for a transmissibility or a unit that cannot be read.

record_dimension property
record_dimension: str | None

The ordinate_dim a record compared with this prediction has: 'acceleration**2/frequency' for an ASD of acceleration, 'acceleration' for an amplitude, None for a transmissibility (any quantity over itself).

unit_label property
unit_label: str

The unit the levels are written in, as text: 'g²/Hz', 'g', or '' for a transmissibility.

key property
key: tuple[str, str, str | None]

What makes two predictions the same one: the channel, the quantity and, for a transmissibility, the input DOF.

Methods:
si
si(values: Any) -> Any

Levels in the prediction's unit, in SI (the unit squared per hertz for an ASD; a transmissibility unchanged).

Source code in src/visualdynamics/core/prediction.py
def si(self, values: Any) -> Any:
    """Levels in the prediction's unit, in SI (the unit squared per
    hertz for an ASD; a transmissibility unchanged)."""
    if self.quantity == 'transmissibility':
        return values
    from ..units import si_factor

    factor = si_factor(str(self.unit), self.dimension)
    return values * factor ** (2 if self.quantity == 'asd' else 1)
own
own(values: Any) -> Any

SI levels in the prediction's unit: the inverse of si.

Source code in src/visualdynamics/core/prediction.py
def own(self, values: Any) -> Any:
    """SI levels in the prediction's unit: the inverse of `si`."""
    return values / self.si(1.0)
as_record
as_record() -> dict[str, Any]

The prediction as plain values (the curve left out), what a file stores; from_record reads it back. A field that is not stated is None, and a peak is [frequency, level, Q, band].

Source code in src/visualdynamics/core/prediction.py
def as_record(self) -> dict[str, Any]:
    """The prediction as plain values (the curve left out), what a
    file stores; `from_record` reads it back. A field that is not
    stated is None, and a peak is [frequency, level, Q, band]."""
    return {'channel': self.channel, 'quantity': self.quantity,
            'unit': self.unit, 'level_db': self.level_db,
            'reference': self.reference, 'source': self.source,
            'customer': self.customer,
            'frequency_uncertainty': self.frequency_uncertainty,
            'level_uncertainty': self.level_uncertainty,
            'peaks': [[peak.frequency, peak.level, peak.q,
                       None if peak.band is None else list(peak.band)]
                      for peak in self.peaks]}
from_record classmethod
from_record(record: Mapping[str, Any], curve: tuple[Any, Any] | None = None) -> Prediction

A prediction from as_record's values and its curve.

Source code in src/visualdynamics/core/prediction.py
@classmethod
def from_record(cls, record: Mapping[str, Any],
                curve: tuple[Any, Any] | None = None) -> Prediction:
    """A prediction from `as_record`'s values and its curve."""
    fields = dict(record)
    peaks = [PredictedPeak(frequency, level, q,
                           None if band is None else tuple(band))
             for frequency, level, q, band in fields.pop('peaks')]
    return cls(peaks=peaks, curve=curve, **fields)
describe
describe() -> str

The prediction in one line, in its own unit: '201Z+, ASD in g²/Hz at 0 dB: 385 Hz 0.5 (Q 12); from the stress analysis (customer-supplied)'.

Source code in src/visualdynamics/core/prediction.py
def describe(self) -> str:
    """The prediction in one line, in its own unit: '201Z+, ASD in
    g²/Hz at 0 dB: 385 Hz 0.5 (Q 12); from the stress analysis
    (customer-supplied)'."""
    unit = f' in {self.unit_label}' if self.unit_label else ''
    level = (f' at {self.level_db:+g} dB' if self.level_db is not None
             else f' from {self.reference}')
    noun = 'ASD' if self.quantity == 'asd' else self.quantity
    peaks = []
    for peak in self.peaks:
        text = f'{_figures(peak.frequency, 4)} Hz'
        if peak.level is not None:
            text += f' {_figures(peak.level, 3)}'
        if peak.q is not None:
            text += f' (Q {_figures(peak.q, 3)})'
        peaks.append(text)
    tail = ''
    if self.source:
        tail = f'; from {self.source}'
    if self.customer:
        tail += ' (customer-supplied)'
    return (f'{self.channel}, {noun}{unit}{level}: '
            f'{", ".join(peaks)}{tail}')

Predictions

Predictions(predictions: Iterable[Prediction] = (), comment: str = '')

A test's predictions, one per response channel (and quantity, and for a transmissibility input DOF).

Parameters:

Name Type Description Default
predictions iterable of Prediction

The predictions.

()
comment str

What the set is: the axis or the run setup it predicts.

''

Raises:

Type Description
ValueError

When two predictions are for the same channel and quantity.

Methods:

Name Description
put

Add prediction, replacing the one for the same channel and

compare

compare against this set.

Attributes:

Name Type Description
channels list[str]

The channels predicted, in order.

Source code in src/visualdynamics/core/prediction.py
def __init__(self, predictions: Iterable[Prediction] = (),
             comment: str = '') -> None:
    self.predictions: list[Prediction] = []
    self.comment: str = str(comment)
    for prediction in predictions:
        if not isinstance(prediction, Prediction):
            raise TypeError(f'a Predictions object holds Prediction, '
                            f'not {type(prediction).__name__}')
        if any(held.key == prediction.key for held in self.predictions):
            raise ValueError(f'two predictions for {prediction.channel} '
                             f'as {NOUN[prediction.quantity]}; one each')
        self.predictions.append(prediction)
Attributes
channels property
channels: list[str]

The channels predicted, in order.

Methods:
put
put(prediction: Prediction) -> None

Add prediction, replacing the one for the same channel and quantity (and input DOF) in its place.

Source code in src/visualdynamics/core/prediction.py
def put(self, prediction: Prediction) -> None:
    """Add `prediction`, replacing the one for the same channel and
    quantity (and input DOF) in its place."""
    if not isinstance(prediction, Prediction):
        raise TypeError(f'a Predictions object holds Prediction, not '
                        f'{type(prediction).__name__}')
    for k, held in enumerate(self.predictions):
        if held.key == prediction.key:
            self.predictions[k] = prediction
            return
    self.predictions.append(prediction)
compare
compare(measured: Any, level_db: float | None = None) -> PredictionComparison

compare against this set.

Source code in src/visualdynamics/core/prediction.py
def compare(self, measured: Any, level_db: float | None = None
            ) -> PredictionComparison:
    """`compare` against this set."""
    return compare(self, measured, level_db)

PeakRow dataclass

PeakRow(quantity: str, predicted_frequency: float, predicted_level: float, predicted_q: float | None, band: tuple[float, float], measured_frequency: float | None = None, measured_level: float | None = None, measured_q: float | None = None, missing: str = '', notes: tuple[str, ...] = ())

One predicted peak against its measured counterpart.

Levels are in SI, the predicted one scaled to the level run. The measured fields are None when the band holds no measured peak, and missing says why. notes are the half-power reader's, said where the reading is read (too few lines across the peak, a crossing outside the record).

Attributes:

Name Type Description
ratio float | None

The measured level over the predicted one: an ASD ratio for

ratio_db float | None

ratio in decibels: 10 log10 of an ASD ratio, 20 log10 of an

frequency_difference float | None

The measured frequency's difference from the predicted, in

Attributes
ratio property
ratio: float | None

The measured level over the predicted one: an ASD ratio for an ASD, an amplitude ratio otherwise (RATIO).

ratio_db property
ratio_db: float | None

ratio in decibels: 10 log10 of an ASD ratio, 20 log10 of an amplitude ratio, so either reads as the same decibels. None when the ratio is zero or missing.

frequency_difference property
frequency_difference: float | None

The measured frequency's difference from the predicted, in percent of the predicted.

ChannelComparison dataclass

ChannelComparison(prediction: Prediction, record: int | None, label: str, factor: float, rows: tuple[PeakRow, ...] = (), note: str = '')

One prediction read against its record.

record is the record's index in the measured object, None when the object has no record of the channel. factor is what the prediction was multiplied by for the level run. note says why the channel was not compared (rows then empty).

PredictionComparison dataclass

PredictionComparison(quantity: str, level_db: float | None, level_source: str, channels: tuple[ChannelComparison, ...], absent: tuple[str, ...] = ())

What compare found.

quantity is what the measured object holds; level_db the level it was run at (None for a transmissibility) and level_source where that was read (level_run). channels are the predictions with a record in the object, in the set's order; absent the channels predicted that the object has no record of.

Methods:

Name Description
lines

The comparison as text, one line per row and per channel not

Attributes:

Name Type Description
ratio_name str

'ASD ratio' or 'amplitude ratio': what every row's ratio is.

rows list[tuple[ChannelComparison, PeakRow]]

Every row with its channel, in order.

scaling str

How the predictions were scaled, in words: 'scaled by -12 dB

Attributes
ratio_name property
ratio_name: str

'ASD ratio' or 'amplitude ratio': what every row's ratio is.

rows property
rows: list[tuple[ChannelComparison, PeakRow]]

Every row with its channel, in order.

scaling property
scaling: str

How the predictions were scaled, in words: 'scaled by -12 dB on the ASD, 10^(L/10)', or that a transmissibility is not.

Methods:
lines
lines() -> list[str]

The comparison as text, one line per row and per channel not compared, levels in each prediction's own unit. Nothing judged says so.

Source code in src/visualdynamics/core/prediction.py
def lines(self) -> list[str]:
    """The comparison as text, one line per row and per channel not
    compared, levels in each prediction's own unit. Nothing judged
    says so."""
    out = []
    for channel in self.channels:
        prediction = channel.prediction
        if channel.note:
            out.append(f'{channel.label}: not compared, {channel.note}')
            continue
        unit = (f' {prediction.unit_label}' if prediction.unit_label
                else '')
        for row in channel.rows:
            head = (f'{channel.label}: predicted '
                    f'{_figures(row.predicted_frequency, 4)} Hz, '
                    f'{_figures(prediction.own(row.predicted_level), 3)}'
                    f'{unit}')
            if row.measured_frequency is None:
                out.append(f'{head}; {row.missing}')
                continue
            assert row.measured_level is not None
            ratio, decibels = row.ratio, row.ratio_db
            assert ratio is not None
            said = (f'{decibels:+.1f} dB' if decibels is not None
                    else 'no decibels')
            out.append(
                f'{head}; measured '
                f'{_figures(row.measured_frequency, 4)} Hz, '
                f'{_figures(prediction.own(row.measured_level), 3)}'
                f'{unit}; {self.ratio_name} {_figures(ratio, 3)}, '
                f'{said}')
    if self.absent:
        out.append(f'No record of {", ".join(self.absent)}.')
    if not self.rows:
        out.append('Nothing was compared.')
    return out

Functions:

scale_factor

scale_factor(quantity: str, level_db: float) -> float

What a prediction is multiplied by to read at level_db decibels relative to the level it was predicted at.

10^(L/10) on an ASD, a power; 10^(L/20) on an amplitude. A transmissibility is a ratio of response to input and is not scaled, so a level for one is refused.

Raises:

Type Description
ValueError

For an unknown quantity, a level that is not a finite number, or any level for a transmissibility.

Source code in src/visualdynamics/core/prediction.py
def scale_factor(quantity: str, level_db: float) -> float:
    """What a prediction is multiplied by to read at `level_db` decibels
    relative to the level it was predicted at.

    10^(L/10) on an ASD, a power; 10^(L/20) on an amplitude. A
    transmissibility is a ratio of response to input and is not scaled,
    so a level for one is refused.

    Raises
    ------
    ValueError
        For an unknown quantity, a level that is not a finite number,
        or any level for a transmissibility.
    """
    _quantity(quantity)
    if quantity == 'transmissibility':
        raise ValueError('a transmissibility is a ratio of response to '
                         'input and is not scaled by a level')
    level = _finite(level_db, 'the level')
    return 10.0 ** (level / DECIBELS[quantity])

level_run

level_run(measured: Any, stated: float | None = None) -> tuple[float | None, str]

The level measured was run at, in dB relative to full level, and where it was read: 'stated' or 'held on the record'.

The level stated for the comparison when one is; otherwise the scaling a PSD holds for its comparison against its specification (Psd.scale_db: a run at -12 dB is scaled up by 12, so it holds +12); otherwise (None, ''). This is the one place a comparison takes its level from, so a later source of run levels is read here.

Raises:

Type Description
ValueError

When the level stated and the scaling held on the record disagree, or the level is not a finite number.

Source code in src/visualdynamics/core/prediction.py
def level_run(measured: Any, stated: float | None = None
              ) -> tuple[float | None, str]:
    """The level `measured` was run at, in dB relative to full level, and
    where it was read: 'stated' or 'held on the record'.

    The level `stated` for the comparison when one is; otherwise the
    scaling a PSD holds for its comparison against its specification
    (`Psd.scale_db`: a run at -12 dB is scaled up by 12, so it holds
    +12); otherwise (None, ''). This is the one place a comparison takes
    its level from, so a later source of run levels is read here.

    Raises
    ------
    ValueError
        When the level stated and the scaling held on the record
        disagree, or the level is not a finite number.
    """
    from .data import Psd

    held = getattr(measured, 'scale_db', None) if isinstance(
        measured, Psd) else None
    if stated is not None:
        stated = _finite(stated, 'the level run')
        if held is not None and not math.isclose(-float(held), stated,
                                                 abs_tol=1e-9):
            raise ValueError(
                f'the level stated, {stated:+g} dB, disagrees with the '
                f'scaling held on the record for its comparison against '
                f'the specification, {float(held):+g} dB (a run at '
                f'{-float(held):+g} dB)')
        return stated, 'stated'
    if held is not None:
        return -float(held), 'held on the record'
    return None, ''

compare

compare(predictions: Predictions | Sequence[Prediction], measured: Any, level_db: float | None = None) -> PredictionComparison

Read measured against predictions, each scaled to the level run.

Each prediction of the quantity measured holds is paired with the record of its channel (its response DOF, and for a transmissibility its input DOF); near each predicted peak, inside the peak's band, the measured peak is read by the half-power reader. A channel that cannot be read one way is not compared and says why: a prediction of another quantity, a record in units of another dimension or with its units undefined, several records of the channel, or a record that is zero, constant or empty.

Parameters:

Name Type Description Default
predictions Predictions or sequence of Prediction

What was predicted.

required
measured (Psd, Spectrum or Frf)

The response: a narrowband PSD for ASD predictions, an amplitude spectrum or a sine level for amplitude ones, an FRF for a transmissibility.

required
level_db float

The level the record was run at, dB relative to full level. When None, the scaling a PSD holds is used (level_run). Required for an ASD or an amplitude; refused for a transmissibility.

None

Returns:

Type Description
PredictionComparison

Raises:

Type Description
ValueError

For an empty set or record, an octave-band PSD, a run level not stated or one given for a transmissibility, or a stated level that disagrees with the record's held scaling.

TypeError

For a measured object that is not a measured response (a specification, a time history), or a prediction that is not a Prediction.

Source code in src/visualdynamics/core/prediction.py
def compare(predictions: Predictions | Sequence[Prediction], measured: Any,
            level_db: float | None = None) -> PredictionComparison:
    """Read `measured` against `predictions`, each scaled to the level
    run.

    Each prediction of the quantity `measured` holds is paired with the
    record of its channel (its response DOF, and for a transmissibility
    its input DOF); near each predicted peak, inside the peak's band,
    the measured peak is read by the half-power reader. A channel that
    cannot be read one way is not compared and says why: a prediction
    of another quantity, a record in units of another dimension or with
    its units undefined, several records of the channel, or a record
    that is zero, constant or empty.

    Parameters
    ----------
    predictions : Predictions or sequence of Prediction
        What was predicted.
    measured : Psd, Spectrum or Frf
        The response: a narrowband PSD for ASD predictions, an amplitude
        spectrum or a sine level for amplitude ones, an FRF for a
        transmissibility.
    level_db : float, optional
        The level the record was run at, dB relative to full level. When
        None, the scaling a PSD holds is used (`level_run`). Required
        for an ASD or an amplitude; refused for a transmissibility.

    Returns
    -------
    PredictionComparison

    Raises
    ------
    ValueError
        For an empty set or record, an octave-band PSD, a run level not
        stated or one given for a transmissibility, or a stated level
        that disagrees with the record's held scaling.
    TypeError
        For a measured object that is not a measured response (a
        specification, a time history), or a prediction that is not a
        `Prediction`.
    """
    held = list(predictions)
    if not held:
        raise ValueError('there are no predictions to compare against')
    for prediction in held:
        if not isinstance(prediction, Prediction):
            raise TypeError(f'a prediction is a Prediction, not '
                            f'{type(prediction).__name__}')
    quantity = _measured_quantity(measured)
    if not measured.num_records:
        raise ValueError('the measured object holds no records')
    level, source = level_run(measured, level_db)
    if quantity == 'transmissibility':
        if level is not None:
            raise ValueError('a transmissibility is a ratio of response to '
                             'input and is not scaled; leave the level '
                             'unstated')
    elif level is None:
        raise ValueError(f'the level the record was run at is not stated; '
                         f'{NOUN[quantity]} prediction is scaled to it (dB '
                         'relative to full level, 0 for a full-level run)')
    frequencies = np.asarray(measured.abscissa, dtype=np.float64)
    channels: list[ChannelComparison] = []
    absent: list[str] = []
    for prediction in held:
        at_dof = [i for i in range(measured.num_records)
                  if measured.response_dof[i] == prediction.channel
                  and _same_input(measured, i, prediction, quantity)]
        # the package's channel identity is (DOF, quantity): a drive
        # point's accelerometer and force gauge share a DOF and are two
        # records, and the prediction's quantity picks one. Matched by
        # DOF alone, every drive point was refused as two records
        # (2026-10-28). With none of the quantity, the refusal below
        # still names the dimension that was there.
        records = [i for i in at_dof
                   if _holds_quantity(measured, i, prediction, quantity)
                   ] or at_dof
        if not records:
            absent.append(prediction.channel if prediction.reference is None
                          else f'{prediction.channel}/{prediction.reference}')
            continue
        label = measured.record_label(records[0])
        note = _refusal(prediction, quantity, measured, records)
        if note:
            # not compared, so not scaled
            channels.append(ChannelComparison(prediction, records[0], label,
                                              1.0, note=note))
            continue
        record = records[0]
        factor = 1.0
        if quantity != 'transmissibility':
            assert level is not None and prediction.level_db is not None
            factor = scale_factor(quantity, level - prediction.level_db)
        magnitude = _amplitude(np.asarray(measured.ordinate[record]),
                               quantity)
        below = getattr(measured, 'below_floor', None)
        if below is not None:
            # a sine level reported at its noise floor is not a reading
            magnitude = np.where(np.asarray(below)[record], np.nan,
                                 magnitude)
        rows = tuple(_row(prediction, peak, band, factor, frequencies,
                          magnitude)
                     for peak, band in zip(prediction.peaks,
                                           prediction.bands, strict=True))
        channels.append(ChannelComparison(prediction, record, label, factor,
                                          rows))
    return PredictionComparison(quantity, level, source, tuple(channels),
                                tuple(absent))