visualdynamics.core.sine_tracking¶
sine_tracking
¶
A swept sine read the way a controller's tracking filter reads it.
core.sine.extract_sine gives the best estimate of a tone's level:
every tone solved jointly, debiased, with its smoothing chosen from the
data. That is the right value for a report, but it does not answer a
question often asked on a test floor, why the controller's level differs
from the extracted one, because the controller did not read the level
that way. A controller reads the response through a tracking filter, a
band-pass that follows the drive frequency, with a fixed width in Hz or a
width proportional to the drive frequency. A detector then turns the
filter's output into a level: the peak, the RMS or the mean absolute
value (the last two reported as the peak of the sine they correspond
to), or the filter's own output. This module does the same, so the
readings can be plotted side by side and the difference seen directly.
A harmonic or broadband noise changes the level by an amount that depends on the detector and on the filter. Read unfiltered, a peak detector includes the harmonic's peaks and an RMS detector includes its power; read through the filter, neither includes it. The cost of the filter is time: it settles in roughly one over its bandwidth, so a narrow band lags a change in level that a wide one follows. Which reading a controller used is a setting, and the same response record gives a different level curve for each.
The record is demodulated: multiplied by the complex exponential of the drive's own phase, which moves the drive frequency to zero. A low-pass with its corner at half the bandwidth is then the band-pass about the drive, and its magnitude is the tone's amplitude. A band-pass run on the raw record would have to be centered again at every frequency of the sweep; the low-pass only changes its corner.
The filter is causal. core.filters.filtered runs forward and backward
for zero phase, which is right for a record and wrong here: the reading
is meant to show the lag a controller's filter has, and a zero-phase
pass would spread it to both sides of a change and hide it. The filter
runs forward only, starting settled on the tone's first cycle, the way a
controller's filter has settled during the ramp-up before the sweep
begins.
One filter is integrated through every sample. A proportional band's
corner moves with the drive, so no fixed digital design fits it. The
filter is the Butterworth low-pass from scipy's analog prototype (the
one core.filters.design maps to a digital filter through butter),
split into its first-order modes, and each mode is integrated exactly
across each sample at that sample's corner, with the input held over
the sample. Nothing is redesigned along the sweep. A constant input
stays constant to rounding whatever the corner does, because a mode's
settled state does not depend on its corner, and the demodulation's
image (below) is attenuated by the filter's own response, as measured.
A fixed band is the same code with a corner that does not move. Against
design's bilinear filter at a fixed corner well below Nyquist, the
output differs by 0.3 % RMS on white noise (the held input against the
bilinear map), which is too small to change a level.
The order is four by default because of that image. Demodulating a real record moves the tone to zero and also leaves its mirror image at twice the drive frequency, which the low-pass attenuates by its response there, about (bandwidth / (4 x drive)) to the power of the order. What gets through is a ripple at twice the drive frequency. At second order it is 1.6 % at 50 % proportional and 6.2 % at 100 %, an error from the demodulation and not part of the reading; at fourth order it is 0.02 % and 0.4 %. A wide fixed band at the bottom of a sweep, with half the bandwidth near or above the drive frequency, is no longer a band about the drive, and there the reading shows the ripple.
A peak is the peak of the samples, as a sampled detector reads it. It
under-reads a sine by at most a factor of cos(pi x drive / sample rate),
0.3 % at a fortieth of the sample rate. RMS and mean are read over a
span of the drive's cycles (one, DETECTOR_CYCLES, by default). Over a
whole number of cycles a pure sine's RMS times the square root of two,
and its mean absolute value times pi/2, equal its peak exactly.
That is exact only if the span really is a whole number of cycles and
the waveform between the samples is included. Read from the samples
alone, neither holds when a cycle is only a few samples long: the span
is rounded to whole samples, and |x| has a corner at every zero crossing
that a few samples cannot locate. On a clean 2 m/s**2 tone sampled at
4096 Hz, over one cycle, RMS read 3.6 % off at 400 Hz (10 samples a
cycle) and mean 9 % off at 750 Hz (5.5 samples a cycle). So both are
read from the band-limited waveform: the samples around each span are
upsampled until a cycle has FINE_POINTS points
(scipy.signal.resample_poly) and averaged over exactly the span's
phase, with its first point interpolated at the span's start. Measured
the same way on a clean sweep to 780 Hz, RMS is within 0.005 % and mean
within 0.08 % sampled at 4096 Hz (5.3 samples a cycle at the top), and
both are within 0.09 % at 2048 Hz (2.6 samples a cycle). The
reconstruction uses MARGIN samples on either side of a span, so a
reading looks that far past its instant; that is interpolation, not a
lag. The peak is still the peak of the samples, because that is how a
sampled controller reads it.
The waveform, the band's shape and its weighting all come from the same
filter. A controller reports a level; the view that explains it draws
the record through the band, the band's magnitude about the drive, and
the band's weighting of the record before any instant
(plot.tracking_filter). All three come from here, so the plot cannot
describe a different filter from the one that read the level.
track_waveform runs track_sine's own band over every sample of the
span, and SineTracking.response and SineTracking.weighting are the
frequency and impulse responses of the same Butterworth modes the band
is integrated with, at the corner a given drive frequency has, since a
proportional band has a different corner at every instant.
One channel's span of the tone is held in memory at a time, with its phase, the demodulated record, the filter's output and one mode's working arrays, about a hundred bytes a sample. The extraction streams its record in chunks and this module does not, so a recording of tens of millions of samples per channel is better read with the extraction.
References
The tracking filter here is complex demodulation followed by a Butterworth low-pass from scipy's analog prototype, integrated with its input held over each sample, each worked through above; these are where reading a tone along a known frequency history, and the test this reading is used to understand, are set out.
- Brandt, A. (2011). Noise and Vibration Analysis: Signal Analysis and Experimental Procedures. Wiley. Order tracking: a component that follows a known frequency history, read as an amplitude and a phase that vary slowly along it.
- Department of Defense (2019). MIL-STD-810H, Environmental Engineering Considerations and Laboratory Tests. Method 514.8, Table 514.8-III and its note 3: the tolerances of a sinusoidal test, among them the unfiltered RMS held within 5 percent of the filtered RMS, the level a tracking filter reads.
Classes:
| Name | Description |
|---|---|
SineTracking |
How a tone is read: through which band, by which detector. |
TrackedWaveform |
One channel's span of a tone as a tracking band passes it, |
Functions:
| Name | Description |
|---|---|
track_sine |
Read one tone through a tracking filter and detector, one level |
track_waveform |
One channel's span of a tone through a tracking band, at every |
Classes¶
SineTracking
dataclass
¶
SineTracking(*, detector: str = 'filtered', proportional: float | None = None, fixed: float | None = None, order: int = ORDER, cycles: float = DETECTOR_CYCLES)
How a tone is read: through which band, by which detector.
proportional is the band's full width as a fraction of the drive
frequency (0.5 is 50 %); fixed is a width in Hz. With neither, the
reading is unfiltered and the detector reads the record as it is.
Both at once raises ValueError, since a band is one or the other, and
so does the filter's own output with no filter. Which band this is
follows from which width is set (kind), the way
core.filters.Filtering derives low- or high-pass from its edges,
so no setting can contradict its numbers.
detector is 'filtered' (the default: the band's own output, the
demodulated amplitude at the instant of the reading), 'peak', 'rms'
or 'mean', the last two reported as the peak of the sine they
correspond to (TO_PEAK). The three waveform detectors read over the
last cycles cycles of the drive (default DETECTOR_CYCLES, more
than zero); the filter's output does not use it. order is the
low-pass's design order (default ORDER, at least one).
A proportional band must be narrower than twice the drive frequency
(a fraction between zero and two): wider, half of it reaches below
zero frequency at every point of the sweep, and the band is not
about the drive anywhere. A fixed band must be wider than zero. The
setting is frozen like Filtering, so it cannot change under the
levels read with it.
Methods:
| Name | Description |
|---|---|
bandwidth |
The band's full width in Hz at drive frequency |
settling |
Roughly how long the band takes to follow a change of level, |
response |
The band's magnitude in dB at |
weighting |
How much the record |
describe |
The reading in words, such as 'rms as peak, 10 % proportional'. |
Attributes:
| Name | Type | Description |
|---|---|---|
kind |
str
|
'unfiltered', 'proportional' or 'fixed', derived from which |
Attributes¶
kind
property
¶
'unfiltered', 'proportional' or 'fixed', derived from which width is set rather than stored beside it.
Methods:¶
bandwidth
¶
The band's full width in Hz at drive frequency frequency.
Raises ValueError on an unfiltered reading, which has no band.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
frequency
|
float or array - like
|
The drive frequency, Hz. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
The width, Hz, the shape of |
Source code in src/visualdynamics/core/sine_tracking.py
settling
¶
Roughly how long the band takes to follow a change of level, in seconds: one over its width.
A rule of thumb rather than a bound. On a step in level in a swept tone, the fourth-order filter reaches half of the step at about 0.9 of this time and nine tenths at about 1.3, the same at every width.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
frequency
|
float or array - like
|
The drive frequency, Hz. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Seconds, the shape of |
Source code in src/visualdynamics/core/sine_tracking.py
response
¶
The band's magnitude in dB at frequencies when the drive is
at drive Hz: the shape the record is read through at that
instant.
The low-pass's response at the offset from the drive, which is the band-pass about the drive after demodulation. It is -3 dB at the drive plus and minus half the bandwidth, by the Butterworth's definition. The ripple from the image at twice the drive (see the module docstring) is not included, since it comes from the demodulation, not from the band's shape. This is the analog prototype's response, the one each mode is integrated from; well below the sample rate, the held input changes it by less than a plot can show.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
drive
|
float
|
The drive frequency, Hz. |
required |
frequencies
|
float or array - like
|
Where to read the band, Hz. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
dB, the shape of |
Source code in src/visualdynamics/core/sine_tracking.py
weighting
¶
How much the record lags seconds before an instant counts
in the band's output at that instant, when the drive is at
drive Hz: the low-pass's impulse response, per second.
It integrates to one (a settled band reads a steady tone at its
amplitude), rises from zero, peaks and has a small negative
lobe, the cause of the overshoot a fourth-order filter has on a
step. Its running integral is the band's step response, half way
at about 0.9 of one over the bandwidth (settling). It is zero
at negative lags, since the filter is causal.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
drive
|
float
|
The drive frequency, Hz, which sets a proportional band's corner. |
required |
lags
|
float or array - like
|
Seconds before the instant. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Per second, the shape of |
Source code in src/visualdynamics/core/sine_tracking.py
describe
¶
The reading in words, such as 'rms as peak, 10 % proportional'.
The legend, the levels' comments and the guide all use this wording.
Source code in src/visualdynamics/core/sine_tracking.py
TrackedWaveform
dataclass
¶
TrackedWaveform(*, setting: SineTracking, tone: str, onset: float, dof: str, dimension: str, unit: str, time: ndarray, drive: ndarray, passed: ndarray, level: ndarray)
One channel's span of a tone as a tracking band passes it,
sample by sample: what track_waveform returns.
time is the record's own clock over the span, in seconds; drive
the drive frequency in Hz at each of those samples; passed the
record through the band, in the record's units, which is the
waveform a detector after the band reads; and level the band's
output amplitude, the 'filtered' reading at every sample rather
than at a reading's lines. Two of these compare equal only if they
are the same object, since the arrays have no single truth value,
and they are frozen like the setting they were read with.
Methods:
| Name | Description |
|---|---|
instant |
The first second, on the record's clock, at which the drive |
Methods:¶
instant
¶
The first second, on the record's clock, at which the drive
reaches frequency: where a cursor is placed to show the band
as the tone passes that frequency.
Interpolated linearly between samples. Raises ValueError when
the drive never reaches frequency.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
frequency
|
float
|
Hz. |
required |
Returns:
| Type | Description |
|---|---|
float
|
Seconds on the record's clock. |
Source code in src/visualdynamics/core/sine_tracking.py
Functions:¶
track_sine
¶
track_sine(history: Any, specification: SineSweepSpecification, settings: SineTracking | Sequence[SineTracking], *, tone: str | None = None, onset: float | None = None, channels: Sequence[str] | None = None, lines: int = LINES) -> list[SineLevel]
Read one tone through a tracking filter and detector, one level per setting.
The tone's sweep is rebuilt from the specification and placed on the
recording where its sweep begins (found by matched filter, as the
extraction finds it, or at onset seconds into the recording). Each
channel is demodulated against the sweep's phase once, and each
setting reads it through its band, if it has one, then by its
detector. Every setting is read at the same instants, up to lines
of them, evenly spaced from the first instant at which every
detector has a whole span behind it to the end of the tone's span,
so the levels overlay line for line.
Raises ValueError when no setting is given, when the recording is not evenly sampled, when a channel to be read is not in it, or when it holds less of the tone than the longest detector span.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
history
|
TimeHistory
|
The recording, evenly sampled. |
required |
specification
|
SineSweepSpecification
|
The sweep the drive followed. |
required |
settings
|
SineTracking or sequence of SineTracking
|
The readings to take: usually several, the same response through different bands and detectors. |
required |
tone
|
str
|
Which tone, by name; the specification's first by default. |
None
|
onset
|
float
|
Seconds into the recording where the tone's sweep begins. Found by matched filter when omitted. |
None
|
channels
|
sequence of str
|
The DOFs to read; the specification's control channels by default. Any channel of the recording may be named. |
None
|
lines
|
int
|
How many readings along the sweep, at most. |
`LINES`
|
Returns:
| Type | Description |
|---|---|
list of SineLevel
|
One per setting, in order: the level against frequency on
every channel read, ascending in frequency whichever way the
tone swept, in the recording's own units, each line with the
second it was read ( |
Source code in src/visualdynamics/core/sine_tracking.py
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 | |
track_waveform
¶
track_waveform(history: Any, specification: SineSweepSpecification, setting: SineTracking, *, tone: str | None = None, onset: float | None = None, channel: str | None = None) -> TrackedWaveform
One channel's span of a tone through a tracking band, at every sample: the waveform the band passes and its output amplitude.
The same band track_sine reads its levels through, run the same
way: the tone placed on the recording where its sweep begins,
demodulated against its phase, and low-passed causally from a
settled start. The output is kept at every sample instead of read at
lines, so the band can be drawn on the record it was applied to.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
history
|
TimeHistory
|
The recording, evenly sampled. |
required |
specification
|
SineSweepSpecification
|
The sweep the drive followed. |
required |
setting
|
SineTracking
|
The band. An unfiltered setting raises ValueError, since there is no band to pass the record through. |
required |
tone
|
str
|
Which tone; the specification's first by default. |
None
|
onset
|
float
|
Seconds into the recording where the tone's sweep begins; found by matched filter when omitted. |
None
|
channel
|
str
|
The DOF read; the specification's first control channel by default. Any channel of the recording may be named. |
None
|
Returns:
| Type | Description |
|---|---|
TrackedWaveform
|
The span's clock, drive frequency, passed waveform and output amplitude, in the recording's own units. |