Taking a record to a lower sample rate¶
A record sampled at a rate fs holds frequencies up to half that rate, the Nyquist frequency, and no higher. Content above it does not vanish when the record is sampled. It lands at a lower frequency, the alias frequency |n fs - F| for whichever whole number n brings it into the band from zero to fs / 2, and once it is in the samples nothing in the record can tell it from real content. A 4,600 Hz tone on a channel sampled at 5,120 Hz lands at 520 Hz, at its full height, and its samples are those of a 520 Hz tone with its sign reversed.
Keeping every q-th sample of a record is sampling it again at a q-th of
the rate, and the same rule applies: everything the record holds
between the new Nyquist frequency and the old one folds into the new
band. So a record is filtered before it is decimated, and
visualdynamics.core.resample does the two in that order.
The span and the dropped lines¶
A real filter cannot go from flat to fully down in no distance, so the band a record is trusted over stops short of the Nyquist frequency. Analyzers and controllers commonly keep the alias-free span, the sample rate divided by 2.56. A frame of 1,024 samples has 512 lines up to the Nyquist frequency; 1,024 / 2.56 = 400 of them lie inside the span and are kept, and the other 112, in the filter's roll-off, are computed and dropped. That division is where the line counts 400, 800 and 1,600 on an analyzer's menu come from.
| Frame | Lines to Nyquist | Kept | Dropped | Resolution at 5,120 Hz |
|---|---|---|---|---|
| 1,024 | 512 | 400 | 112 | 5 Hz |
| 2,048 | 1,024 | 800 | 224 | 2.5 Hz |
| 4,096 | 2,048 | 1,600 | 448 | 1.25 Hz |
alias_free_span(rate), line_counts(frame) and
alias_frequency(frequency, rate) give the arithmetic on its own.
Resampling¶
resample(history, rate), or history.resample(rate), filters every
channel through a zero-phase Butterworth low-pass (anti_alias_filter),
then keeps every q-th sample. The new rate must divide the record's into a whole factor of at
least 2; a rate that does not is refused with the nearest two that do.
The filter runs forward and backward, as the Filter reading's does, so
nothing moves in time and the edge transients land at the record's
ends. The result keeps the records' descriptions, the source's clock at
every q-th sample (a record that starts at 3.25 s still does), and the
shock windows, which are in seconds from the record's start. It does
not carry the averaging, since an averaging counts its frame in
samples, which spans another time at the new rate. The channels are
filtered and decimated one at a time, so the record filtered at the
full rate is never held whole beside it. A channel holding a sample
that is not finite is refused, naming the channel and the count: the
filter's recursion runs forward and backward, so one such sample would
make every sample of its channel so.
The filter's corner is at 1.2 times the span (CORNER), 2,400 Hz at
5,120 Hz, among the dropped lines and below the new Nyquist frequency.
A zero-phase pair is 6.02 dB down at its nominal corner, so a corner on
the span would take the span's top line 6 dB down and 0.9 of the span
0.65 dB down; at 1.2 spans the span is flat to 0.1 dB. The lowest
frequency that can fold into the span is the new rate less the span
(3,120 Hz at 5,120 Hz, 1.56 spans), 1.3 corners up. The default design
order is 12 (ORDER), which run forward and backward is 55.8 dB down
there taking 51,200 Hz to 5,120 Hz, 55.1 dB taking 81,920 Hz to
5,120 Hz, and never less than 54.7 dB at any factor, past 50 dB of
attenuation where content would fold. Eleventh order
just gets there at large factors, and tenth falls short at about
46 dB.
resample(history, rate, anti_alias=False) keeps every q-th sample
with no filter and issues an AliasWarning every time. Use it only for
a record known to be band-limited below the new span, because whether
it was is not something the record can show.
The order cannot be reversed. A low-pass applied after the decimation finds the alias already inside its pass band, at a frequency where real content could be, and leaves it where it is. The cure has to come before the samples are thrown away.
An example¶
A record at 81,920 samples a second holds a 0.1 g tone at 300 Hz on a light floor, and a 1 g sensor resonance at 4,600 Hz. Taken to 5,120 Hz:
import numpy as np
from visualdynamics.core.data import TimeHistory
from visualdynamics.core.resample import read_resample
from visualdynamics.plot import plot_resample
rate, g = 81920.0, 9.80665
t = np.arange(16 * 8192) / rate
floor = 0.005 * np.random.default_rng(14).standard_normal(t.size)
y = 0.1 * np.sin(2 * np.pi * 300.0 * t) + np.sin(2 * np.pi * 4600.0 * t)
history = TimeHistory(t, (y + floor)[None] * g, response_dof=['1Z+'],
ordinate_dim=['acceleration'], ordinate_unit=['g'])
slow = history.resample(5120.0) # filtered, then every 16th sample
reading = read_resample(history, 5120.0, frequencies=[4600.0])
print(reading.describe())
plot_resample(history, 5120.0, frequencies=[4600.0], path='resample.png')
read_resample resamples one channel three ways, filtered first,
decimated bare, and decimated then low-passed through the same filter,
and turns
each and the record as recorded into an autospectrum on one line
spacing (Hann frames, half overlapped, 1,024 samples at the new rate by
default). Each frequency given above the new Nyquist frequency is
folded, and the level each spectrum has where it lands is read; with
none given, the record's tallest line above the new Nyquist frequency
is the one folded.
Decimated bare, the 4,600 Hz resonance stands at 520 Hz at 0.0667 g²/Hz, the height of a 1 g tone on a 5 Hz Hann line, and the whole floor between 2,560 and 40,960 Hz has folded on top of the real one, sixteen times as high. Low-passed at 2,400 Hz after the decimation, the 520 Hz line is unchanged. Filtered first, the resonance is gone: the line at 520 Hz is the floor, 79.8 dB below the alias. The 300 Hz tone comes through all three at its height.

| Reading | What it is |
|---|---|
source_rate, rate, factor |
the record's rate, the new one, and how many samples make one |
nyquist, span, corner |
half the new rate, the alias-free span, and the filter's corner, 1.2 spans |
lines, kept, dropped, resolution |
the spectra's lines to the Nyquist frequency, inside the span and above it, and their spacing |
gain_at_span, lowest_fold, gain_at_fold |
the filter's zero-phase gain at the span's top, the lowest frequency that folds into the span, and the gain there |
folds |
each frequency folded: where it lands, whether inside the span or among the dropped lines, and the level at the alias decimated bare, low-passed after, and filtered first, with the last two in dB against the bare |
spectra |
the four autospectra the levels are read from |
In the app¶
Select a time history and choose Resample on the bar. The plot
shows the first selected channel's autospectrum as the example draws
it, and the panel beside it sets the decimation factor (the new rate
is the record's over it), the filter's order, the frame, and the
frequencies to fold, the record's tallest line above the new Nyquist
frequency when the field is empty. Under them it reads out the rates,
the span, the lines kept and dropped, the resolution, and the filter's
gain at the span and at the lowest frequency that folds into it, and
under those each fold in words, with a note where there is nothing to
read: a channel of zeros, or a record with no line above the new
Nyquist frequency. A channel of more than 400,000 samples is read with
the progress bar up. Every edit redraws at once, the
settings are kept when another record is selected, and the reading is
journaled as its plot_resample call. The reading shows what a
resampling would do; history.resample(rate) makes the record.
Where judgment lives¶
- The new rate. Choose it from the highest frequency that matters, at 2.56 times it, not from the specification's top line. A channel whose peak is wanted, a shock's or an SRS's, needs about ten times its filter frequency, since a peak is a single sample.
- Content the record never held. Resampling with a filter keeps anything above the new span out of the result, but it cannot remove an alias that was already in the samples when the record was made. A narrow peak that may be one is settled by recording again at another rate and seeing whether it moves.
anti_alias=False. It is right only for a record already filtered below the new span, and the warning is there because the record cannot say whether it was.