visualdynamics.core.sine_distortion¶
sine_distortion
¶
The sine distortion check: what is on a channel besides the tone.
A sine controller reads its control channel through a narrow band that
follows the drive (core.sine_tracking), so the level it holds is the
fundamental and nothing else. Whatever else the channel carries is
outside the band: harmonics from a rattle, a loose fixture bolt, a joint
that claps or an amplifier at its limit, a fixture resonance excited at a
multiple of the drive, noise. The controller does not see it, and the
article feels it. The check reads it directly. The tones the extraction
tracked are rebuilt on the record's own samples and taken away from it,
and what is left is everything that is not a tone. With r the RMS of
that residual and x_1 the RMS of the rebuilt tones, both over the same
span of the record, the distortion is
D = r / x_1,
reported as a fraction (a percentage on screen) and in dB, 20 log10 D.
The tolerance is usually written on a ratio rather than on D: the
unfiltered RMS no more than some share above the filtered one. Where
the residual is orthogonal to the tones their mean squares add, so the
ratio is sqrt(1 + D2) and D = sqrt(ratio2 - 1) (distortion). Five
percent is an example of what test documents ask for, and it is the
default here (DISTORTION_EXCESS). Five percent over is a distortion of
sqrt(1.05**2 - 1) = 0.320, 32 % or -9.9 dB (distortion_limit), which is
the line the check draws and the figure a line is flagged above.
The residual is read directly because D taken from two readings, the unfiltered RMS over the tone's, rests on two things that need not hold. The tones must be orthogonal to each other for their powers to add, and two tones sweeping through each other are not orthogonal where they cross. And the two readings must cover the same stretch of the record: a level read at an instant and an RMS read over a span differ wherever the level moves inside the span, as it does through a sharp resonance. The residual is read over the same samples as the tones it is measured against, and two tones that beat or cross are rebuilt together and subtracted together, so neither assumption is made.
The check is not sensitive, and it is not meant to be. A tone with a third harmonic at 10 % of it reads a ratio of 1.005, at 20 % 1.020, and only at 32 % does it reach 1.05: it catches distortion large enough to change the test. It hardly sees clipping at all. A sine clipped flat at four fifths of its peak reads 1.004, because cutting the tops of a sine puts most of the lost power back into the fundamental, so flat tops are found in the time history and its crest factor, not here.
The tones are rebuilt by the estimator the extraction uses
(core.sine.vold_kalman): every tone the levels hold that plays over
the checked tone's stretch, solved jointly on the record's own samples
with the extraction's smoothing, each tone's complex envelope on its
own sweep argument. The envelope follows the level as it moves, a
resonance included, so the rebuilt tone is the tone the controller's
band would pass, sample by sample. Where the extraction corrected the
sweep clock (SineLevel.drift_hz not zero) the rebuild fits the same
kind of correction to its own envelope's phase and solves again, so a
drifted sweep is not rebuilt off its tone. The residual and the tones
are then read over cycles cycles of the tone's phase centered on each
line's instant (the extraction's smoothing by default), moved inward
where a span would run off either end of the tone's stretch.
The record's mean over the tone's stretch is taken out first: a sensor's offset is not motion the article feels. Anything slower than the span that is not constant is counted, as it should be, since it is not the tone either. Content up to the Nyquist frequency is counted in full; above it the record holds nothing, so a sweep whose third harmonic passes the Nyquist frequency says so in the notes.
A line whose span reaches a tone of the specification the levels do not
hold cannot be judged, and neither can a line the extraction found under
its noise floor (SineLevel.below_floor), nor a line with no tone on
the channel to judge against (a dead channel), nor any line of a channel
whose stretch holds a sample that is not a finite number. All are kept,
counted in the notes and left out of the verdict
(DistortionCheck.judged), and a channel with no judged line does not
pass.
References
The definition of the distortion and the estimator of the tone it is read against are set out in these.
- Department of Defense (2019). MIL-STD-810H, Environmental Engineering Considerations and Laboratory Tests. Method 514.8, Table 514.8-III, note 3: the distortion check on a sinusoidal test, the unfiltered RMS within 5 percent of the filtered RMS, stated as about 32 percent distortion.
- Brandt, A. (2011). Noise and Vibration Analysis: Signal Analysis and Experimental Procedures. Wiley. Order tracking, the Vold-Kalman filter among it: a component on a known frequency history read as a slowly varying amplitude by least squares.
- Bendat, J. S., & Piersol, A. G. (2010). Random Data: Analysis and Measurement Procedures, 4th ed. Wiley. The mean square of a sum of orthogonal components as the sum of their mean squares.
Classes:
| Name | Description |
|---|---|
DistortionCheck |
One tone's distortion along its sweep, on each channel read: what |
Functions:
| Name | Description |
|---|---|
distortion |
The distortion from two RMS readings, where what is not the tone |
distortion_limit |
The distortion a tolerance on the ratio allows: sqrt((1 + |
decibels |
A distortion in dB, 20 log10 D; minus infinity where it is zero. |
check_distortion |
Check one tone's distortion along its sweep: the RMS of the record |
Classes¶
DistortionCheck
dataclass
¶
DistortionCheck(*, tone: str, dofs: list[str], dimensions: list[str], units: list[str | None], frequency: ndarray, seconds: ndarray, unfiltered: ndarray, fundamental: ndarray, residual: ndarray, judged: ndarray, excess: float, cycles: float, notes: tuple[str, ...] = ())
One tone's distortion along its sweep, on each channel read: what
check_distortion returns.
frequency and seconds are the extraction's lines, ascending in
frequency, each with the second it was read, from the recording's
first sample. unfiltered, fundamental and residual are channels
x lines of RMS in SI over cycles cycles about each line: the
channel itself, the tones rebuilt on it, and the channel less the
tones. judged is channels x lines, False where a line cannot be
judged (the tone under its noise floor, a tone the levels do not
hold live beside it, no tone on the channel, or a sample that is not
a finite number). excess is the tolerance on the ratio. notes
says what was not judged and why, empty when every line was. Two of
these compare equal only if they are the same object, since the
arrays have no single truth value.
Methods:
| Name | Description |
|---|---|
row |
The row of channel |
worst |
The largest judged distortion and the frequency it is at, on |
describe |
The verdict in one line, such as 'Up at 101Z+: worst 12.3 % |
Attributes:
| Name | Type | Description |
|---|---|---|
distortion |
ndarray
|
Channels x lines: D as a fraction, the residual's RMS over the |
ratio |
ndarray
|
Channels x lines: the ratio the distortion stands for, |
decibels |
ndarray
|
Channels x lines: D in dB ( |
limit |
float
|
The distortion the tolerance allows, as a fraction |
over |
ndarray
|
Channels x lines, True where a judged line's distortion is |
passed |
bool
|
Whether every channel read has a judged line and no judged |
Attributes¶
distortion
property
¶
Channels x lines: D as a fraction, the residual's RMS over the rebuilt tones'; not a number where there is no tone.
ratio
property
¶
Channels x lines: the ratio the distortion stands for, sqrt(1 + D**2), the unfiltered RMS over the tone's where the rest is orthogonal to the tones; the number a tolerance like 'within 5 %' is written on.
limit
property
¶
The distortion the tolerance allows, as a fraction
(distortion_limit).
over
property
¶
Channels x lines, True where a judged line's distortion is above the limit.
passed
property
¶
Whether every channel read has a judged line and no judged line on any channel is over the limit. A channel with nothing judged (dead, not a number, or every line unjudged) has not passed: there is no reading to pass.
Methods:¶
row
¶
The row of channel dof; the first when None. Raises
ValueError when the check did not read it.
Source code in src/visualdynamics/core/sine_distortion.py
worst
¶
The largest judged distortion and the frequency it is at, on
channel dof or, when None, on any channel: (Hz, D as a
fraction), or None when no line was judged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dof
|
str
|
The channel; every channel read when omitted. |
None
|
Returns:
| Type | Description |
|---|---|
tuple of float, or None
|
(frequency in Hz, distortion as a fraction). |
Source code in src/visualdynamics/core/sine_distortion.py
describe
¶
The verdict in one line, such as 'Up at 101Z+: worst 12.3 %
(-18.2 dB) at 412 Hz, within the 32.0 % limit'. Every channel's
worst when dof is None, naming any channel with no line
judged.
Source code in src/visualdynamics/core/sine_distortion.py
Functions:¶
distortion
¶
The distortion from two RMS readings, where what is not the tone is orthogonal to it: sqrt((unfiltered / fundamental)**2 - 1), the RMS of everything that is not the tone over the RMS of the tone.
This is the conversion between a tolerance written on the ratio and
a distortion. The check itself reads the residual directly
(check_distortion), since two tones that cross are not orthogonal
and two readings over different stretches are not comparable.
A ratio under one, which only the scatter of the two readings gives (the unfiltered record holds the tone and more), reads as no distortion rather than as the root of a negative number. A fundamental of zero reads as not a number.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
unfiltered
|
float or array - like
|
The channel's RMS, unfiltered. |
required |
fundamental
|
float or array - like
|
The tone's RMS, in the same units. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
D as a fraction (0.32 is 32 %), the shape the two broadcast to. |
Source code in src/visualdynamics/core/sine_distortion.py
distortion_limit
¶
The distortion a tolerance on the ratio allows: sqrt((1 + excess)**2 - 1), 0.320 for 5 %.
Raises ValueError unless excess is more than zero.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
excess
|
float
|
The share the unfiltered RMS may stand above the fundamental's. |
`DISTORTION_EXCESS`
|
Returns:
| Type | Description |
|---|---|
float
|
D as a fraction. |
Source code in src/visualdynamics/core/sine_distortion.py
decibels
¶
A distortion in dB, 20 log10 D; minus infinity where it is zero.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
float or array - like
|
D as a fraction. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
dB, the shape of |
Source code in src/visualdynamics/core/sine_distortion.py
check_distortion
¶
check_distortion(history: Any, specification: SineSweepSpecification, levels: SineLevelSet | None = None, *, tone: str | None = None, channels: Sequence[str] | None = None, excess: float = DISTORTION_EXCESS, cycles: float | None = None) -> DistortionCheck
Check one tone's distortion along its sweep: the RMS of the record less the tracked tones against the RMS of the tones, at every line of the extraction.
The tones come from levels, a SineLevelSet that extract_sine
(Extract Sine Levels) read from this recording against this
specification; with none, the extraction is run here with the
recording's own sine_extraction setting (or its suggestion), as the
project verb runs it. Over the checked tone's stretch, every tone the
levels hold is rebuilt on each channel's samples by the joint
Vold-Kalman solve with the extraction's smoothing, the record's mean
taken out, and the rebuilt tones subtracted. At each line the residual
and the rebuilt tones are read over cycles cycles of the tone
centered on the line's instant, and the distortion is the one RMS
over the other. A line is flagged where it is above the limit
excess sets (distortion_limit).
Raises ValueError when excess or cycles is not more than zero, when
the recording is not evenly sampled, when the levels hold no such tone
or channel, when the recording carries no record of a channel in the
quantity its levels were read in, when the levels do not say when
each line was read or were read at times the recording does not
hold, or when the recording holds less of the
tone than one span.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
history
|
TimeHistory
|
The recording, evenly sampled. |
required |
specification
|
SineSweepSpecification
|
The sweep the drive followed. |
required |
levels
|
SineLevelSet
|
The extracted levels; extracted here when omitted. |
None
|
tone
|
str
|
Which tone, by name; the first the levels hold by default. |
None
|
channels
|
sequence of str
|
Which control channels; every channel the levels hold by default. |
None
|
excess
|
float
|
The share the unfiltered RMS may stand above the fundamental's; 5 %, a distortion of 32 %, by default. |
`DISTORTION_EXCESS`
|
cycles
|
float
|
The span the RMS readings are taken over, in cycles of the tone; the extraction's smoothing by default. |
None
|
Returns:
| Type | Description |
|---|---|
DistortionCheck
|
The three RMS readings at every line on every channel read, which lines could be judged, and the tolerance. |
Source code in src/visualdynamics/core/sine_distortion.py
641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 | |