rota-order 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rota/__init__.py +31 -0
- rota/_broadband.py +289 -0
- rota/_components.py +52 -0
- rota/_excerpt.py +456 -0
- rota/_modify.py +779 -0
- rota/_ordertable.py +1089 -0
- rota/_provenance.py +258 -0
- rota/_resolution.py +679 -0
- rota/_ridge.py +973 -0
- rota/_tracked.py +499 -0
- rota/_utils.py +59 -0
- rota/_vk.py +469 -0
- rota/angle.py +669 -0
- rota/extract.py +103 -0
- rota/maps.py +293 -0
- rota/metrics.py +817 -0
- rota/resynth.py +39 -0
- rota/slices.py +278 -0
- rota/speed.py +690 -0
- rota/synth.py +1096 -0
- rota_order-0.3.0.dist-info/METADATA +188 -0
- rota_order-0.3.0.dist-info/RECORD +25 -0
- rota_order-0.3.0.dist-info/WHEEL +5 -0
- rota_order-0.3.0.dist-info/licenses/LICENSE +674 -0
- rota_order-0.3.0.dist-info/top_level.txt +1 -0
rota/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""ROTA: rotational order tracking and analysis.
|
|
2
|
+
|
|
3
|
+
Submodules
|
|
4
|
+
----------
|
|
5
|
+
speed Speed references: tachometer pulses, tacholess estimation, phase,
|
|
6
|
+
control signals.
|
|
7
|
+
angle Angle-domain (computed order tracking) resampling.
|
|
8
|
+
maps RPM-frequency and RPM-order maps.
|
|
9
|
+
slices Order spectra and order slices.
|
|
10
|
+
extract Order extraction (Vold-Kalman, tracked harmonics), removal, residual.
|
|
11
|
+
resynth Order tables, order and broadband resynthesis, provenance.
|
|
12
|
+
synth Synthetic test signals with analytic ground truth.
|
|
13
|
+
metrics Validation metrics.
|
|
14
|
+
|
|
15
|
+
Conventions
|
|
16
|
+
-----------
|
|
17
|
+
Time in seconds, sampling rates in Hz, rotational speed in rev/min (rpm),
|
|
18
|
+
shaft angle in revolutions, vehicle speed in km/h. Signals are arrays of
|
|
19
|
+
shape ``(n,)`` or ``(n, ch)``; every operator acts on each channel
|
|
20
|
+
independently and linearly. Amplitude conventions (peak or RMS) are stated
|
|
21
|
+
in each docstring and selected explicitly where a choice exists.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
__version__ = "0.3.0"
|
|
25
|
+
|
|
26
|
+
from . import angle, extract, maps, metrics, resynth, slices, speed, synth # noqa: F401
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"angle", "extract", "maps", "metrics", "resynth", "slices", "speed", "synth",
|
|
30
|
+
"__version__",
|
|
31
|
+
]
|
rota/_broadband.py
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""Broadband noise resynthesis from a spectral envelope.
|
|
2
|
+
|
|
3
|
+
:func:`synthesize_broadband` renders stationary or operating-point dependent
|
|
4
|
+
noise (the stochastic part of a deterministic-plus-stochastic model, Serra &
|
|
5
|
+
Smith 1990) from a one-sided power spectral density. The noise is built from
|
|
6
|
+
overlapping *grains*: each grain is the inverse FFT of a spectrum with the
|
|
7
|
+
magnitude of the envelope and random phases, multiplied by a window and
|
|
8
|
+
overlap-added (Allen & Rabiner 1977).
|
|
9
|
+
|
|
10
|
+
The window decides whether the result is free of level pumping at the grain
|
|
11
|
+
rate. Grains with independent random phases are uncorrelated, so they add in
|
|
12
|
+
*power*, not in amplitude. The window must therefore be *power
|
|
13
|
+
complementary*, ``sum_k w(n - k R)**2 = 1``, which the square root of the
|
|
14
|
+
periodic Hann window satisfies at 50 % overlap. The amplitude-COLA window
|
|
15
|
+
that is the right choice for a deterministic signal (``sum_k w = 1``, plain
|
|
16
|
+
Hann) makes the power of independent grains dip by 3 dB at every grain
|
|
17
|
+
boundary (ROADMAP section 6, item 11; Griffin & Lim 1984 discuss the two
|
|
18
|
+
conditions for signal estimation from modified short-time spectra).
|
|
19
|
+
|
|
20
|
+
Units: the output has the unit of ``sqrt(psd)`` (e.g. Pa for a PSD in
|
|
21
|
+
Pa^2/Hz). Amplitude convention: the output is a random signal characterised by
|
|
22
|
+
its PSD and RMS (``mean(y**2) = integral of psd``), not by peak amplitudes.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import numpy as np
|
|
26
|
+
|
|
27
|
+
__all__ = ["synthesize_broadband"]
|
|
28
|
+
|
|
29
|
+
_WINDOWS = ("sqrt_hann", "hann")
|
|
30
|
+
_CHUNK_ELEMENTS = 1 << 20 # complex values per block of grains (bounds memory)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _grain_window(length, kind):
|
|
34
|
+
"""Periodic grain window of ``length`` samples.
|
|
35
|
+
|
|
36
|
+
``"sqrt_hann"`` is ``sin(pi m / L)``, the square root of the periodic Hann
|
|
37
|
+
window (power complementary at 50 % overlap); ``"hann"`` is the periodic
|
|
38
|
+
Hann window ``sin(pi m / L)**2`` (amplitude COLA at 50 % overlap).
|
|
39
|
+
"""
|
|
40
|
+
s = np.sin(np.pi * np.arange(length) / length)
|
|
41
|
+
return s if kind == "sqrt_hann" else s * s
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _check_psd_inputs(psd, freqs, fs, n, control, control_axis, grain, highpass):
|
|
45
|
+
"""Validate and normalise the arguments; return arrays and the grain length."""
|
|
46
|
+
fs = float(fs)
|
|
47
|
+
if not (np.isfinite(fs) and fs > 0):
|
|
48
|
+
raise ValueError("fs must be positive and finite")
|
|
49
|
+
if int(n) != n or n < 1:
|
|
50
|
+
raise ValueError("n must be a positive integer number of samples")
|
|
51
|
+
n = int(n)
|
|
52
|
+
|
|
53
|
+
freqs = np.asarray(freqs, dtype=float)
|
|
54
|
+
if freqs.ndim != 1 or freqs.size < 2:
|
|
55
|
+
raise ValueError("freqs must be a 1-D array of at least two frequencies")
|
|
56
|
+
if not np.all(np.isfinite(freqs)) or freqs[0] < 0 or np.any(np.diff(freqs) <= 0):
|
|
57
|
+
raise ValueError("freqs must be finite, non-negative and strictly increasing")
|
|
58
|
+
|
|
59
|
+
psd = np.asarray(psd, dtype=float)
|
|
60
|
+
if psd.ndim not in (1, 2):
|
|
61
|
+
raise ValueError("psd must have shape (n_freq,) or (n_control, n_freq)")
|
|
62
|
+
if psd.shape[-1] != freqs.size:
|
|
63
|
+
raise ValueError("psd must have one column per entry of freqs")
|
|
64
|
+
if not np.all(np.isfinite(psd)) or np.any(psd < 0):
|
|
65
|
+
raise ValueError("psd must be finite and non-negative")
|
|
66
|
+
|
|
67
|
+
if psd.ndim == 1:
|
|
68
|
+
if control is not None or control_axis is not None:
|
|
69
|
+
raise ValueError("control and control_axis apply only to a 2-D psd")
|
|
70
|
+
control_axis = None
|
|
71
|
+
control = None
|
|
72
|
+
else:
|
|
73
|
+
if control is None or control_axis is None:
|
|
74
|
+
raise ValueError("a 2-D psd needs control_axis and control")
|
|
75
|
+
control_axis = np.asarray(control_axis, dtype=float)
|
|
76
|
+
if control_axis.shape != (psd.shape[0],) or control_axis.size < 2:
|
|
77
|
+
raise ValueError("control_axis must have one entry per row of psd (at least two rows)")
|
|
78
|
+
if not np.all(np.isfinite(control_axis)) or np.any(np.diff(control_axis) <= 0):
|
|
79
|
+
raise ValueError("control_axis must be finite and strictly increasing")
|
|
80
|
+
control = np.asarray(control, dtype=float)
|
|
81
|
+
if control.shape != (n,):
|
|
82
|
+
raise ValueError(f"control must have shape ({n},), one value per output sample")
|
|
83
|
+
if not np.all(np.isfinite(control)):
|
|
84
|
+
raise ValueError("control contains non-finite values; regularise it first")
|
|
85
|
+
|
|
86
|
+
grain = float(grain)
|
|
87
|
+
if not (np.isfinite(grain) and grain > 0):
|
|
88
|
+
raise ValueError("grain must be positive and finite")
|
|
89
|
+
length = 2 * round(grain * fs / 2.0)
|
|
90
|
+
if length < 8:
|
|
91
|
+
raise ValueError("grain is shorter than 8 samples; increase grain or fs")
|
|
92
|
+
|
|
93
|
+
highpass = float(highpass)
|
|
94
|
+
if not (np.isfinite(highpass) and 0.0 <= highpass < fs / 2.0):
|
|
95
|
+
raise ValueError("highpass must lie in [0, fs/2)")
|
|
96
|
+
return psd, freqs, fs, n, control, control_axis, length, highpass
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _blend_rows(table, axis, c):
|
|
100
|
+
"""Power-linear blend of the rows of ``table`` at control values ``c``.
|
|
101
|
+
|
|
102
|
+
``table`` has shape ``(n_control, n_bins)`` on the increasing ``axis``;
|
|
103
|
+
values of ``c`` outside the axis are held at the end rows.
|
|
104
|
+
"""
|
|
105
|
+
i = np.clip(np.searchsorted(axis, c, side="right") - 1, 0, axis.size - 2)
|
|
106
|
+
a = np.clip((c - axis[i]) / (axis[i + 1] - axis[i]), 0.0, 1.0)[:, None]
|
|
107
|
+
return (1.0 - a) * table[i] + a * table[i + 1]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _synthesize(psd, freqs, fs, n, *, control, control_axis, grain, highpass, seed, window):
|
|
111
|
+
"""Grain synthesis with a selectable window (``"sqrt_hann"`` or ``"hann"``).
|
|
112
|
+
|
|
113
|
+
``"hann"`` exists only to demonstrate the pumping of an amplitude-COLA
|
|
114
|
+
window with independent random-phase grains (ROADMAP section 6, item 11):
|
|
115
|
+
the scaling is that of ``"sqrt_hann"`` and is not renormalised, so the mean
|
|
116
|
+
power is 1.25 dB low.
|
|
117
|
+
"""
|
|
118
|
+
if window not in _WINDOWS:
|
|
119
|
+
raise ValueError(f"window must be one of {_WINDOWS}, got {window!r}")
|
|
120
|
+
psd, freqs, fs, n, control, control_axis, length, highpass = _check_psd_inputs(
|
|
121
|
+
psd, freqs, fs, n, control, control_axis, grain, highpass
|
|
122
|
+
)
|
|
123
|
+
rng = np.random.default_rng(seed)
|
|
124
|
+
|
|
125
|
+
hop = length // 2
|
|
126
|
+
n_bins = hop + 1
|
|
127
|
+
f_bins = np.arange(n_bins) * (fs / length)
|
|
128
|
+
keep = f_bins >= highpass
|
|
129
|
+
|
|
130
|
+
# Envelope on the grain's frequency bins (linear in frequency and power).
|
|
131
|
+
rows = np.atleast_2d(psd)
|
|
132
|
+
table = np.stack([np.interp(f_bins, freqs, row) for row in rows])
|
|
133
|
+
|
|
134
|
+
# Grain k starts at sample (k - 1) * hop and is centred on sample k * hop,
|
|
135
|
+
# so that every output sample lies in the fully overlapped region.
|
|
136
|
+
n_grains = -(-n // hop) + 1
|
|
137
|
+
if control is not None:
|
|
138
|
+
centres = np.minimum(np.arange(n_grains) * hop, n - 1)
|
|
139
|
+
grain_control = control[centres]
|
|
140
|
+
|
|
141
|
+
win = _grain_window(length, window)
|
|
142
|
+
# |G_k| = sqrt(psd_k * fs * L / 2) for every bin k (see the Notes of
|
|
143
|
+
# synthesize_broadband).
|
|
144
|
+
scale = np.sqrt(0.5 * fs * length)
|
|
145
|
+
buf = np.zeros((n_grains + 1, hop))
|
|
146
|
+
step = max(1, _CHUNK_ELEMENTS // n_bins)
|
|
147
|
+
for k0 in range(0, n_grains, step):
|
|
148
|
+
k1 = min(n_grains, k0 + step)
|
|
149
|
+
if control is None:
|
|
150
|
+
power = np.broadcast_to(table[0], (k1 - k0, n_bins))
|
|
151
|
+
else:
|
|
152
|
+
power = _blend_rows(table, control_axis, grain_control[k0:k1])
|
|
153
|
+
mag = scale * np.sqrt(power) * keep
|
|
154
|
+
u = rng.random((k1 - k0, n_bins))
|
|
155
|
+
spec = mag * np.exp(2j * np.pi * u)
|
|
156
|
+
# DC and Nyquist bins are real: a random sign instead of a phase.
|
|
157
|
+
spec[:, 0] = np.where(u[:, 0] < 0.5, mag[:, 0], -mag[:, 0])
|
|
158
|
+
spec[:, -1] = np.where(u[:, -1] < 0.5, mag[:, -1], -mag[:, -1])
|
|
159
|
+
frames = np.fft.irfft(spec, n=length, axis=1) * win
|
|
160
|
+
buf[k0:k1] += frames[:, :hop]
|
|
161
|
+
buf[k0 + 1 : k1 + 1] += frames[:, hop:]
|
|
162
|
+
return buf.reshape(-1)[hop : hop + n].copy()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def synthesize_broadband(
|
|
166
|
+
psd, freqs, fs, n, *, control=None, control_axis=None, grain=0.05, highpass=20.0, seed=None
|
|
167
|
+
):
|
|
168
|
+
"""Broadband noise with a given power spectral density, optionally operating-point dependent.
|
|
169
|
+
|
|
170
|
+
The noise is made of grains of ``grain`` seconds (rounded to an even
|
|
171
|
+
number of samples ``L``) at a hop of ``L/2``. Each grain is the inverse
|
|
172
|
+
real FFT of a spectrum whose magnitude follows ``psd`` at the grain's
|
|
173
|
+
control value and whose phases are independent and uniformly random;
|
|
174
|
+
bins below ``highpass`` are zeroed. The grains are multiplied by the
|
|
175
|
+
square root of the periodic Hann window and overlap-added. Because
|
|
176
|
+
``sin**2 + cos**2 = 1`` this window is *power complementary* at 50 %
|
|
177
|
+
overlap, so the power of the independent grains adds up to a constant and
|
|
178
|
+
the output has no level pumping at the grain rate (ROADMAP section 6,
|
|
179
|
+
item 11).
|
|
180
|
+
|
|
181
|
+
With a 2-D ``psd`` the envelope depends on a control variable (engine
|
|
182
|
+
speed in rpm, vehicle speed in km/h, load, ...). Each grain uses the
|
|
183
|
+
control value at its centre sample, located between two neighbouring rows
|
|
184
|
+
of ``psd``; the *power* spectrum is interpolated linearly between those
|
|
185
|
+
rows, ``(1 - a) psd_i + a psd_{i+1}``. This is a crossfade of the two
|
|
186
|
+
operating points with weights ``sqrt(1 - a)`` and ``sqrt(a)`` on the
|
|
187
|
+
amplitude: the power stays between the two operating points (a linear
|
|
188
|
+
amplitude crossfade of two independent noises dips by 3 dB at the middle).
|
|
189
|
+
Between grains the window crossfades the power in the same
|
|
190
|
+
power-complementary way, so the level follows the control smoothly.
|
|
191
|
+
|
|
192
|
+
Parameters
|
|
193
|
+
----------
|
|
194
|
+
psd : array_like, shape (n_freq,) or (n_control, n_freq)
|
|
195
|
+
One-sided power spectral density in unit**2/Hz, non-negative, on
|
|
196
|
+
``freqs``. A 1-D array is a stationary envelope; a 2-D array holds one
|
|
197
|
+
envelope per operating point (row) for ``control_axis``. The envelope
|
|
198
|
+
is interpolated linearly in frequency and in power onto the grain's
|
|
199
|
+
bins and held constant beyond the ends of ``freqs`` (add zero-valued
|
|
200
|
+
end points to band-limit; give a dense grid for a steep envelope).
|
|
201
|
+
freqs : array_like, shape (n_freq,)
|
|
202
|
+
Strictly increasing frequencies in Hz, at least two.
|
|
203
|
+
fs : float
|
|
204
|
+
Sampling rate in Hz.
|
|
205
|
+
n : int
|
|
206
|
+
Number of output samples.
|
|
207
|
+
control : array_like, shape (n,), optional
|
|
208
|
+
Control value for every output sample, in the unit of
|
|
209
|
+
``control_axis`` (rpm, km/h, ...). Required for a 2-D ``psd``. Values
|
|
210
|
+
outside ``control_axis`` use the first or last row (no
|
|
211
|
+
extrapolation).
|
|
212
|
+
control_axis : array_like, shape (n_control,), optional
|
|
213
|
+
Strictly increasing control values of the rows of ``psd`` (at least
|
|
214
|
+
two). Required for a 2-D ``psd``.
|
|
215
|
+
grain : float, default 0.05
|
|
216
|
+
Grain length in s. The hop is half of it, so the grain rate (the
|
|
217
|
+
frequency at which pumping would appear) is ``fs / hop = 2 / grain``
|
|
218
|
+
(40 Hz by default). The grain is rounded to an even number of samples
|
|
219
|
+
and must have at least 8. The frequency resolution of the envelope is
|
|
220
|
+
about ``1 / grain``; a shorter grain follows a changing control more
|
|
221
|
+
closely, a longer one resolves narrower spectral features.
|
|
222
|
+
highpass : float, default 20.0
|
|
223
|
+
Cut-off in Hz: grain bins with a centre frequency below it are set to
|
|
224
|
+
zero, which removes the sub-audio content whose offsets at the window
|
|
225
|
+
switches cause clicks (ROADMAP section 6, item 11). The cut-off
|
|
226
|
+
is a bin-wise zeroing at the grain's resolution ``1 / grain``; energy
|
|
227
|
+
within about that distance below the cut-off leaks in through the
|
|
228
|
+
window. 0 keeps the DC bin.
|
|
229
|
+
seed : int, numpy.random.Generator or None
|
|
230
|
+
Seed or generator for the random phases. The same integer seed and
|
|
231
|
+
arguments give bit-identical output; None draws fresh entropy.
|
|
232
|
+
|
|
233
|
+
Returns
|
|
234
|
+
-------
|
|
235
|
+
ndarray, shape (n,)
|
|
236
|
+
Noise in the unit of ``sqrt(psd)`` (e.g. Pa). It is a random signal
|
|
237
|
+
characterised by its PSD, not by peak amplitudes: the amplitude
|
|
238
|
+
convention is RMS, with ``mean(y**2)`` equal to the integral of the
|
|
239
|
+
(high-passed) ``psd`` over 0 to ``fs/2`` at every instant, in the
|
|
240
|
+
sense of the expectation over the random phases.
|
|
241
|
+
|
|
242
|
+
Raises
|
|
243
|
+
------
|
|
244
|
+
ValueError
|
|
245
|
+
For inconsistent shapes, non-finite or negative ``psd``, non-increasing
|
|
246
|
+
``freqs`` or ``control_axis``, a ``control`` of the wrong length or
|
|
247
|
+
with non-finite values, a ``grain`` shorter than 8 samples, or a
|
|
248
|
+
``highpass`` outside ``[0, fs/2)``.
|
|
249
|
+
|
|
250
|
+
Notes
|
|
251
|
+
-----
|
|
252
|
+
Scaling. Let ``L`` be the grain length, ``R = L/2`` the hop, ``df = fs/L``
|
|
253
|
+
the bin spacing, ``f_k = k df`` and ``G_k`` the grain's spectrum
|
|
254
|
+
(``k = 0 .. L/2``). With ``g = irfft(G)``, Parseval gives the mean square
|
|
255
|
+
of a grain ``(|G_0|**2 + |G_{L/2}|**2 + 2 sum_{0<k<L/2} |G_k|**2) / L**2``.
|
|
256
|
+
The target is ``integral psd df ~ sum_{0<k<L/2} psd_k df + (psd_0 +
|
|
257
|
+
psd_{L/2}) df / 2`` (trapezoid rule; DC and Nyquist cover half a bin).
|
|
258
|
+
Equating the two term by term gives, for every bin including DC and
|
|
259
|
+
Nyquist, ``|G_k| = sqrt(psd_k fs L / 2)``. With independent uniform phases
|
|
260
|
+
(a random sign for the real DC and Nyquist bins) ``E[g[m]**2]`` equals that
|
|
261
|
+
mean square for every sample ``m`` of the grain, not only on average over
|
|
262
|
+
``m``. Independent zero-mean grains add in power, so
|
|
263
|
+
``E[y[n]**2] = sum_k w(n - kR)**2 E[g**2] = integral psd df`` for every
|
|
264
|
+
``n``, since ``sum_k w**2 = 1``. The long-term PSD of ``y`` is the
|
|
265
|
+
envelope smoothed by the normalised power transfer function of the window
|
|
266
|
+
(width about ``1 / grain``), which leaves a locally smooth envelope
|
|
267
|
+
unchanged. Within a grain the magnitudes are deterministic, so the sample
|
|
268
|
+
values approach a Gaussian distribution only by the central limit theorem
|
|
269
|
+
over the bins; an envelope only a few bins wide gives a sinusoid-like
|
|
270
|
+
rather than a Gaussian narrow-band noise.
|
|
271
|
+
|
|
272
|
+
References
|
|
273
|
+
----------
|
|
274
|
+
Serra, X., & Smith, J. O. (1990). Spectral modeling synthesis: A sound
|
|
275
|
+
analysis/synthesis system based on a deterministic plus stochastic
|
|
276
|
+
decomposition. Computer Music Journal, 14(4), 12-24.
|
|
277
|
+
Allen, J. B., & Rabiner, L. R. (1977). A unified approach to short-time
|
|
278
|
+
Fourier analysis and synthesis. Proceedings of the IEEE, 65(11), 1558-1564.
|
|
279
|
+
Griffin, D. W., & Lim, J. S. (1984). Signal estimation from modified
|
|
280
|
+
short-time Fourier transform. IEEE Transactions on Acoustics, Speech, and
|
|
281
|
+
Signal Processing, 32(2), 236-243.
|
|
282
|
+
Welch, P. D. (1967). The use of fast Fourier transform for the estimation
|
|
283
|
+
of power spectra. IEEE Transactions on Audio and Electroacoustics, 15(2),
|
|
284
|
+
70-73.
|
|
285
|
+
"""
|
|
286
|
+
return _synthesize(
|
|
287
|
+
psd, freqs, fs, n, control=control, control_axis=control_axis,
|
|
288
|
+
grain=grain, highpass=highpass, seed=seed, window="sqrt_hann",
|
|
289
|
+
)
|
rota/_components.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Result container shared by the order extractors."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class OrderComponents:
|
|
10
|
+
"""Order components extracted from a signal.
|
|
11
|
+
|
|
12
|
+
For order ``k`` with carrier angle ``theta_k(t) = 2 pi orders[k] rev(t)``
|
|
13
|
+
(``rev`` the shaft angle in revolutions of the order's shaft, from
|
|
14
|
+
:func:`rota.speed.rpm_to_phase`)::
|
|
15
|
+
|
|
16
|
+
waveforms[:, k] = Re{ envelopes[:, k] * exp(1j * theta_k) }
|
|
17
|
+
|
|
18
|
+
so ``|envelopes|`` is the *peak* amplitude in the signal unit and
|
|
19
|
+
``angle(envelopes)`` the phase relative to the carrier. This matches the
|
|
20
|
+
truth of :func:`rota.synth.synth_runup`:
|
|
21
|
+
``order_amplitude * exp(1j * (order_phase - 2 pi order rev))``.
|
|
22
|
+
|
|
23
|
+
Attributes
|
|
24
|
+
----------
|
|
25
|
+
orders : ndarray, shape (k,)
|
|
26
|
+
Order numbers.
|
|
27
|
+
shafts : ndarray of int, shape (k,)
|
|
28
|
+
Column of ``rpm`` each order refers to.
|
|
29
|
+
waveforms : ndarray, shape (n, k) or (n, k, ch)
|
|
30
|
+
Real order waveforms, signal unit.
|
|
31
|
+
envelopes : ndarray, complex, shape (n, k) or (n, k, ch)
|
|
32
|
+
Complex peak envelopes, signal unit.
|
|
33
|
+
rev : ndarray, shape (n,) or (n, n_shafts)
|
|
34
|
+
Shaft angle(s) in revolutions used for the carriers.
|
|
35
|
+
method : str
|
|
36
|
+
Name of the extractor.
|
|
37
|
+
params : dict
|
|
38
|
+
Parameters used (JSON-serialisable).
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
orders: np.ndarray
|
|
42
|
+
shafts: np.ndarray
|
|
43
|
+
waveforms: np.ndarray
|
|
44
|
+
envelopes: np.ndarray
|
|
45
|
+
rev: np.ndarray
|
|
46
|
+
method: str
|
|
47
|
+
params: dict = field(default_factory=dict)
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def total(self):
|
|
51
|
+
"""Sum of all order waveforms, shape (n,) or (n, ch)."""
|
|
52
|
+
return self.waveforms.sum(axis=1)
|