Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Factored out spectral analysis functions from mlab into a separate module #9757

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
rcsetup_api.rst
sankey_api.rst
scale_api.rst
spectral_api.rst
spines_api.rst
style_api.rst
text_api.rst
Expand Down
27 changes: 27 additions & 0 deletions doc/api/spectral_api.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@

.. _spectral_api:

********
spectral
********

This module provides the classes `Spectrum` and `Periodgramm` for
performing spectral analysis on signals. In the example section
:ref:`spectral_examples` some applications are shown.


Class `Spectrum`
================

.. autoclass:: matplotlib.spectral.Spectrum
:members:
:inherited-members:


Class `Periodogram`
===================

.. autoclass:: matplotlib.spectral.Periodogram
:inherited-members:
:members:

9 changes: 9 additions & 0 deletions examples/spectral/README.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

.. _spectral_examples:

Spectral Analysis
=================

This section covers examples using the :ref:`spectral module <spectral_api>`
which utilizes the Fast Fourier Transform (FFT).

48 changes: 48 additions & 0 deletions examples/spectral/psd_shotnoise.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
r"""
=================================================
Power Spectral Density (PSD) using Welch's Method
=================================================

When investigating noisy signals, Periodograms utilizing
`Welch's Method <https://en.wikipedia.org/wiki/Welch's_method>`_
(i.e., Hann Window and 50% overlap) are useful.

This example shows a signal with white noise dominating above 1 KHz and
`flickr noise <https://en.wikipedia.org/wiki/Flicker_noise>`_ (or 1/f noise)
dominating below 1 KHz. This kind of noise is typical for analog electrial
circuits.

The plot has double logarithmic scaling with the left y-scale depicting the
relative power spectral density and the right one showing the relative spectral
power.

Checkout the :ref:`sphx_glr_gallery_spectral_spectrum_density.py` example for
the influence of the avaraging on the result.
"""
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.spectral import Periodogram

np.random.seed(1648142464) # for reproducibility

n = 2**13 # number of samples
T = 1e-5 # sampling interval
t = np.arange(n) * T # time points
f0, sig_w0, sig_w1 = 5e3, .02, 10
# generate shot nose via low path filtering white noise:
fW = np.fft.rfftfreq(n, T)
W1 = np.random.randn(len(fW)) * sig_w1/2
W1[0] = 0
W1[1:] = 1/np.sqrt(fW[1:])
w1 = np.fft.irfft(W1) * n
# the signal:
x = np.sin(2*np.pi*f0*t) + sig_w0 * np.random.randn(n) + w1


PS = Periodogram(x, f_s=1/T, window='hann', nperseg=n//8, noverlap=n//16,
detrend='mean', unit='V')
fig1, axx1 = PS.plot(density=True, yscale='db', xscale='log', fig_num=1)
axx1.set_xlim(PS.f[1], PS.f_s/2)

plt.show()
59 changes: 59 additions & 0 deletions examples/spectral/spectrum_density.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
r"""
================================
Spectrum versus Spectral density
================================

To demonstrate the difference of a spectrum and a spectral density, this
example investigates a 20 Hz sine signal with amplitude of 1 Volt
corrupted by additive white noise.
with different averaging factors. Increasing the averaging lowers
variance of spectrum (or density) as well as decreasew the frequency
resolution.

The left plot shows the spectrum scaled to amplitude (unit V) and the right
plot depicts the amplitude density (unit V :math:`/\sqrt{Hz}`) of the analytic
signal. A Hanning window with an overlap of 50% is utilized, which corresponds
to `Welch's Method <https://en.wikipedia.org/wiki/Welch's_method>`_.

Note that in the spectrum, the height of the peak at 2 Hz stays is more or less
constant over the different averaging factors. The variations are due to the
influnce of the noise. For longer signals (large `n`) the peak will converge to
its theoretical value of 1 V.
In the density, on the other hand, the noise floor has
a constant magnitude. In summary: Use a spectrum for determining height of
peaks and a density for the magnitude of the noise.

"""
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.spectral import Periodogram
plt.style.use('seaborn-darkgrid') # less intrusive grid lines in this style

np.random.seed(2243049845) # for reproducibility

n = 2**10 # number of samples
T = 10/n # sampling interval (for a duration of 10 s)
t = np.arange(n) * T # time points
f0, sig_w = 20, 2
x = 2*np.sin(2*np.pi*f0*t) + sig_w * np.random.randn(n)

PS = Periodogram(x, f_s=1/T, window='hann', unit='V')
PS.oversampling(8)

fig1, axx1 = plt.subplots(1, 2, sharex=True, num=1, clear=True)
for c, nperseg in enumerate([n//8, n//16, n//32]):
PS.nperseg, PS.noverlap = nperseg, nperseg//2
fa, Xa = PS.spectrum(yscale='amp')
fd, Xd = PS.density(yscale='amp')
axx1[0].plot(fa, Xa, alpha=.8, label=r"$%d\times$ avg." % PS.seg_num)
axx1[1].plot(fd, Xd, alpha=.8, label=r"$%d\times$ avg." % PS.seg_num)

axx1[0].set(title="Avg. Spectrum (Analytic)", ylabel="Amplitude in V")
axx1[1].set(title="Avg. Spectral Density (Analytic)",
ylabel=r"Amplitude in V/$\sqrt{Hz}$")
for ax in axx1:
ax.set_xlabel("Frequency in Hertz")
ax.legend()
fig1.tight_layout()
plt.show()
41 changes: 41 additions & 0 deletions examples/spectral/spectrum_phase.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
r"""
============================
Amplitude and Phase Spectrum
============================

The signal :math:`x(t)` of a damped
`harmonic oscillator <https://en.wikipedia.org/wiki/Harmonic_oscillator>`_ with
frequency :math:`f_0=10` Hz, (initial) amplitude :math:`a=10` Volt (V), and a
damping ratio of :math:`D=.01` can be written as

.. math::

x(t) = a\, e^{-2\pi f_0 D t}\, \cos(2 \pi \sqrt{1-D^2} f_0 t)\ .

The first plot shows a two-sided spectrum with the scaling set to amplitude
(`yscale='amp'`), meaning there's a peak at :math:`\pm f_0`.
The phase spectrum in the second plot shows that the phase is rising
proportionally with the frequency except around :math:`\pm f_0`,
where the phase shifts by almost -180°. Would :math:`D` be zero, then the phase
shift would be exactly -180°.
"""
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.spectral import Spectrum
# plt.style.use('seaborn-darkgrid')

n = 2**9 # number of samples
T = 10/n # sampling interval (for a duration of 10 s)
t = np.arange(n) * T # time points
a, w0, D = 10, 2*np.pi*10, .01 # Amplitude, angular frequency, damping ratio

x = a * np.exp(-D * w0 * t) * np.cos(np.sqrt(1-D**2) * w0 * t)

SP = Spectrum(x, f_s=1/T, unit='V')
fig1, ax1 = SP.plot(yscale='amp', fft_repr='twosided', right_yaxis=True,
fig_num=1)
fig2, ax2 = SP.plot_phase(yunit='deg', fft_repr='twosided',
plt_kw=dict(color='C1'), fig_num=2)

plt.show()
80 changes: 80 additions & 0 deletions examples/spectral/spectrum_scalings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
r"""
======================
Scalings of a Spectrum
======================

Different scalings of a sampled cosine signal :math:`x(t)` with frequency
:math:`f_0 = 2` Hz and amplitude :math:`a = 2` V (Volt), i.e.,

.. math::

x(t) = a\, \cos(2 \pi f_0 t)
= a\, \frac{e^{i 2 \pi f_0 t} + e^{-i 2 \pi f_0 t}}{2}

are presented in this example.
There are three common representations of a singled-sided Spectrum or
FFT of a real signal (property `fft_repr`):

1. Don't scale the amplitudes ('single').
2. Scale the amplitude by a factor of :math:`\sqrt{2}` representing the power
of the signal ('rms', i.e., root-mean-square).
3. Scale by a factor of :math:`2` for representing the amplitude of the
signal ('analytic').

Furthermore, there are three common scalings of the :math:`y`-axis (`yscale`):

a. Unscaled magnitude, revealing the amplitude :math:`a` ('amp').
b. Squared magnitude, which is proportional to the signal's power ('power').
c. Relative power scaled logarithmically in Decibel. The notation dB(1V²) means
:math:`y = 10\, \log_{10}(P_y) - 10\, \log_{10}(1V^2)` ('dB').

The first plot shows the combination of different single-sided FFT. The height
of the peak is denoted in the plot's upper right corner.
The second plot shows the two-sided amplitude spectrum, with the scaling being
set to amplitude (`yscale='amp'`) meaning there's a frequency component of
:math:`a/2` at :math:`\pm f_0`.
"""
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.spectral import Spectrum
plt.style.use('seaborn-darkgrid') # grid is less intrusive with this style

n = 2**7 # number of samples
T = 10/n # sampling interval (for a duration of 10 s)
t = np.arange(n) * T # time points
a, f0 = 2, 2
x = a*np.cos(2*np.pi*f0*t) # the signal

SP = Spectrum(x, f_s=1/T, unit='V')


fig1, axx1 = plt.subplots(3, 3, sharex=True, num=1, figsize=(10, 5.5),
clear=True)
# Legend labels:
lb = [(r"$|a|/2$", r"$|a|/\sqrt{2}$", "$|a|$"),
(r"$|a|^2/4$", r"$|a|^2/2$", "$|a|^2$")]
lb.append([r"$10\,\log_{10}(%s)$" % s[1:-1] for s in lb[1]])

for p, yscale in enumerate(('amp', 'power', 'dB')):
for q, side in enumerate(('onesided', 'rms', 'analytic')):
ax_ = SP.plot(yscale=yscale, fft_repr=side, right_yaxis=False,
plt_kw=dict(color=f'C{q}'), ax=axx1[p, q])
if q != 1 or p != 2: # only one xlabel
ax_.set_xlabel("")
ax_.text(.98, .9, lb[p][q], horizontalalignment='right',
verticalalignment='top', transform=ax_.transAxes)
axx1[0, 0].set_xlim(0, SP.f_s/2)
for p in range(3):
axx1[0, p].set_ylim(-0.1, 2.2)
axx1[1, p].set_ylim(-0.2, 4.4)
fig1.tight_layout()


fig2, ax2 = SP.plot(yscale='amp', fft_repr='twosided', right_yaxis=False,
fig_num=2)
ax2.set_xlim(-SP.f_s/2, +SP.f_s/2)
ax2.text(.98, .95, lb[0][0], horizontalalignment='right',
verticalalignment='top', transform=ax2.transAxes)

plt.show()
58 changes: 58 additions & 0 deletions examples/spectral/window_spectrums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
r"""
===========================
Spectra of Window Functions
===========================

Windowing allows trading frequency resolution for better suppression of side
lobes. This can be illustrated by looking at the spectrum of single frequency
signal with amplitude one and length of one second, i. e.,

.. math::

z(t) = \exp(i 2 \pi f_0 t)

In the plot the frequency :math:`f_0` has been shifted to zero by setting
the center frequency `f_c=f_0`. It shows the spectra of the standard window
functions, where :math:`\Delta f_r` denotes the distance from the maximum
to the first minimum. The high oversampling (`nfft=1024`) improves the
resolution from :math:`\Delta f=1` Hz to :math:`\Delta f=7.8` mHz (property
`df`). The left plot has amplitude scaling, the right depicts a dB-scale. Note
that with dB scaling, the minimas are not depicted correctly due to the limited
resolution in `f`.

The plot shows that the unwindowed spectrum (`window='rect'`) has the
thinnest main lobe, but the highest side lobes. The Blackman window has the
best side lobe suppression at the cost of a third of the frequency resolution.
Frequently the Hann window is used, because it is a good compromise between the
width of the main lobe and the suppression of the higher order side lobes.
"""

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.spectral import Spectrum, WINDOWS

plt.style.use('seaborn-darkgrid') # grid is less intrusive with this style

n, tau = 2**3, 1
T, f0 = tau / n, 2
t = T * np.arange(n)
z = np.exp(2j*np.pi*f0*t) # the signal

SP = Spectrum(z, 1/T, nfft=2**10, fft_repr='twosided', f_c=f0)

fig, axx = plt.subplots(1, 2, sharex=True)
axx[0].set(title="Amplitude Spectra", ylabel="Normalized Amplitude",
xlabel="Normalized Frequency", xlim=(-SP.f_s/2, +SP.f_s/2))
axx[1].set(title="Rel. Power Spectra", xlabel="Normalized Frequency",
ylabel="Relative Power in dB(1)")
wins = [k for k in WINDOWS if k != 'user'] # extract valid window names
for w in wins:
f, X_amp = SP.spectrum(yscale='amp', window=w)
_, X_db = SP.spectrum(yscale='db')
lb_str = w.title() + r", $\Delta f_r=%g$" % SP.f_res
axx[0].plot(f, X_amp, label=lb_str, alpha=.7)
axx[1].plot(f, X_db, label=lb_str, alpha=.7)
axx[1].set_ylim(-120, 5)
axx[1].legend(loc='best', framealpha=1, frameon=True, fancybox=True)
fig.tight_layout()
1 change: 1 addition & 0 deletions examples/ticks_and_spines/README.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@

Ticks and spines
================

Loading