CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
_spectral_py.py2292 linesDownload Raw Back to signal
1"""Tools for spectral analysis.
2"""
3import numpy as np
4import numpy.typing as npt
5from scipy import fft as sp_fft
6from . import _signaltools
7from .windows import get_window
8from ._arraytools import const_ext, even_ext, odd_ext, zero_ext
9import warnings
10from typing import Literal
11
12
13__all__ = ['periodogram', 'welch', 'lombscargle', 'csd', 'coherence',
14           'spectrogram', 'stft', 'istft', 'check_COLA', 'check_NOLA']
15
16
17def lombscargle(
18    x: npt.ArrayLike,
19    y: npt.ArrayLike,
20    freqs: npt.ArrayLike,
21    precenter: bool = False,
22    normalize: bool | Literal["power", "normalize", "amplitude"] = False,
23    *,
24    weights: npt.NDArray | None = None,
25    floating_mean: bool = False,
26) -> npt.NDArray:
27    """
28    Compute the generalized Lomb-Scargle periodogram.
29
30    The Lomb-Scargle periodogram was developed by Lomb [1]_ and further
31    extended by Scargle [2]_ to find, and test the significance of weak
32    periodic signals with uneven temporal sampling. The algorithm used
33    here is based on a weighted least-squares fit of the form
34    ``y(ω) = a*cos(ω*x) + b*sin(ω*x) + c``, where the fit is calculated for
35    each frequency independently. This algorithm was developed by Zechmeister
36    and Kürster which improves the Lomb-Scargle periodogram by enabling
37    the weighting of individual samples and calculating an unknown y offset
38    (also called a "floating-mean" model) [3]_. For more details, and practical
39    considerations, see the excellent reference on the Lomb-Scargle periodogram [4]_.
40
41    When *normalize* is False (or "power") (default) the computed periodogram
42    is unnormalized, it takes the value ``(A**2) * N/4`` for a harmonic
43    signal with amplitude A for sufficiently large N. Where N is the length of x or y.
44
45    When *normalize* is True (or "normalize") the computed periodogram is normalized
46    by the residuals of the data around a constant reference model (at zero).
47
48    When *normalize* is "amplitude" the computed periodogram is the complex
49    representation of the amplitude and phase.
50
51    Input arrays should be 1-D of a real floating data type, which are converted into
52    float64 arrays before processing.
53
54    Parameters
55    ----------
56    x : array_like
57        Sample times.
58    y : array_like
59        Measurement values. Values are assumed to have a baseline of ``y = 0``. If
60        there is a possibility of a y offset, it is recommended to set `floating_mean`
61        to True.
62    freqs : array_like
63        Angular frequencies (e.g., having unit rad/s=2π/s for `x` having unit s) for
64        output periodogram. Frequencies are normally >= 0, as any peak at ``-freq`` will
65        also exist at ``+freq``.
66    precenter : bool, optional
67        Pre-center measurement values by subtracting the mean, if True. This is
68        a legacy parameter and unnecessary if `floating_mean` is True.
69    normalize : bool | str, optional
70        Compute normalized or complex (amplitude + phase) periodogram.
71        Valid options are: ``False``/``"power"``, ``True``/``"normalize"``, or
72        ``"amplitude"``.
73    weights : array_like, optional
74        Weights for each sample. Weights must be nonnegative.
75    floating_mean : bool, optional
76        Determines a y offset for each frequency independently, if True.
77        Else the y offset is assumed to be `0`.
78
79    Returns
80    -------
81    pgram : array_like
82        Lomb-Scargle periodogram.
83
84    Raises
85    ------
86    ValueError
87        If any of the input arrays x, y, freqs, or weights are not 1D, or if any are
88        zero length. Or, if the input arrays x, y, and weights do not have the same
89        shape as each other.
90    ValueError
91        If any weight is < 0, or the sum of the weights is <= 0.
92    ValueError
93        If the normalize parameter is not one of the allowed options.
94
95    See Also
96    --------
97    periodogram: Power spectral density using a periodogram
98    welch: Power spectral density by Welch's method
99    csd: Cross spectral density by Welch's method
100
101    Notes
102    -----
103    The algorithm used will not automatically account for any unknown y offset, unless
104    floating_mean is True. Therefore, for most use cases, if there is a possibility of
105    a y offset, it is recommended to set floating_mean to True. If precenter is True,
106    it performs the operation ``y -= y.mean()``. However, precenter is a legacy
107    parameter, and unnecessary when floating_mean is True. Furthermore, the mean
108    removed by precenter does not account for sample weights, nor will it correct for
109    any bias due to consistently missing observations at peaks and/or troughs. When the
110    normalize parameter is "amplitude", for any frequency in freqs that is below
111    ``(2*pi)/(x.max() - x.min())``, the predicted amplitude will tend towards infinity.
112    The concept of a "Nyquist frequency" limit (see Nyquist-Shannon sampling theorem)
113    is not generally applicable to unevenly sampled data. Therefore, with unevenly
114    sampled data, valid frequencies in freqs can often be much higher than expected.
115
116    References
117    ----------
118    .. [1] N.R. Lomb "Least-squares frequency analysis of unequally spaced
119           data", Astrophysics and Space Science, vol 39, pp. 447-462, 1976
120           :doi:`10.1007/bf00648343`
121
122    .. [2] J.D. Scargle "Studies in astronomical time series analysis. II -
123           Statistical aspects of spectral analysis of unevenly spaced data",
124           The Astrophysical Journal, vol 263, pp. 835-853, 1982
125           :doi:`10.1086/160554`
126
127    .. [3] M. Zechmeister and M. Kürster, "The generalised Lomb-Scargle periodogram.
128           A new formalism for the floating-mean and Keplerian periodograms,"
129           Astronomy and Astrophysics, vol. 496, pp. 577-584, 2009
130           :doi:`10.1051/0004-6361:200811296`
131
132    .. [4] J.T. VanderPlas, "Understanding the Lomb-Scargle Periodogram,"
133           The Astrophysical Journal Supplement Series, vol. 236, no. 1, p. 16,
134           May 2018
135           :doi:`10.3847/1538-4365/aab766`
136
137
138    Examples
139    --------
140    >>> import numpy as np
141    >>> rng = np.random.default_rng()
142
143    First define some input parameters for the signal:
144
145    >>> A = 2.  # amplitude
146    >>> c = 2.  # offset
147    >>> w0 = 1.  # rad/sec
148    >>> nin = 150
149    >>> nout = 1002
150
151    Randomly generate sample times:
152
153    >>> x = rng.uniform(0, 10*np.pi, nin)
154
155    Plot a sine wave for the selected times:
156
157    >>> y = A * np.cos(w0*x) + c
158
159    Define the array of frequencies for which to compute the periodogram:
160
161    >>> w = np.linspace(0.25, 10, nout)
162
163    Calculate Lomb-Scargle periodogram for each of the normalize options:
164
165    >>> from scipy.signal import lombscargle
166    >>> pgram_power = lombscargle(x, y, w, normalize=False)
167    >>> pgram_norm = lombscargle(x, y, w, normalize=True)
168    >>> pgram_amp = lombscargle(x, y, w, normalize='amplitude')
169    ...
170    >>> pgram_power_f = lombscargle(x, y, w, normalize=False, floating_mean=True)
171    >>> pgram_norm_f = lombscargle(x, y, w, normalize=True, floating_mean=True)
172    >>> pgram_amp_f = lombscargle(x, y, w, normalize='amplitude', floating_mean=True)
173
174    Now make a plot of the input data:
175
176    >>> import matplotlib.pyplot as plt
177    >>> fig, (ax_t, ax_p, ax_n, ax_a) = plt.subplots(4, 1, figsize=(5, 6))
178    >>> ax_t.plot(x, y, 'b+')
179    >>> ax_t.set_xlabel('Time [s]')
180    >>> ax_t.set_ylabel('Amplitude')
181
182    Then plot the periodogram for each of the normalize options, as well as with and
183    without floating_mean=True:
184
185    >>> ax_p.plot(w, pgram_power, label='default')
186    >>> ax_p.plot(w, pgram_power_f, label='floating_mean=True')
187    >>> ax_p.set_xlabel('Angular frequency [rad/s]')
188    >>> ax_p.set_ylabel('Power')
189    >>> ax_p.legend(prop={'size': 7})
190    ...
191    >>> ax_n.plot(w, pgram_norm, label='default')
192    >>> ax_n.plot(w, pgram_norm_f, label='floating_mean=True')
193    >>> ax_n.set_xlabel('Angular frequency [rad/s]')
194    >>> ax_n.set_ylabel('Normalized')
195    >>> ax_n.legend(prop={'size': 7})
196    ...
197    >>> ax_a.plot(w, np.abs(pgram_amp), label='default')
198    >>> ax_a.plot(w, np.abs(pgram_amp_f), label='floating_mean=True')
199    >>> ax_a.set_xlabel('Angular frequency [rad/s]')
200    >>> ax_a.set_ylabel('Amplitude')
201    >>> ax_a.legend(prop={'size': 7})
202    ...
203    >>> plt.tight_layout()
204    >>> plt.show()
205
206    """
207
208    # if no weights are provided, assume all data points are equally important
209    if weights is None:
210        weights = np.ones_like(y, dtype=np.float64)
211    else:
212        # if provided, make sure weights is an array and cast to float64
213        weights = np.asarray(weights, dtype=np.float64)
214
215    # make sure other inputs are arrays and cast to float64
216    # done before validation, in case they were not arrays
217    x = np.asarray(x, dtype=np.float64)
218    y = np.asarray(y, dtype=np.float64)
219    freqs = np.asarray(freqs, dtype=np.float64)
220
221    # validate input shapes
222    if not (x.ndim == 1 and x.size > 0 and x.shape == y.shape == weights.shape):
223        raise ValueError("Parameters x, y, weights must be 1-D arrays of "
224                         "equal non-zero length!")
225    if not (freqs.ndim == 1 and freqs.size > 0):
226        raise ValueError("Parameter freqs must be a 1-D array of non-zero length!")
227
228    # validate weights
229    if not (np.all(weights >= 0) and np.sum(weights) > 0):
230        raise ValueError("Parameter weights must have only non-negative entries "
231                         "which sum to a positive value!")
232
233    # validate normalize parameter
234    if isinstance(normalize, bool):
235        # if bool, convert to str literal
236        normalize = "normalize" if normalize else "power"
237
238    if normalize not in ["power", "normalize", "amplitude"]:
239        raise ValueError(
240            "Normalize must be: False (or 'power'), True (or 'normalize'), "
241            "or 'amplitude'."
242        )
243
244    # weight vector must sum to 1
245    weights *= 1.0 / weights.sum()
246
247    # if requested, perform precenter
248    if precenter:
249        y -= y.mean()
250
251    # transform arrays
252    # row vector
253    freqs = freqs.reshape(1, -1)
254    # column vectors
255    x = x.reshape(-1, 1)
256    y = y.reshape(-1, 1)
257    weights = weights.reshape(-1, 1)
258
259    # store frequent intermediates
260    weights_y = weights * y
261    freqst = freqs * x
262    coswt = np.cos(freqst)
263    sinwt = np.sin(freqst)
264
265    Y = np.dot(weights.T, y)  # Eq. 7
266    CC = np.dot(weights.T, coswt * coswt)  # Eq. 13
267    SS = 1.0 - CC  # trig identity: S^2 = 1 - C^2  Eq.14
268    CS = np.dot(weights.T, coswt * sinwt)  # Eq. 15
269
270    if floating_mean:
271        C = np.dot(weights.T, coswt)  # Eq. 8
272        S = np.dot(weights.T, sinwt)  # Eq. 9
273        CC -= C * C  # Eq. 13
274        SS -= S * S  # Eq. 14
275        CS -= C * S  # Eq. 15
276
277    # calculate tau (phase offset to eliminate CS variable)
278    tau = 0.5 * np.arctan2(2.0 * CS, CC - SS)  # Eq. 19
279    freqst_tau = freqst - tau
280
281    # coswt and sinwt are now offset by tau, which eliminates CS
282    coswt_tau = np.cos(freqst_tau)
283    sinwt_tau = np.sin(freqst_tau)
284
285    YC = np.dot(weights_y.T, coswt_tau)  # Eq. 11
286    YS = np.dot(weights_y.T, sinwt_tau)  # Eq. 12
287    CC = np.dot(weights.T, coswt_tau * coswt_tau)  # Eq. 13, CC range is [0.5, 1.0]
288    SS = 1.0 - CC  # trig identity: S^2 = 1 - C^2    Eq. 14, SS range is [0.0, 0.5]
289
290    if floating_mean:
291        C = np.dot(weights.T, coswt_tau)  # Eq. 8
292        S = np.dot(weights.T, sinwt_tau)  # Eq. 9
293        YC -= Y * C  # Eq. 11
294        YS -= Y * S  # Eq. 12
295        CC -= C * C  # Eq. 13, CC range is now [0.0, 1.0]
296        SS -= S * S  # Eq. 14, SS range is now [0.0, 0.5]
297
298    # to prevent division by zero errors with a and b, as well as correcting for
299    # numerical precision errors that lead to CC or SS being approximately -0.0,
300    # make sure CC and SS are both > 0
301    epsneg = np.finfo(dtype=y.dtype).epsneg
302    CC[CC < epsneg] = epsneg
303    SS[SS < epsneg] = epsneg
304
305    # calculate a and b
306    # where: y(w) = a*cos(w) + b*sin(w) + c
307    a = YC / CC  # Eq. A.4 and 6, eliminating CS
308    b = YS / SS  # Eq. A.4 and 6, eliminating CS
309    # c = Y - a * C - b * S
310
311    # store final value as power in A^2 (i.e., (y units)^2)
312    pgram = 2.0 * (a * YC + b * YS)
313
314    # squeeze back to a vector
315    pgram = np.squeeze(pgram)
316
317    if normalize == "power":  # (default)
318        # return the legacy power units ((A**2) * N/4)
319
320        pgram *= float(x.shape[0]) / 4.0
321
322    elif normalize == "normalize":
323        # return the normalized power (power at current frequency wrt the entire signal)
324        # range will be [0, 1]
325
326        YY = np.dot(weights_y.T, y)  # Eq. 10
327        if floating_mean:
328            YY -= Y * Y  # Eq. 10
329
330        pgram *= 0.5 / np.squeeze(YY)  # Eq. 20
331
332    else:  # normalize == "amplitude":
333        # return the complex representation of the best-fit amplitude and phase
334
335        # squeeze back to vectors
336        a = np.squeeze(a)
337        b = np.squeeze(b)
338        tau = np.squeeze(tau)
339
340        # calculate the complex representation, and correct for tau rotation
341        pgram = (a + 1j * b) * np.exp(1j * tau)
342
343    return pgram
344
345
346def periodogram(x, fs=1.0, window='boxcar', nfft=None, detrend='constant',
347                return_onesided=True, scaling='density', axis=-1):
348    """
349    Estimate power spectral density using a periodogram.
350
351    Parameters
352    ----------
353    x : array_like
354        Time series of measurement values
355    fs : float, optional
356        Sampling frequency of the `x` time series. Defaults to 1.0.
357    window : str or tuple or array_like, optional
358        Desired window to use. If `window` is a string or tuple, it is
359        passed to `get_window` to generate the window values, which are
360        DFT-even by default. See `get_window` for a list of windows and
361        required parameters. If `window` is array_like it will be used
362        directly as the window and its length must be equal to the length
363        of the axis over which the periodogram is computed. Defaults
364        to 'boxcar'.
365    nfft : int, optional
366        Length of the FFT used. If `None` the length of `x` will be
367        used.
368    detrend : str or function or `False`, optional
369        Specifies how to detrend each segment. If `detrend` is a
370        string, it is passed as the `type` argument to the `detrend`
371        function. If it is a function, it takes a segment and returns a
372        detrended segment. If `detrend` is `False`, no detrending is
373        done. Defaults to 'constant'.
374    return_onesided : bool, optional
375        If `True`, return a one-sided spectrum for real data. If
376        `False` return a two-sided spectrum. Defaults to `True`, but for
377        complex data, a two-sided spectrum is always returned.
378    scaling : { 'density', 'spectrum' }, optional
379        Selects between computing the power spectral density ('density')
380        where `Pxx` has units of V**2/Hz and computing the squared magnitude
381        spectrum ('spectrum') where `Pxx` has units of V**2, if `x`
382        is measured in V and `fs` is measured in Hz. Defaults to
383        'density'
384    axis : int, optional
385        Axis along which the periodogram is computed; the default is
386        over the last axis (i.e. ``axis=-1``).
387
388    Returns
389    -------
390    f : ndarray
391        Array of sample frequencies.
392    Pxx : ndarray
393        Power spectral density or power spectrum of `x`.
394
395    See Also
396    --------
397    welch: Estimate power spectral density using Welch's method
398    lombscargle: Lomb-Scargle periodogram for unevenly sampled data
399
400    Notes
401    -----
402    Consult the :ref:`tutorial_SpectralAnalysis` section of the :ref:`user_guide`
403    for a discussion of the scalings of the power spectral density and
404    the magnitude (squared) spectrum.
405
406    .. versionadded:: 0.12.0
407
408    Examples
409    --------
410    >>> import numpy as np
411    >>> from scipy import signal
412    >>> import matplotlib.pyplot as plt
413    >>> rng = np.random.default_rng()
414
415    Generate a test signal, a 2 Vrms sine wave at 1234 Hz, corrupted by
416    0.001 V**2/Hz of white noise sampled at 10 kHz.
417
418    >>> fs = 10e3
419    >>> N = 1e5
420    >>> amp = 2*np.sqrt(2)
421    >>> freq = 1234.0
422    >>> noise_power = 0.001 * fs / 2
423    >>> time = np.arange(N) / fs
424    >>> x = amp*np.sin(2*np.pi*freq*time)
425    >>> x += rng.normal(scale=np.sqrt(noise_power), size=time.shape)
426
427    Compute and plot the power spectral density.
428
429    >>> f, Pxx_den = signal.periodogram(x, fs)
430    >>> plt.semilogy(f, Pxx_den)
431    >>> plt.ylim([1e-7, 1e2])
432    >>> plt.xlabel('frequency [Hz]')
433    >>> plt.ylabel('PSD [V**2/Hz]')
434    >>> plt.show()
435
436    If we average the last half of the spectral density, to exclude the
437    peak, we can recover the noise power on the signal.
438
439    >>> np.mean(Pxx_den[25000:])
440    0.000985320699252543
441
442    Now compute and plot the power spectrum.
443
444    >>> f, Pxx_spec = signal.periodogram(x, fs, 'flattop', scaling='spectrum')
445    >>> plt.figure()
446    >>> plt.semilogy(f, np.sqrt(Pxx_spec))
447    >>> plt.ylim([1e-4, 1e1])
448    >>> plt.xlabel('frequency [Hz]')
449    >>> plt.ylabel('Linear spectrum [V RMS]')
450    >>> plt.show()
451
452    The peak height in the power spectrum is an estimate of the RMS
453    amplitude.
454
455    >>> np.sqrt(Pxx_spec.max())
456    2.0077340678640727
457
458    """
459    x = np.asarray(x)
460
461    if x.size == 0:
462        return np.empty(x.shape), np.empty(x.shape)
463
464    if window is None:
465        window = 'boxcar'
466
467    if nfft is None:
468        nperseg = x.shape[axis]
469    elif nfft == x.shape[axis]:
470        nperseg = nfft
471    elif nfft > x.shape[axis]:
472        nperseg = x.shape[axis]
473    elif nfft < x.shape[axis]:
474        s = [np.s_[:]]*len(x.shape)
475        s[axis] = np.s_[:nfft]
476        x = x[tuple(s)]
477        nperseg = nfft
478        nfft = None
479
480    if hasattr(window, 'size'):
481        if window.size != nperseg:
482            raise ValueError('the size of the window must be the same size '
483                             'of the input on the specified axis')
484
485    return welch(x, fs=fs, window=window, nperseg=nperseg, noverlap=0,
486                 nfft=nfft, detrend=detrend, return_onesided=return_onesided,
487                 scaling=scaling, axis=axis)
488
489
490def welch(x, fs=1.0, window='hann', nperseg=None, noverlap=None, nfft=None,
491          detrend='constant', return_onesided=True, scaling='density',
492          axis=-1, average='mean'):
493    r"""
494    Estimate power spectral density using Welch's method.
495
496    Welch's method [1]_ computes an estimate of the power spectral
497    density by dividing the data into overlapping segments, computing a
498    modified periodogram for each segment and averaging the
499    periodograms.
500
501    Parameters
502    ----------
503    x : array_like
504        Time series of measurement values
505    fs : float, optional
506        Sampling frequency of the `x` time series. Defaults to 1.0.
507    window : str or tuple or array_like, optional
508        Desired window to use. If `window` is a string or tuple, it is
509        passed to `get_window` to generate the window values, which are
510        DFT-even by default. See `get_window` for a list of windows and
511        required parameters. If `window` is array_like it will be used
512        directly as the window and its length must be nperseg. Defaults
513        to a Hann window.
514    nperseg : int, optional
515        Length of each segment. Defaults to None, but if window is str or
516        tuple, is set to 256, and if window is array_like, is set to the
517        length of the window.
518    noverlap : int, optional
519        Number of points to overlap between segments. If `None`,
520        ``noverlap = nperseg // 2``. Defaults to `None`.
521    nfft : int, optional
522        Length of the FFT used, if a zero padded FFT is desired. If
523        `None`, the FFT length is `nperseg`. Defaults to `None`.
524    detrend : str or function or `False`, optional
525        Specifies how to detrend each segment. If `detrend` is a
526        string, it is passed as the `type` argument to the `detrend`
527        function. If it is a function, it takes a segment and returns a
528        detrended segment. If `detrend` is `False`, no detrending is
529        done. Defaults to 'constant'.
530    return_onesided : bool, optional
531        If `True`, return a one-sided spectrum for real data. If
532        `False` return a two-sided spectrum. Defaults to `True`, but for
533        complex data, a two-sided spectrum is always returned.
534    scaling : { 'density', 'spectrum' }, optional
535        Selects between computing the power spectral density ('density')
536        where `Pxx` has units of V**2/Hz and computing the squared magnitude
537        spectrum ('spectrum') where `Pxx` has units of V**2, if `x`
538        is measured in V and `fs` is measured in Hz. Defaults to
539        'density'
540    axis : int, optional
541        Axis along which the periodogram is computed; the default is
542        over the last axis (i.e. ``axis=-1``).
543    average : { 'mean', 'median' }, optional
544        Method to use when averaging periodograms. Defaults to 'mean'.
545
546        .. versionadded:: 1.2.0
547
548    Returns
549    -------
550    f : ndarray
551        Array of sample frequencies.
552    Pxx : ndarray
553        Power spectral density or power spectrum of x.
554
555    See Also
556    --------
557    periodogram: Simple, optionally modified periodogram
558    lombscargle: Lomb-Scargle periodogram for unevenly sampled data
559
560    Notes
561    -----
562    An appropriate amount of overlap will depend on the choice of window
563    and on your requirements. For the default Hann window an overlap of
564    50% is a reasonable trade off between accurately estimating the
565    signal power, while not over counting any of the data. Narrower
566    windows may require a larger overlap.
567
568    If `noverlap` is 0, this method is equivalent to Bartlett's method
569    [2]_.
570
571    Consult the :ref:`tutorial_SpectralAnalysis` section of the :ref:`user_guide`
572    for a discussion of the scalings of the power spectral density and
573    the (squared) magnitude spectrum.
574
575    .. versionadded:: 0.12.0
576
577    References
578    ----------
579    .. [1] P. Welch, "The use of the fast Fourier transform for the
580           estimation of power spectra: A method based on time averaging
581           over short, modified periodograms", IEEE Trans. Audio
582           Electroacoust. vol. 15, pp. 70-73, 1967.
583    .. [2] M.S. Bartlett, "Periodogram Analysis and Continuous Spectra",
584           Biometrika, vol. 37, pp. 1-16, 1950.
585
586    Examples
587    --------
588    >>> import numpy as np
589    >>> from scipy import signal
590    >>> import matplotlib.pyplot as plt
591    >>> rng = np.random.default_rng()
592
593    Generate a test signal, a 2 Vrms sine wave at 1234 Hz, corrupted by
594    0.001 V**2/Hz of white noise sampled at 10 kHz.
595
596    >>> fs = 10e3
597    >>> N = 1e5
598    >>> amp = 2*np.sqrt(2)
599    >>> freq = 1234.0
600    >>> noise_power = 0.001 * fs / 2
601    >>> time = np.arange(N) / fs
602    >>> x = amp*np.sin(2*np.pi*freq*time)
603    >>> x += rng.normal(scale=np.sqrt(noise_power), size=time.shape)
604
605    Compute and plot the power spectral density.
606
607    >>> f, Pxx_den = signal.welch(x, fs, nperseg=1024)
608    >>> plt.semilogy(f, Pxx_den)
609    >>> plt.ylim([0.5e-3, 1])
610    >>> plt.xlabel('frequency [Hz]')
611    >>> plt.ylabel('PSD [V**2/Hz]')
612    >>> plt.show()
613
614    If we average the last half of the spectral density, to exclude the
615    peak, we can recover the noise power on the signal.
616
617    >>> np.mean(Pxx_den[256:])
618    0.0009924865443739191
619
620    Now compute and plot the power spectrum.
621
622    >>> f, Pxx_spec = signal.welch(x, fs, 'flattop', 1024, scaling='spectrum')
623    >>> plt.figure()
624    >>> plt.semilogy(f, np.sqrt(Pxx_spec))
625    >>> plt.xlabel('frequency [Hz]')
626    >>> plt.ylabel('Linear spectrum [V RMS]')
627    >>> plt.show()
628
629    The peak height in the power spectrum is an estimate of the RMS
630    amplitude.
631
632    >>> np.sqrt(Pxx_spec.max())
633    2.0077340678640727
634
635    If we now introduce a discontinuity in the signal, by increasing the
636    amplitude of a small portion of the signal by 50, we can see the
637    corruption of the mean average power spectral density, but using a
638    median average better estimates the normal behaviour.
639
640    >>> x[int(N//2):int(N//2)+10] *= 50.
641    >>> f, Pxx_den = signal.welch(x, fs, nperseg=1024)
642    >>> f_med, Pxx_den_med = signal.welch(x, fs, nperseg=1024, average='median')
643    >>> plt.semilogy(f, Pxx_den, label='mean')
644    >>> plt.semilogy(f_med, Pxx_den_med, label='median')
645    >>> plt.ylim([0.5e-3, 1])
646    >>> plt.xlabel('frequency [Hz]')
647    >>> plt.ylabel('PSD [V**2/Hz]')
648    >>> plt.legend()
649    >>> plt.show()
650
651    """
652    freqs, Pxx = csd(x, x, fs=fs, window=window, nperseg=nperseg,
653                     noverlap=noverlap, nfft=nfft, detrend=detrend,
654                     return_onesided=return_onesided, scaling=scaling,
655                     axis=axis, average=average)
656
657    return freqs, Pxx.real
658
659
660def csd(x, y, fs=1.0, window='hann', nperseg=None, noverlap=None, nfft=None,
661        detrend='constant', return_onesided=True, scaling='density',
662        axis=-1, average='mean'):
663    r"""
664    Estimate the cross power spectral density, Pxy, using Welch's method.
665
666    Parameters
667    ----------
668    x : array_like
669        Time series of measurement values
670    y : array_like
671        Time series of measurement values
672    fs : float, optional
673        Sampling frequency of the `x` and `y` time series. Defaults
674        to 1.0.
675    window : str or tuple or array_like, optional
676        Desired window to use. If `window` is a string or tuple, it is
677        passed to `get_window` to generate the window values, which are
678        DFT-even by default. See `get_window` for a list of windows and
679        required parameters. If `window` is array_like it will be used
680        directly as the window and its length must be nperseg. Defaults
681        to a Hann window.
682    nperseg : int, optional
683        Length of each segment. Defaults to None, but if window is str or
684        tuple, is set to 256, and if window is array_like, is set to the
685        length of the window.
686    noverlap: int, optional
687        Number of points to overlap between segments. If `None`,
688        ``noverlap = nperseg // 2``. Defaults to `None`.
689    nfft : int, optional
690        Length of the FFT used, if a zero padded FFT is desired. If
691        `None`, the FFT length is `nperseg`. Defaults to `None`.
692    detrend : str or function or `False`, optional
693        Specifies how to detrend each segment. If `detrend` is a
694        string, it is passed as the `type` argument to the `detrend`
695        function. If it is a function, it takes a segment and returns a
696        detrended segment. If `detrend` is `False`, no detrending is
697        done. Defaults to 'constant'.
698    return_onesided : bool, optional
699        If `True`, return a one-sided spectrum for real data. If
700        `False` return a two-sided spectrum. Defaults to `True`, but for
701        complex data, a two-sided spectrum is always returned.
702    scaling : { 'density', 'spectrum' }, optional
703        Selects between computing the cross spectral density ('density')
704        where `Pxy` has units of V**2/Hz and computing the cross spectrum
705        ('spectrum') where `Pxy` has units of V**2, if `x` and `y` are
706        measured in V and `fs` is measured in Hz. Defaults to 'density'
707    axis : int, optional
708        Axis along which the CSD is computed for both inputs; the
709        default is over the last axis (i.e. ``axis=-1``).
710    average : { 'mean', 'median' }, optional
711        Method to use when averaging periodograms. If the spectrum is
712        complex, the average is computed separately for the real and
713        imaginary parts. Defaults to 'mean'.
714
715        .. versionadded:: 1.2.0
716
717    Returns
718    -------
719    f : ndarray
720        Array of sample frequencies.
721    Pxy : ndarray
722        Cross spectral density or cross power spectrum of x,y.
723
724    See Also
725    --------
726    periodogram: Simple, optionally modified periodogram
727    lombscargle: Lomb-Scargle periodogram for unevenly sampled data
728    welch: Power spectral density by Welch's method. [Equivalent to
729           csd(x,x)]
730    coherence: Magnitude squared coherence by Welch's method.
731
732    Notes
733    -----
734    By convention, Pxy is computed with the conjugate FFT of X
735    multiplied by the FFT of Y.
736
737    If the input series differ in length, the shorter series will be
738    zero-padded to match.
739
740    An appropriate amount of overlap will depend on the choice of window
741    and on your requirements. For the default Hann window an overlap of
742    50% is a reasonable trade off between accurately estimating the
743    signal power, while not over counting any of the data. Narrower
744    windows may require a larger overlap.
745
746    Consult the :ref:`tutorial_SpectralAnalysis` section of the :ref:`user_guide`
747    for a discussion of the scalings of a spectral density and an (amplitude) spectrum.
748
749    .. versionadded:: 0.16.0
750
751    References
752    ----------
753    .. [1] P. Welch, "The use of the fast Fourier transform for the
754           estimation of power spectra: A method based on time averaging
755           over short, modified periodograms", IEEE Trans. Audio
756           Electroacoust. vol. 15, pp. 70-73, 1967.
757    .. [2] Rabiner, Lawrence R., and B. Gold. "Theory and Application of
758           Digital Signal Processing" Prentice-Hall, pp. 414-419, 1975
759
760    Examples
761    --------
762    >>> import numpy as np
763    >>> from scipy import signal
764    >>> import matplotlib.pyplot as plt
765    >>> rng = np.random.default_rng()
766
767    Generate two test signals with some common features.
768
769    >>> fs = 10e3
770    >>> N = 1e5
771    >>> amp = 20
772    >>> freq = 1234.0
773    >>> noise_power = 0.001 * fs / 2
774    >>> time = np.arange(N) / fs
775    >>> b, a = signal.butter(2, 0.25, 'low')
776    >>> x = rng.normal(scale=np.sqrt(noise_power), size=time.shape)
777    >>> y = signal.lfilter(b, a, x)
778    >>> x += amp*np.sin(2*np.pi*freq*time)
779    >>> y += rng.normal(scale=0.1*np.sqrt(noise_power), size=time.shape)
780
781    Compute and plot the magnitude of the cross spectral density.
782
783    >>> f, Pxy = signal.csd(x, y, fs, nperseg=1024)
784    >>> plt.semilogy(f, np.abs(Pxy))
785    >>> plt.xlabel('frequency [Hz]')
786    >>> plt.ylabel('CSD [V**2/Hz]')
787    >>> plt.show()
788
789    """
790    freqs, _, Pxy = _spectral_helper(x, y, fs, window, nperseg, noverlap,
791                                     nfft, detrend, return_onesided, scaling,
792                                     axis, mode='psd')
793
794    # Average over windows.
795    if len(Pxy.shape) >= 2 and Pxy.size > 0:
796        if Pxy.shape[-1] > 1:
797            if average == 'median':
798                # np.median must be passed real arrays for the desired result
799                bias = _median_bias(Pxy.shape[-1])
800                if np.iscomplexobj(Pxy):
801                    Pxy = (np.median(np.real(Pxy), axis=-1)
802                           + 1j * np.median(np.imag(Pxy), axis=-1))
803                else:
804                    Pxy = np.median(Pxy, axis=-1)
805                Pxy /= bias
806            elif average == 'mean':
807                Pxy = Pxy.mean(axis=-1)
808            else:
809                raise ValueError(f'average must be "median" or "mean", got {average}')
810        else:
811            Pxy = np.reshape(Pxy, Pxy.shape[:-1])
812
813    return freqs, Pxy
814
815
816def spectrogram(x, fs=1.0, window=('tukey', .25), nperseg=None, noverlap=None,
817                nfft=None, detrend='constant', return_onesided=True,
818                scaling='density', axis=-1, mode='psd'):
819    """Compute a spectrogram with consecutive Fourier transforms (legacy function).
820
821    Spectrograms can be used as a way of visualizing the change of a
822    nonstationary signal's frequency content over time.
823
824    .. legacy:: function
825
826        :class:`ShortTimeFFT` is a newer STFT / ISTFT implementation with more
827        features also including a :meth:`~ShortTimeFFT.spectrogram` method.
828        A :ref:`comparison <tutorial_stft_legacy_stft>` between the
829        implementations can be found in the :ref:`tutorial_stft` section of
830        the :ref:`user_guide`.
831
832    Parameters
833    ----------
834    x : array_like
835        Time series of measurement values
836    fs : float, optional
837        Sampling frequency of the `x` time series. Defaults to 1.0.
838    window : str or tuple or array_like, optional
839        Desired window to use. If `window` is a string or tuple, it is
840        passed to `get_window` to generate the window values, which are
841        DFT-even by default. See `get_window` for a list of windows and
842        required parameters. If `window` is array_like it will be used
843        directly as the window and its length must be nperseg.
844        Defaults to a Tukey window with shape parameter of 0.25.
845    nperseg : int, optional
846        Length of each segment. Defaults to None, but if window is str or
847        tuple, is set to 256, and if window is array_like, is set to the
848        length of the window.
849    noverlap : int, optional
850        Number of points to overlap between segments. If `None`,
851        ``noverlap = nperseg // 8``. Defaults to `None`.
852    nfft : int, optional
853        Length of the FFT used, if a zero padded FFT is desired. If
854        `None`, the FFT length is `nperseg`. Defaults to `None`.
855    detrend : str or function or `False`, optional
856        Specifies how to detrend each segment. If `detrend` is a
857        string, it is passed as the `type` argument to the `detrend`
858        function. If it is a function, it takes a segment and returns a
859        detrended segment. If `detrend` is `False`, no detrending is
860        done. Defaults to 'constant'.
861    return_onesided : bool, optional
862        If `True`, return a one-sided spectrum for real data. If
863        `False` return a two-sided spectrum. Defaults to `True`, but for
864        complex data, a two-sided spectrum is always returned.
865    scaling : { 'density', 'spectrum' }, optional
866        Selects between computing the power spectral density ('density')
867        where `Sxx` has units of V**2/Hz and computing the power
868        spectrum ('spectrum') where `Sxx` has units of V**2, if `x`
869        is measured in V and `fs` is measured in Hz. Defaults to
870        'density'.
871    axis : int, optional
872        Axis along which the spectrogram is computed; the default is over
873        the last axis (i.e. ``axis=-1``).
874    mode : str, optional
875        Defines what kind of return values are expected. Options are
876        ['psd', 'complex', 'magnitude', 'angle', 'phase']. 'complex' is
877        equivalent to the output of `stft` with no padding or boundary
878        extension. 'magnitude' returns the absolute magnitude of the
879        STFT. 'angle' and 'phase' return the complex angle of the STFT,
880        with and without unwrapping, respectively.
881
882    Returns
883    -------
884    f : ndarray
885        Array of sample frequencies.
886    t : ndarray
887        Array of segment times.
888    Sxx : ndarray
889        Spectrogram of x. By default, the last axis of Sxx corresponds
890        to the segment times.
891
892    See Also
893    --------
894    periodogram: Simple, optionally modified periodogram
895    lombscargle: Lomb-Scargle periodogram for unevenly sampled data
896    welch: Power spectral density by Welch's method.
897    csd: Cross spectral density by Welch's method.
898    ShortTimeFFT: Newer STFT/ISTFT implementation providing more features,
899                  which also includes a :meth:`~ShortTimeFFT.spectrogram`
900                  method.
901
902    Notes
903    -----
904    An appropriate amount of overlap will depend on the choice of window
905    and on your requirements. In contrast to welch's method, where the
906    entire data stream is averaged over, one may wish to use a smaller
907    overlap (or perhaps none at all) when computing a spectrogram, to
908    maintain some statistical independence between individual segments.
909    It is for this reason that the default window is a Tukey window with
910    1/8th of a window's length overlap at each end.
911
912
913    .. versionadded:: 0.16.0
914
915    References
916    ----------
917    .. [1] Oppenheim, Alan V., Ronald W. Schafer, John R. Buck
918           "Discrete-Time Signal Processing", Prentice Hall, 1999.
919
920    Examples
921    --------
922    >>> import numpy as np
923    >>> from scipy import signal
924    >>> from scipy.fft import fftshift
925    >>> import matplotlib.pyplot as plt
926    >>> rng = np.random.default_rng()
927
928    Generate a test signal, a 2 Vrms sine wave whose frequency is slowly
929    modulated around 3kHz, corrupted by white noise of exponentially
930    decreasing magnitude sampled at 10 kHz.
931
932    >>> fs = 10e3
933    >>> N = 1e5
934    >>> amp = 2 * np.sqrt(2)
935    >>> noise_power = 0.01 * fs / 2
936    >>> time = np.arange(N) / float(fs)
937    >>> mod = 500*np.cos(2*np.pi*0.25*time)
938    >>> carrier = amp * np.sin(2*np.pi*3e3*time + mod)
939    >>> noise = rng.normal(scale=np.sqrt(noise_power), size=time.shape)
940    >>> noise *= np.exp(-time/5)
941    >>> x = carrier + noise
942
943    Compute and plot the spectrogram.
944
945    >>> f, t, Sxx = signal.spectrogram(x, fs)
946    >>> plt.pcolormesh(t, f, Sxx, shading='gouraud')
947    >>> plt.ylabel('Frequency [Hz]')
948    >>> plt.xlabel('Time [sec]')
949    >>> plt.show()
950
951    Note, if using output that is not one sided, then use the following:
952
953    >>> f, t, Sxx = signal.spectrogram(x, fs, return_onesided=False)
954    >>> plt.pcolormesh(t, fftshift(f), fftshift(Sxx, axes=0), shading='gouraud')
955    >>> plt.ylabel('Frequency [Hz]')
956    >>> plt.xlabel('Time [sec]')
957    >>> plt.show()
958
959    """
960    modelist = ['psd', 'complex', 'magnitude', 'angle', 'phase']
961    if mode not in modelist:
962        raise ValueError(f'unknown value for mode {mode}, must be one of {modelist}')
963
964    # need to set default for nperseg before setting default for noverlap below
965    window, nperseg = _triage_segments(window, nperseg,
966                                       input_length=x.shape[axis])
967
968    # Less overlap than welch, so samples are more statistically independent
969    if noverlap is None:
970        noverlap = nperseg // 8
971
972    if mode == 'psd':
973        freqs, time, Sxx = _spectral_helper(x, x, fs, window, nperseg,
974                                            noverlap, nfft, detrend,
975                                            return_onesided, scaling, axis,
976                                            mode='psd')
977
978    else:
979        freqs, time, Sxx = _spectral_helper(x, x, fs, window, nperseg,
980                                            noverlap, nfft, detrend,
981                                            return_onesided, scaling, axis,
982                                            mode='stft')
983
984        if mode == 'magnitude':
985            Sxx = np.abs(Sxx)
986        elif mode in ['angle', 'phase']:
987            Sxx = np.angle(Sxx)
988            if mode == 'phase':
989                # Sxx has one additional dimension for time strides
990                if axis < 0:
991                    axis -= 1
992                Sxx = np.unwrap(Sxx, axis=axis)
993
994        # mode =='complex' is same as `stft`, doesn't need modification
995
996    return freqs, time, Sxx
997
998
999def check_COLA(window, nperseg, noverlap, tol=1e-10):
1000    r"""Check whether the Constant OverLap Add (COLA) constraint is met.
1001
1002    Parameters
1003    ----------
1004    window : str or tuple or array_like
1005        Desired window to use. If `window` is a string or tuple, it is
1006        passed to `get_window` to generate the window values, which are
1007        DFT-even by default. See `get_window` for a list of windows and
1008        required parameters. If `window` is array_like it will be used
1009        directly as the window and its length must be nperseg.
1010    nperseg : int
1011        Length of each segment.
1012    noverlap : int
1013        Number of points to overlap between segments.
1014    tol : float, optional
1015        The allowed variance of a bin's weighted sum from the median bin
1016        sum.
1017
1018    Returns
1019    -------
1020    verdict : bool
1021        `True` if chosen combination satisfies COLA within `tol`,
1022        `False` otherwise
1023
1024    See Also
1025    --------
1026    check_NOLA: Check whether the Nonzero Overlap Add (NOLA) constraint is met
1027    stft: Short Time Fourier Transform
1028    istft: Inverse Short Time Fourier Transform
1029
1030    Notes
1031    -----
1032    In order to enable inversion of an STFT via the inverse STFT in
1033    `istft`, it is sufficient that the signal windowing obeys the constraint of
1034    "Constant OverLap Add" (COLA). This ensures that every point in the input
1035    data is equally weighted, thereby avoiding aliasing and allowing full
1036    reconstruction.
1037
1038    Some examples of windows that satisfy COLA:
1039        - Rectangular window at overlap of 0, 1/2, 2/3, 3/4, ...
1040        - Bartlett window at overlap of 1/2, 3/4, 5/6, ...
1041        - Hann window at 1/2, 2/3, 3/4, ...
1042        - Any Blackman family window at 2/3 overlap
1043        - Any window with ``noverlap = nperseg-1``
1044
1045    A very comprehensive list of other windows may be found in [2]_,
1046    wherein the COLA condition is satisfied when the "Amplitude
1047    Flatness" is unity.
1048
1049    .. versionadded:: 0.19.0
1050
1051    References
1052    ----------
1053    .. [1] Julius O. Smith III, "Spectral Audio Signal Processing", W3K
1054           Publishing, 2011,ISBN 978-0-9745607-3-1.
1055    .. [2] G. Heinzel, A. Ruediger and R. Schilling, "Spectrum and
1056           spectral density estimation by the Discrete Fourier transform
1057           (DFT), including a comprehensive list of window functions and
1058           some new at-top windows", 2002,
1059           http://hdl.handle.net/11858/00-001M-0000-0013-557A-5
1060
1061    Examples
1062    --------
1063    >>> from scipy import signal
1064
1065    Confirm COLA condition for rectangular window of 75% (3/4) overlap:
1066
1067    >>> signal.check_COLA(signal.windows.boxcar(100), 100, 75)
1068    True
1069
1070    COLA is not true for 25% (1/4) overlap, though:
1071
1072    >>> signal.check_COLA(signal.windows.boxcar(100), 100, 25)
1073    False
1074
1075    "Symmetrical" Hann window (for filter design) is not COLA:
1076
1077    >>> signal.check_COLA(signal.windows.hann(120, sym=True), 120, 60)
1078    False
1079
1080    "Periodic" or "DFT-even" Hann window (for FFT analysis) is COLA for
1081    overlap of 1/2, 2/3, 3/4, etc.:
1082
1083    >>> signal.check_COLA(signal.windows.hann(120, sym=False), 120, 60)
1084    True
1085
1086    >>> signal.check_COLA(signal.windows.hann(120, sym=False), 120, 80)
1087    True
1088
1089    >>> signal.check_COLA(signal.windows.hann(120, sym=False), 120, 90)
1090    True
1091
1092    """
1093    nperseg = int(nperseg)
1094
1095    if nperseg < 1:
1096        raise ValueError('nperseg must be a positive integer')
1097
1098    if noverlap >= nperseg:
1099        raise ValueError('noverlap must be less than nperseg.')
1100    noverlap = int(noverlap)
1101
1102    if isinstance(window, str) or type(window) is tuple:
1103        win = get_window(window, nperseg)
1104    else:
1105        win = np.asarray(window)
1106        if len(win.shape) != 1:
1107            raise ValueError('window must be 1-D')
1108        if win.shape[0] != nperseg:
1109            raise ValueError('window must have length of nperseg')
1110
1111    step = nperseg - noverlap
1112    binsums = sum(win[ii*step:(ii+1)*step] for ii in range(nperseg//step))
1113
1114    if nperseg % step != 0:
1115        binsums[:nperseg % step] += win[-(nperseg % step):]
1116
1117    deviation = binsums - np.median(binsums)
1118    return np.max(np.abs(deviation)) < tol
1119
1120
1121def check_NOLA(window, nperseg, noverlap, tol=1e-10):
1122    r"""Check whether the Nonzero Overlap Add (NOLA) constraint is met.
1123
1124    Parameters
1125    ----------
1126    window : str or tuple or array_like
1127        Desired window to use. If `window` is a string or tuple, it is
1128        passed to `get_window` to generate the window values, which are
1129        DFT-even by default. See `get_window` for a list of windows and
1130        required parameters. If `window` is array_like it will be used
1131        directly as the window and its length must be nperseg.
1132    nperseg : int
1133        Length of each segment.
1134    noverlap : int
1135        Number of points to overlap between segments.
1136    tol : float, optional
1137        The allowed variance of a bin's weighted sum from the median bin
1138        sum.
1139
1140    Returns
1141    -------
1142    verdict : bool
1143        `True` if chosen combination satisfies the NOLA constraint within
1144        `tol`, `False` otherwise
1145
1146    See Also
1147    --------
1148    check_COLA: Check whether the Constant OverLap Add (COLA) constraint is met
1149    stft: Short Time Fourier Transform
1150    istft: Inverse Short Time Fourier Transform
1151
1152    Notes
1153    -----
1154    In order to enable inversion of an STFT via the inverse STFT in
1155    `istft`, the signal windowing must obey the constraint of "nonzero
1156    overlap add" (NOLA):
1157
1158    .. math:: \sum_{t}w^{2}[n-tH] \ne 0
1159
1160    for all :math:`n`, where :math:`w` is the window function, :math:`t` is the
1161    frame index, and :math:`H` is the hop size (:math:`H` = `nperseg` -
1162    `noverlap`).
1163
1164    This ensures that the normalization factors in the denominator of the
1165    overlap-add inversion equation are not zero. Only very pathological windows
1166    will fail the NOLA constraint.
1167
1168    .. versionadded:: 1.2.0
1169
1170    References
1171    ----------
1172    .. [1] Julius O. Smith III, "Spectral Audio Signal Processing", W3K
1173           Publishing, 2011,ISBN 978-0-9745607-3-1.
1174    .. [2] G. Heinzel, A. Ruediger and R. Schilling, "Spectrum and
1175           spectral density estimation by the Discrete Fourier transform
1176           (DFT), including a comprehensive list of window functions and
1177           some new at-top windows", 2002,
1178           http://hdl.handle.net/11858/00-001M-0000-0013-557A-5
1179
1180    Examples
1181    --------
1182    >>> import numpy as np
1183    >>> from scipy import signal
1184
1185    Confirm NOLA condition for rectangular window of 75% (3/4) overlap:
1186
1187    >>> signal.check_NOLA(signal.windows.boxcar(100), 100, 75)
1188    True
1189
1190    NOLA is also true for 25% (1/4) overlap:
1191
1192    >>> signal.check_NOLA(signal.windows.boxcar(100), 100, 25)
1193    True
1194
1195    "Symmetrical" Hann window (for filter design) is also NOLA:
1196
1197    >>> signal.check_NOLA(signal.windows.hann(120, sym=True), 120, 60)
1198    True
1199
1200    As long as there is overlap, it takes quite a pathological window to fail

Showing the first 1,200 of 2292 lines. Download the file for the rest.

Aluode/PerceptionLabPortable · CoolFace