ndpass filter corresponds to the frequencies produced by ``scipy.fft.fftfreq(len(z), T)``. If the envelope of a complex-valued signal `z` with no bandpass filtering is desired, i.e., ``bp_in=(None, None)``, then the envelope corresponds to the absolute value. Hence, it is more efficient to use ``np.abs(z)`` instead of this function. Although computing the envelope based on the analytic signal [1]_ is the natural method for real-valued signals, other methods are also frequently used. The most popular alternative is probably the so-called "square-law" envelope detector and its relatives [2]_. They do not always compute the correct result for all kinds of signals, but are usually correct and typically computationally more efficient for most kinds of narrowband signals. The definition for an envelope presented here is common where instantaneous amplitude and phase are of interest (e.g., as described in [3]_). There exist also other concepts, which rely on the general mathematical idea of an envelope [4]_: A pragmatic approach is to determine all upper and lower signal peaks and use a spline interpolation to determine the curves [5]_. References ---------- .. [1] "Analytic Signal", Wikipedia, https://en.wikipedia.org/wiki/Analytic_signal .. [2] Lyons, Richard, "Digital envelope detection: The good, the bad, and the ugly", IEEE Signal Processing Magazine 34.4 (2017): 183-187. `PDF `__ .. [3] T.G. Kincaid, "The complex representation of signals.", TIS R67# MH5, General Electric Co. (1966). `PDF `__ .. [4] "Envelope (mathematics)", Wikipedia, https://en.wikipedia.org/wiki/Envelope_(mathematics) .. [5] Yang, Yanli. "A signal theoretic approach for envelope analysis of real-valued signals." IEEE Access 5 (2017): 5623-5630. `PDF `__ See Also -------- hilbert: Compute analytic signal by means of Hilbert transform. Examples -------- The following plot illustrates the envelope of a signal with variable frequency and a low-frequency drift. To separate the drift from the envelope, a 4 Hz highpass filter is used. The low-pass residuum of the input bandpass filter is utilized to determine an asymmetric upper and lower bound to enclose the signal. Due to the smoothness of the resulting envelope, it is down-sampled from 500 to 40 samples. Note that the instantaneous amplitude ``a_x`` and the computed envelope ``x_env`` are not perfectly identical. This is due to the signal not being perfectly periodic as well as the existence of some spectral overlapping of ``x_carrier`` and ``x_drift``. Hence, they cannot be completely separated by a bandpass filter. >>> import matplotlib.pyplot as plt >>> import numpy as np >>> from scipy.signal.windows import gaussian >>> from scipy.signal import envelope ... >>> n, n_out = 500, 40 # number of signal samples and envelope samples >>> T = 2 / n # sampling interval for 2 s duration >>> t = np.arange(n) * T # time stamps >>> a_x = gaussian(len(t), 0.4/T) # instantaneous amplitude >>> phi_x = 30*np.pi*t + 35*np.cos(2*np.pi*0.25*t) # instantaneous phase >>> x_carrier = a_x * np.cos(phi_x) >>> x_drift = 0.3 * gaussian(len(t), 0.4/T) # drift >>> x = x_carrier + x_drift ... >>> bp_in = (int(4 * (n*T)), None) # 4 Hz highpass input filter >>> x_env, x_res = envelope(x, bp_in, n_out=n_out) >>> t_out = np.arange(n_out) * (n / n_out) * T ... >>> fg0, ax0 = plt.subplots(1, 1, tight_layout=True) >>> ax0.set_title(r"$4\,$Hz Highpass Envelope of Drifting Signal") >>> ax0.set(xlabel="Time in seconds", xlim=(0, n*T), ylabel="Amplitude") >>> ax0.plot(t, x, 'C0-', alpha=0.5, label="Signal") >>> ax0.plot(t, x_drift, 'C2--', alpha=0.25, label="Drift") >>> ax0.plot(t_out, x_res+x_env, 'C1.-', alpha=0.5, label="Envelope") >>> ax0.plot(t_out, x_res-x_env, 'C1.-', alpha=0.5, label=None) >>> ax0.grid(True) >>> ax0.legend() >>> plt.show() The second example provides a geometric envelope interpretation of complex-valued signals: The following two plots show the complex-valued signal as a blue 3d-trajectory and the envelope as an orange round tube with varying diameter, i.e., as :math:`|a(t)| \exp(j\rho(t))`, with :math:`\rho(t)\in[-\pi,\pi]`. Also, the projection into the 2d real and imaginary coordinate planes of trajectory and tube is depicted. Every point of the complex-valued signal touches the tube's surface. The left plot shows an analytic signal, i.e, the phase difference between imaginary and real part is always 90 degrees, resulting in a spiraling trajectory. It can be seen that in this case the real part has also the expected envelope, i.e., representing the absolute value of the instantaneous amplitude. The right plot shows the real part of that analytic signal being interpreted as a complex-vauled signal, i.e., having zero imaginary part. There the resulting envelope is not as smooth as in the analytic case and the instantaneous amplitude in the real plane is not recovered. If ``z_re`` had been passed as a real-valued signal, i.e., as ``z_re = z.real`` instead of ``z_re = z.real + 0j``, the result would have been identical to the left plot. The reason for this is that real-valued signals are interpreted as being the real part of a complex-valued analytic signal. >>> import matplotlib.pyplot as plt >>> import numpy as np >>> from scipy.signal.windows import gaussian >>> from scipy.signal import envelope ... >>> n, T = 1000, 1/1000 # number of samples and sampling interval >>> t = np.arange(n) * T # time stamps for 1 s duration >>> f_c = 3 # Carrier frequency for signal >>> z = gaussian(len(t), 0.3/T) * np.exp(2j*np.pi*f_c*t) # analytic signal >>> z_re = z.real + 0j # complex signal with zero imaginary part ... >>> e_a, e_r = (envelope(z_, (None, None), residual=None) for z_ in (z, z_re)) ... >>> # Generate grids to visualize envelopes as 2d and 3d surfaces: >>> E2d_t, E2_amp = np.meshgrid(t, [-1, 1]) >>> E2d_1 = np.ones_like(E2_amp) >>> E3d_t, E3d_phi = np.meshgrid(t, np.linspace(-np.pi, np.pi, 300)) >>> ma = 1.8 # maximum axis values in real and imaginary direction ... >>> fg0 = plt.figure(figsize=(6.2, 4.)) >>> ax00 = fg0.add_subplot(1, 2, 1, projection='3d') >>> ax01 = fg0.add_subplot(1, 2, 2, projection='3d', sharex=ax00, ... sharey=ax00, sharez=ax00) >>> ax00.set_title("Analytic Signal") >>> ax00.set(xlim=(0, 1), ylim=(-ma, ma), zlim=(-ma, ma)) >>> ax01.set_title("Real-valued Signal") >>> for z_, e_, ax_ in zip((z, z.real), (e_a, e_r), (ax00, ax01)): ... ax_.set(xlabel="Time $t$", ylabel="Real Amp. $x(t)$", ... zlabel="Imag. Amp. $y(t)$") ... ax_.plot(t, z_.real, 'C0-', zs=-ma, zdir='z', alpha=0.5, label="Real") ... ax_.plot_surface(E2d_t, e_*E2_amp, -ma*E2d_1, color='C1', alpha=0.25) ... ax_.plot(t, z_.imag, 'C0-', zs=+ma, zdir='y', alpha=0.5, label="Imag.") ... ax_.plot_surface(E2d_t, ma*E2d_1, e_*E2_amp, color='C1', alpha=0.25) ... ax_.plot(t, z_.real, z_.imag, 'C0-', label="Signal") ... ax_.plot_surface(E3d_t, e_*np.cos(E3d_phi), e_*np.sin(E3d_phi), ... color='C1', alpha=0.5, shade=True, label="Envelope") ... ax_.view_init(elev=22.7, azim=-114.3) >>> fg0.subplots_adjust(left=0.08, right=0.97, wspace=0.15) >>> plt.show() z