123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311 |
- """
- Functions for identifying peaks in signals.
- """
- import math
- import numpy as np
- from scipy.signal._wavelets import cwt, ricker
- from scipy.stats import scoreatpercentile
- from ._peak_finding_utils import (
- _local_maxima_1d,
- _select_by_peak_distance,
- _peak_prominences,
- _peak_widths
- )
- __all__ = ['argrelmin', 'argrelmax', 'argrelextrema', 'peak_prominences',
- 'peak_widths', 'find_peaks', 'find_peaks_cwt']
- def _boolrelextrema(data, comparator, axis=0, order=1, mode='clip'):
- """
- Calculate the relative extrema of `data`.
- Relative extrema are calculated by finding locations where
- ``comparator(data[n], data[n+1:n+order+1])`` is True.
- Parameters
- ----------
- data : ndarray
- Array in which to find the relative extrema.
- comparator : callable
- Function to use to compare two data points.
- Should take two arrays as arguments.
- axis : int, optional
- Axis over which to select from `data`. Default is 0.
- order : int, optional
- How many points on each side to use for the comparison
- to consider ``comparator(n,n+x)`` to be True.
- mode : str, optional
- How the edges of the vector are treated. 'wrap' (wrap around) or
- 'clip' (treat overflow as the same as the last (or first) element).
- Default 'clip'. See numpy.take.
- Returns
- -------
- extrema : ndarray
- Boolean array of the same shape as `data` that is True at an extrema,
- False otherwise.
- See also
- --------
- argrelmax, argrelmin
- Examples
- --------
- >>> import numpy as np
- >>> testdata = np.array([1,2,3,2,1])
- >>> _boolrelextrema(testdata, np.greater, axis=0)
- array([False, False, True, False, False], dtype=bool)
- """
- if (int(order) != order) or (order < 1):
- raise ValueError('Order must be an int >= 1')
- datalen = data.shape[axis]
- locs = np.arange(0, datalen)
- results = np.ones(data.shape, dtype=bool)
- main = data.take(locs, axis=axis, mode=mode)
- for shift in range(1, order + 1):
- plus = data.take(locs + shift, axis=axis, mode=mode)
- minus = data.take(locs - shift, axis=axis, mode=mode)
- results &= comparator(main, plus)
- results &= comparator(main, minus)
- if ~results.any():
- return results
- return results
- def argrelmin(data, axis=0, order=1, mode='clip'):
- """
- Calculate the relative minima of `data`.
- Parameters
- ----------
- data : ndarray
- Array in which to find the relative minima.
- axis : int, optional
- Axis over which to select from `data`. Default is 0.
- order : int, optional
- How many points on each side to use for the comparison
- to consider ``comparator(n, n+x)`` to be True.
- mode : str, optional
- How the edges of the vector are treated.
- Available options are 'wrap' (wrap around) or 'clip' (treat overflow
- as the same as the last (or first) element).
- Default 'clip'. See numpy.take.
- Returns
- -------
- extrema : tuple of ndarrays
- Indices of the minima in arrays of integers. ``extrema[k]`` is
- the array of indices of axis `k` of `data`. Note that the
- return value is a tuple even when `data` is 1-D.
- See Also
- --------
- argrelextrema, argrelmax, find_peaks
- Notes
- -----
- This function uses `argrelextrema` with np.less as comparator. Therefore, it
- requires a strict inequality on both sides of a value to consider it a
- minimum. This means flat minima (more than one sample wide) are not detected.
- In case of 1-D `data` `find_peaks` can be used to detect all
- local minima, including flat ones, by calling it with negated `data`.
- .. versionadded:: 0.11.0
- Examples
- --------
- >>> import numpy as np
- >>> from scipy.signal import argrelmin
- >>> x = np.array([2, 1, 2, 3, 2, 0, 1, 0])
- >>> argrelmin(x)
- (array([1, 5]),)
- >>> y = np.array([[1, 2, 1, 2],
- ... [2, 2, 0, 0],
- ... [5, 3, 4, 4]])
- ...
- >>> argrelmin(y, axis=1)
- (array([0, 2]), array([2, 1]))
- """
- return argrelextrema(data, np.less, axis, order, mode)
- def argrelmax(data, axis=0, order=1, mode='clip'):
- """
- Calculate the relative maxima of `data`.
- Parameters
- ----------
- data : ndarray
- Array in which to find the relative maxima.
- axis : int, optional
- Axis over which to select from `data`. Default is 0.
- order : int, optional
- How many points on each side to use for the comparison
- to consider ``comparator(n, n+x)`` to be True.
- mode : str, optional
- How the edges of the vector are treated.
- Available options are 'wrap' (wrap around) or 'clip' (treat overflow
- as the same as the last (or first) element).
- Default 'clip'. See `numpy.take`.
- Returns
- -------
- extrema : tuple of ndarrays
- Indices of the maxima in arrays of integers. ``extrema[k]`` is
- the array of indices of axis `k` of `data`. Note that the
- return value is a tuple even when `data` is 1-D.
- See Also
- --------
- argrelextrema, argrelmin, find_peaks
- Notes
- -----
- This function uses `argrelextrema` with np.greater as comparator. Therefore,
- it requires a strict inequality on both sides of a value to consider it a
- maximum. This means flat maxima (more than one sample wide) are not detected.
- In case of 1-D `data` `find_peaks` can be used to detect all
- local maxima, including flat ones.
- .. versionadded:: 0.11.0
- Examples
- --------
- >>> import numpy as np
- >>> from scipy.signal import argrelmax
- >>> x = np.array([2, 1, 2, 3, 2, 0, 1, 0])
- >>> argrelmax(x)
- (array([3, 6]),)
- >>> y = np.array([[1, 2, 1, 2],
- ... [2, 2, 0, 0],
- ... [5, 3, 4, 4]])
- ...
- >>> argrelmax(y, axis=1)
- (array([0]), array([1]))
- """
- return argrelextrema(data, np.greater, axis, order, mode)
- def argrelextrema(data, comparator, axis=0, order=1, mode='clip'):
- """
- Calculate the relative extrema of `data`.
- Parameters
- ----------
- data : ndarray
- Array in which to find the relative extrema.
- comparator : callable
- Function to use to compare two data points.
- Should take two arrays as arguments.
- axis : int, optional
- Axis over which to select from `data`. Default is 0.
- order : int, optional
- How many points on each side to use for the comparison
- to consider ``comparator(n, n+x)`` to be True.
- mode : str, optional
- How the edges of the vector are treated. 'wrap' (wrap around) or
- 'clip' (treat overflow as the same as the last (or first) element).
- Default is 'clip'. See `numpy.take`.
- Returns
- -------
- extrema : tuple of ndarrays
- Indices of the maxima in arrays of integers. ``extrema[k]`` is
- the array of indices of axis `k` of `data`. Note that the
- return value is a tuple even when `data` is 1-D.
- See Also
- --------
- argrelmin, argrelmax
- Notes
- -----
- .. versionadded:: 0.11.0
- Examples
- --------
- >>> import numpy as np
- >>> from scipy.signal import argrelextrema
- >>> x = np.array([2, 1, 2, 3, 2, 0, 1, 0])
- >>> argrelextrema(x, np.greater)
- (array([3, 6]),)
- >>> y = np.array([[1, 2, 1, 2],
- ... [2, 2, 0, 0],
- ... [5, 3, 4, 4]])
- ...
- >>> argrelextrema(y, np.less, axis=1)
- (array([0, 2]), array([2, 1]))
- """
- results = _boolrelextrema(data, comparator,
- axis, order, mode)
- return np.nonzero(results)
- def _arg_x_as_expected(value):
- """Ensure argument `x` is a 1-D C-contiguous array of dtype('float64').
- Used in `find_peaks`, `peak_prominences` and `peak_widths` to make `x`
- compatible with the signature of the wrapped Cython functions.
- Returns
- -------
- value : ndarray
- A 1-D C-contiguous array with dtype('float64').
- """
- value = np.asarray(value, order='C', dtype=np.float64)
- if value.ndim != 1:
- raise ValueError('`x` must be a 1-D array')
- return value
- def _arg_peaks_as_expected(value):
- """Ensure argument `peaks` is a 1-D C-contiguous array of dtype('intp').
- Used in `peak_prominences` and `peak_widths` to make `peaks` compatible
- with the signature of the wrapped Cython functions.
- Returns
- -------
- value : ndarray
- A 1-D C-contiguous array with dtype('intp').
- """
- value = np.asarray(value)
- if value.size == 0:
- # Empty arrays default to np.float64 but are valid input
- value = np.array([], dtype=np.intp)
- try:
- # Safely convert to C-contiguous array of type np.intp
- value = value.astype(np.intp, order='C', casting='safe',
- subok=False, copy=False)
- except TypeError as e:
- raise TypeError("cannot safely cast `peaks` to dtype('intp')") from e
- if value.ndim != 1:
- raise ValueError('`peaks` must be a 1-D array')
- return value
- def _arg_wlen_as_expected(value):
- """Ensure argument `wlen` is of type `np.intp` and larger than 1.
- Used in `peak_prominences` and `peak_widths`.
- Returns
- -------
- value : np.intp
- The original `value` rounded up to an integer or -1 if `value` was
- None.
- """
- if value is None:
- # _peak_prominences expects an intp; -1 signals that no value was
- # supplied by the user
- value = -1
- elif 1 < value:
- # Round up to a positive integer
- if not np.can_cast(value, np.intp, "safe"):
- value = math.ceil(value)
- value = np.intp(value)
- else:
- raise ValueError('`wlen` must be larger than 1, was {}'
- .format(value))
- return value
- def peak_prominences(x, peaks, wlen=None):
- """
- Calculate the prominence of each peak in a signal.
- The prominence of a peak measures how much a peak stands out from the
- surrounding baseline of the signal and is defined as the vertical distance
- between the peak and its lowest contour line.
- Parameters
- ----------
- x : sequence
- A signal with peaks.
- peaks : sequence
- Indices of peaks in `x`.
- wlen : int, optional
- A window length in samples that optionally limits the evaluated area for
- each peak to a subset of `x`. The peak is always placed in the middle of
- the window therefore the given length is rounded up to the next odd
- integer. This parameter can speed up the calculation (see Notes).
- Returns
- -------
- prominences : ndarray
- The calculated prominences for each peak in `peaks`.
- left_bases, right_bases : ndarray
- The peaks' bases as indices in `x` to the left and right of each peak.
- The higher base of each pair is a peak's lowest contour line.
- Raises
- ------
- ValueError
- If a value in `peaks` is an invalid index for `x`.
- Warns
- -----
- PeakPropertyWarning
- For indices in `peaks` that don't point to valid local maxima in `x`,
- the returned prominence will be 0 and this warning is raised. This
- also happens if `wlen` is smaller than the plateau size of a peak.
- Warnings
- --------
- This function may return unexpected results for data containing NaNs. To
- avoid this, NaNs should either be removed or replaced.
- See Also
- --------
- find_peaks
- Find peaks inside a signal based on peak properties.
- peak_widths
- Calculate the width of peaks.
- Notes
- -----
- Strategy to compute a peak's prominence:
- 1. Extend a horizontal line from the current peak to the left and right
- until the line either reaches the window border (see `wlen`) or
- intersects the signal again at the slope of a higher peak. An
- intersection with a peak of the same height is ignored.
- 2. On each side find the minimal signal value within the interval defined
- above. These points are the peak's bases.
- 3. The higher one of the two bases marks the peak's lowest contour line. The
- prominence can then be calculated as the vertical difference between the
- peaks height itself and its lowest contour line.
- Searching for the peak's bases can be slow for large `x` with periodic
- behavior because large chunks or even the full signal need to be evaluated
- for the first algorithmic step. This evaluation area can be limited with the
- parameter `wlen` which restricts the algorithm to a window around the
- current peak and can shorten the calculation time if the window length is
- short in relation to `x`.
- However, this may stop the algorithm from finding the true global contour
- line if the peak's true bases are outside this window. Instead, a higher
- contour line is found within the restricted window leading to a smaller
- calculated prominence. In practice, this is only relevant for the highest set
- of peaks in `x`. This behavior may even be used intentionally to calculate
- "local" prominences.
- .. versionadded:: 1.1.0
- References
- ----------
- .. [1] Wikipedia Article for Topographic Prominence:
- https://en.wikipedia.org/wiki/Topographic_prominence
- Examples
- --------
- >>> import numpy as np
- >>> from scipy.signal import find_peaks, peak_prominences
- >>> import matplotlib.pyplot as plt
- Create a test signal with two overlayed harmonics
- >>> x = np.linspace(0, 6 * np.pi, 1000)
- >>> x = np.sin(x) + 0.6 * np.sin(2.6 * x)
- Find all peaks and calculate prominences
- >>> peaks, _ = find_peaks(x)
- >>> prominences = peak_prominences(x, peaks)[0]
- >>> prominences
- array([1.24159486, 0.47840168, 0.28470524, 3.10716793, 0.284603 ,
- 0.47822491, 2.48340261, 0.47822491])
- Calculate the height of each peak's contour line and plot the results
- >>> contour_heights = x[peaks] - prominences
- >>> plt.plot(x)
- >>> plt.plot(peaks, x[peaks], "x")
- >>> plt.vlines(x=peaks, ymin=contour_heights, ymax=x[peaks])
- >>> plt.show()
- Let's evaluate a second example that demonstrates several edge cases for
- one peak at index 5.
- >>> x = np.array([0, 1, 0, 3, 1, 3, 0, 4, 0])
- >>> peaks = np.array([5])
- >>> plt.plot(x)
- >>> plt.plot(peaks, x[peaks], "x")
- >>> plt.show()
- >>> peak_prominences(x, peaks) # -> (prominences, left_bases, right_bases)
- (array([3.]), array([2]), array([6]))
- Note how the peak at index 3 of the same height is not considered as a
- border while searching for the left base. Instead, two minima at 0 and 2
- are found in which case the one closer to the evaluated peak is always
- chosen. On the right side, however, the base must be placed at 6 because the
- higher peak represents the right border to the evaluated area.
- >>> peak_prominences(x, peaks, wlen=3.1)
- (array([2.]), array([4]), array([6]))
- Here, we restricted the algorithm to a window from 3 to 7 (the length is 5
- samples because `wlen` was rounded up to the next odd integer). Thus, the
- only two candidates in the evaluated area are the two neighboring samples
- and a smaller prominence is calculated.
- """
- x = _arg_x_as_expected(x)
- peaks = _arg_peaks_as_expected(peaks)
- wlen = _arg_wlen_as_expected(wlen)
- return _peak_prominences(x, peaks, wlen)
- def peak_widths(x, peaks, rel_height=0.5, prominence_data=None, wlen=None):
- """
- Calculate the width of each peak in a signal.
- This function calculates the width of a peak in samples at a relative
- distance to the peak's height and prominence.
- Parameters
- ----------
- x : sequence
- A signal with peaks.
- peaks : sequence
- Indices of peaks in `x`.
- rel_height : float, optional
- Chooses the relative height at which the peak width is measured as a
- percentage of its prominence. 1.0 calculates the width of the peak at
- its lowest contour line while 0.5 evaluates at half the prominence
- height. Must be at least 0. See notes for further explanation.
- prominence_data : tuple, optional
- A tuple of three arrays matching the output of `peak_prominences` when
- called with the same arguments `x` and `peaks`. This data are calculated
- internally if not provided.
- wlen : int, optional
- A window length in samples passed to `peak_prominences` as an optional
- argument for internal calculation of `prominence_data`. This argument
- is ignored if `prominence_data` is given.
- Returns
- -------
- widths : ndarray
- The widths for each peak in samples.
- width_heights : ndarray
- The height of the contour lines at which the `widths` where evaluated.
- left_ips, right_ips : ndarray
- Interpolated positions of left and right intersection points of a
- horizontal line at the respective evaluation height.
- Raises
- ------
- ValueError
- If `prominence_data` is supplied but doesn't satisfy the condition
- ``0 <= left_base <= peak <= right_base < x.shape[0]`` for each peak,
- has the wrong dtype, is not C-contiguous or does not have the same
- shape.
- Warns
- -----
- PeakPropertyWarning
- Raised if any calculated width is 0. This may stem from the supplied
- `prominence_data` or if `rel_height` is set to 0.
- Warnings
- --------
- This function may return unexpected results for data containing NaNs. To
- avoid this, NaNs should either be removed or replaced.
- See Also
- --------
- find_peaks
- Find peaks inside a signal based on peak properties.
- peak_prominences
- Calculate the prominence of peaks.
- Notes
- -----
- The basic algorithm to calculate a peak's width is as follows:
- * Calculate the evaluation height :math:`h_{eval}` with the formula
- :math:`h_{eval} = h_{Peak} - P \\cdot R`, where :math:`h_{Peak}` is the
- height of the peak itself, :math:`P` is the peak's prominence and
- :math:`R` a positive ratio specified with the argument `rel_height`.
- * Draw a horizontal line at the evaluation height to both sides, starting at
- the peak's current vertical position until the lines either intersect a
- slope, the signal border or cross the vertical position of the peak's
- base (see `peak_prominences` for an definition). For the first case,
- intersection with the signal, the true intersection point is estimated
- with linear interpolation.
- * Calculate the width as the horizontal distance between the chosen
- endpoints on both sides. As a consequence of this the maximal possible
- width for each peak is the horizontal distance between its bases.
- As shown above to calculate a peak's width its prominence and bases must be
- known. You can supply these yourself with the argument `prominence_data`.
- Otherwise, they are internally calculated (see `peak_prominences`).
- .. versionadded:: 1.1.0
- Examples
- --------
- >>> import numpy as np
- >>> from scipy.signal import chirp, find_peaks, peak_widths
- >>> import matplotlib.pyplot as plt
- Create a test signal with two overlayed harmonics
- >>> x = np.linspace(0, 6 * np.pi, 1000)
- >>> x = np.sin(x) + 0.6 * np.sin(2.6 * x)
- Find all peaks and calculate their widths at the relative height of 0.5
- (contour line at half the prominence height) and 1 (at the lowest contour
- line at full prominence height).
- >>> peaks, _ = find_peaks(x)
- >>> results_half = peak_widths(x, peaks, rel_height=0.5)
- >>> results_half[0] # widths
- array([ 64.25172825, 41.29465463, 35.46943289, 104.71586081,
- 35.46729324, 41.30429622, 181.93835853, 45.37078546])
- >>> results_full = peak_widths(x, peaks, rel_height=1)
- >>> results_full[0] # widths
- array([181.9396084 , 72.99284945, 61.28657872, 373.84622694,
- 61.78404617, 72.48822812, 253.09161876, 79.36860878])
- Plot signal, peaks and contour lines at which the widths where calculated
- >>> plt.plot(x)
- >>> plt.plot(peaks, x[peaks], "x")
- >>> plt.hlines(*results_half[1:], color="C2")
- >>> plt.hlines(*results_full[1:], color="C3")
- >>> plt.show()
- """
- x = _arg_x_as_expected(x)
- peaks = _arg_peaks_as_expected(peaks)
- if prominence_data is None:
- # Calculate prominence if not supplied and use wlen if supplied.
- wlen = _arg_wlen_as_expected(wlen)
- prominence_data = _peak_prominences(x, peaks, wlen)
- return _peak_widths(x, peaks, rel_height, *prominence_data)
- def _unpack_condition_args(interval, x, peaks):
- """
- Parse condition arguments for `find_peaks`.
- Parameters
- ----------
- interval : number or ndarray or sequence
- Either a number or ndarray or a 2-element sequence of the former. The
- first value is always interpreted as `imin` and the second, if supplied,
- as `imax`.
- x : ndarray
- The signal with `peaks`.
- peaks : ndarray
- An array with indices used to reduce `imin` and / or `imax` if those are
- arrays.
- Returns
- -------
- imin, imax : number or ndarray or None
- Minimal and maximal value in `argument`.
- Raises
- ------
- ValueError :
- If interval border is given as array and its size does not match the size
- of `x`.
- Notes
- -----
- .. versionadded:: 1.1.0
- """
- try:
- imin, imax = interval
- except (TypeError, ValueError):
- imin, imax = (interval, None)
- # Reduce arrays if arrays
- if isinstance(imin, np.ndarray):
- if imin.size != x.size:
- raise ValueError('array size of lower interval border must match x')
- imin = imin[peaks]
- if isinstance(imax, np.ndarray):
- if imax.size != x.size:
- raise ValueError('array size of upper interval border must match x')
- imax = imax[peaks]
- return imin, imax
- def _select_by_property(peak_properties, pmin, pmax):
- """
- Evaluate where the generic property of peaks confirms to an interval.
- Parameters
- ----------
- peak_properties : ndarray
- An array with properties for each peak.
- pmin : None or number or ndarray
- Lower interval boundary for `peak_properties`. ``None`` is interpreted as
- an open border.
- pmax : None or number or ndarray
- Upper interval boundary for `peak_properties`. ``None`` is interpreted as
- an open border.
- Returns
- -------
- keep : bool
- A boolean mask evaluating to true where `peak_properties` confirms to the
- interval.
- See Also
- --------
- find_peaks
- Notes
- -----
- .. versionadded:: 1.1.0
- """
- keep = np.ones(peak_properties.size, dtype=bool)
- if pmin is not None:
- keep &= (pmin <= peak_properties)
- if pmax is not None:
- keep &= (peak_properties <= pmax)
- return keep
- def _select_by_peak_threshold(x, peaks, tmin, tmax):
- """
- Evaluate which peaks fulfill the threshold condition.
- Parameters
- ----------
- x : ndarray
- A 1-D array which is indexable by `peaks`.
- peaks : ndarray
- Indices of peaks in `x`.
- tmin, tmax : scalar or ndarray or None
- Minimal and / or maximal required thresholds. If supplied as ndarrays
- their size must match `peaks`. ``None`` is interpreted as an open
- border.
- Returns
- -------
- keep : bool
- A boolean mask evaluating to true where `peaks` fulfill the threshold
- condition.
- left_thresholds, right_thresholds : ndarray
- Array matching `peak` containing the thresholds of each peak on
- both sides.
- Notes
- -----
- .. versionadded:: 1.1.0
- """
- # Stack thresholds on both sides to make min / max operations easier:
- # tmin is compared with the smaller, and tmax with the greater thresold to
- # each peak's side
- stacked_thresholds = np.vstack([x[peaks] - x[peaks - 1],
- x[peaks] - x[peaks + 1]])
- keep = np.ones(peaks.size, dtype=bool)
- if tmin is not None:
- min_thresholds = np.min(stacked_thresholds, axis=0)
- keep &= (tmin <= min_thresholds)
- if tmax is not None:
- max_thresholds = np.max(stacked_thresholds, axis=0)
- keep &= (max_thresholds <= tmax)
- return keep, stacked_thresholds[0], stacked_thresholds[1]
- def find_peaks(x, height=None, threshold=None, distance=None,
- prominence=None, width=None, wlen=None, rel_height=0.5,
- plateau_size=None):
- """
- Find peaks inside a signal based on peak properties.
- This function takes a 1-D array and finds all local maxima by
- simple comparison of neighboring values. Optionally, a subset of these
- peaks can be selected by specifying conditions for a peak's properties.
- Parameters
- ----------
- x : sequence
- A signal with peaks.
- height : number or ndarray or sequence, optional
- Required height of peaks. Either a number, ``None``, an array matching
- `x` or a 2-element sequence of the former. The first element is
- always interpreted as the minimal and the second, if supplied, as the
- maximal required height.
- threshold : number or ndarray or sequence, optional
- Required threshold of peaks, the vertical distance to its neighboring
- samples. Either a number, ``None``, an array matching `x` or a
- 2-element sequence of the former. The first element is always
- interpreted as the minimal and the second, if supplied, as the maximal
- required threshold.
- distance : number, optional
- Required minimal horizontal distance (>= 1) in samples between
- neighbouring peaks. Smaller peaks are removed first until the condition
- is fulfilled for all remaining peaks.
- prominence : number or ndarray or sequence, optional
- Required prominence of peaks. Either a number, ``None``, an array
- matching `x` or a 2-element sequence of the former. The first
- element is always interpreted as the minimal and the second, if
- supplied, as the maximal required prominence.
- width : number or ndarray or sequence, optional
- Required width of peaks in samples. Either a number, ``None``, an array
- matching `x` or a 2-element sequence of the former. The first
- element is always interpreted as the minimal and the second, if
- supplied, as the maximal required width.
- wlen : int, optional
- Used for calculation of the peaks prominences, thus it is only used if
- one of the arguments `prominence` or `width` is given. See argument
- `wlen` in `peak_prominences` for a full description of its effects.
- rel_height : float, optional
- Used for calculation of the peaks width, thus it is only used if `width`
- is given. See argument `rel_height` in `peak_widths` for a full
- description of its effects.
- plateau_size : number or ndarray or sequence, optional
- Required size of the flat top of peaks in samples. Either a number,
- ``None``, an array matching `x` or a 2-element sequence of the former.
- The first element is always interpreted as the minimal and the second,
- if supplied as the maximal required plateau size.
- .. versionadded:: 1.2.0
- Returns
- -------
- peaks : ndarray
- Indices of peaks in `x` that satisfy all given conditions.
- properties : dict
- A dictionary containing properties of the returned peaks which were
- calculated as intermediate results during evaluation of the specified
- conditions:
- * 'peak_heights'
- If `height` is given, the height of each peak in `x`.
- * 'left_thresholds', 'right_thresholds'
- If `threshold` is given, these keys contain a peaks vertical
- distance to its neighbouring samples.
- * 'prominences', 'right_bases', 'left_bases'
- If `prominence` is given, these keys are accessible. See
- `peak_prominences` for a description of their content.
- * 'width_heights', 'left_ips', 'right_ips'
- If `width` is given, these keys are accessible. See `peak_widths`
- for a description of their content.
- * 'plateau_sizes', left_edges', 'right_edges'
- If `plateau_size` is given, these keys are accessible and contain
- the indices of a peak's edges (edges are still part of the
- plateau) and the calculated plateau sizes.
- .. versionadded:: 1.2.0
- To calculate and return properties without excluding peaks, provide the
- open interval ``(None, None)`` as a value to the appropriate argument
- (excluding `distance`).
- Warns
- -----
- PeakPropertyWarning
- Raised if a peak's properties have unexpected values (see
- `peak_prominences` and `peak_widths`).
- Warnings
- --------
- This function may return unexpected results for data containing NaNs. To
- avoid this, NaNs should either be removed or replaced.
- See Also
- --------
- find_peaks_cwt
- Find peaks using the wavelet transformation.
- peak_prominences
- Directly calculate the prominence of peaks.
- peak_widths
- Directly calculate the width of peaks.
- Notes
- -----
- In the context of this function, a peak or local maximum is defined as any
- sample whose two direct neighbours have a smaller amplitude. For flat peaks
- (more than one sample of equal amplitude wide) the index of the middle
- sample is returned (rounded down in case the number of samples is even).
- For noisy signals the peak locations can be off because the noise might
- change the position of local maxima. In those cases consider smoothing the
- signal before searching for peaks or use other peak finding and fitting
- methods (like `find_peaks_cwt`).
- Some additional comments on specifying conditions:
- * Almost all conditions (excluding `distance`) can be given as half-open or
- closed intervals, e.g., ``1`` or ``(1, None)`` defines the half-open
- interval :math:`[1, \\infty]` while ``(None, 1)`` defines the interval
- :math:`[-\\infty, 1]`. The open interval ``(None, None)`` can be specified
- as well, which returns the matching properties without exclusion of peaks.
- * The border is always included in the interval used to select valid peaks.
- * For several conditions the interval borders can be specified with
- arrays matching `x` in shape which enables dynamic constrains based on
- the sample position.
- * The conditions are evaluated in the following order: `plateau_size`,
- `height`, `threshold`, `distance`, `prominence`, `width`. In most cases
- this order is the fastest one because faster operations are applied first
- to reduce the number of peaks that need to be evaluated later.
- * While indices in `peaks` are guaranteed to be at least `distance` samples
- apart, edges of flat peaks may be closer than the allowed `distance`.
- * Use `wlen` to reduce the time it takes to evaluate the conditions for
- `prominence` or `width` if `x` is large or has many local maxima
- (see `peak_prominences`).
- .. versionadded:: 1.1.0
- Examples
- --------
- To demonstrate this function's usage we use a signal `x` supplied with
- SciPy (see `scipy.datasets.electrocardiogram`). Let's find all peaks (local
- maxima) in `x` whose amplitude lies above 0.
- >>> import numpy as np
- >>> import matplotlib.pyplot as plt
- >>> from scipy.datasets import electrocardiogram
- >>> from scipy.signal import find_peaks
- >>> x = electrocardiogram()[2000:4000]
- >>> peaks, _ = find_peaks(x, height=0)
- >>> plt.plot(x)
- >>> plt.plot(peaks, x[peaks], "x")
- >>> plt.plot(np.zeros_like(x), "--", color="gray")
- >>> plt.show()
- We can select peaks below 0 with ``height=(None, 0)`` or use arrays matching
- `x` in size to reflect a changing condition for different parts of the
- signal.
- >>> border = np.sin(np.linspace(0, 3 * np.pi, x.size))
- >>> peaks, _ = find_peaks(x, height=(-border, border))
- >>> plt.plot(x)
- >>> plt.plot(-border, "--", color="gray")
- >>> plt.plot(border, ":", color="gray")
- >>> plt.plot(peaks, x[peaks], "x")
- >>> plt.show()
- Another useful condition for periodic signals can be given with the
- `distance` argument. In this case, we can easily select the positions of
- QRS complexes within the electrocardiogram (ECG) by demanding a distance of
- at least 150 samples.
- >>> peaks, _ = find_peaks(x, distance=150)
- >>> np.diff(peaks)
- array([186, 180, 177, 171, 177, 169, 167, 164, 158, 162, 172])
- >>> plt.plot(x)
- >>> plt.plot(peaks, x[peaks], "x")
- >>> plt.show()
- Especially for noisy signals peaks can be easily grouped by their
- prominence (see `peak_prominences`). E.g., we can select all peaks except
- for the mentioned QRS complexes by limiting the allowed prominence to 0.6.
- >>> peaks, properties = find_peaks(x, prominence=(None, 0.6))
- >>> properties["prominences"].max()
- 0.5049999999999999
- >>> plt.plot(x)
- >>> plt.plot(peaks, x[peaks], "x")
- >>> plt.show()
- And, finally, let's examine a different section of the ECG which contains
- beat forms of different shape. To select only the atypical heart beats, we
- combine two conditions: a minimal prominence of 1 and width of at least 20
- samples.
- >>> x = electrocardiogram()[17000:18000]
- >>> peaks, properties = find_peaks(x, prominence=1, width=20)
- >>> properties["prominences"], properties["widths"]
- (array([1.495, 2.3 ]), array([36.93773946, 39.32723577]))
- >>> plt.plot(x)
- >>> plt.plot(peaks, x[peaks], "x")
- >>> plt.vlines(x=peaks, ymin=x[peaks] - properties["prominences"],
- ... ymax = x[peaks], color = "C1")
- >>> plt.hlines(y=properties["width_heights"], xmin=properties["left_ips"],
- ... xmax=properties["right_ips"], color = "C1")
- >>> plt.show()
- """
- # _argmaxima1d expects array of dtype 'float64'
- x = _arg_x_as_expected(x)
- if distance is not None and distance < 1:
- raise ValueError('`distance` must be greater or equal to 1')
- peaks, left_edges, right_edges = _local_maxima_1d(x)
- properties = {}
- if plateau_size is not None:
- # Evaluate plateau size
- plateau_sizes = right_edges - left_edges + 1
- pmin, pmax = _unpack_condition_args(plateau_size, x, peaks)
- keep = _select_by_property(plateau_sizes, pmin, pmax)
- peaks = peaks[keep]
- properties["plateau_sizes"] = plateau_sizes
- properties["left_edges"] = left_edges
- properties["right_edges"] = right_edges
- properties = {key: array[keep] for key, array in properties.items()}
- if height is not None:
- # Evaluate height condition
- peak_heights = x[peaks]
- hmin, hmax = _unpack_condition_args(height, x, peaks)
- keep = _select_by_property(peak_heights, hmin, hmax)
- peaks = peaks[keep]
- properties["peak_heights"] = peak_heights
- properties = {key: array[keep] for key, array in properties.items()}
- if threshold is not None:
- # Evaluate threshold condition
- tmin, tmax = _unpack_condition_args(threshold, x, peaks)
- keep, left_thresholds, right_thresholds = _select_by_peak_threshold(
- x, peaks, tmin, tmax)
- peaks = peaks[keep]
- properties["left_thresholds"] = left_thresholds
- properties["right_thresholds"] = right_thresholds
- properties = {key: array[keep] for key, array in properties.items()}
- if distance is not None:
- # Evaluate distance condition
- keep = _select_by_peak_distance(peaks, x[peaks], distance)
- peaks = peaks[keep]
- properties = {key: array[keep] for key, array in properties.items()}
- if prominence is not None or width is not None:
- # Calculate prominence (required for both conditions)
- wlen = _arg_wlen_as_expected(wlen)
- properties.update(zip(
- ['prominences', 'left_bases', 'right_bases'],
- _peak_prominences(x, peaks, wlen=wlen)
- ))
- if prominence is not None:
- # Evaluate prominence condition
- pmin, pmax = _unpack_condition_args(prominence, x, peaks)
- keep = _select_by_property(properties['prominences'], pmin, pmax)
- peaks = peaks[keep]
- properties = {key: array[keep] for key, array in properties.items()}
- if width is not None:
- # Calculate widths
- properties.update(zip(
- ['widths', 'width_heights', 'left_ips', 'right_ips'],
- _peak_widths(x, peaks, rel_height, properties['prominences'],
- properties['left_bases'], properties['right_bases'])
- ))
- # Evaluate width condition
- wmin, wmax = _unpack_condition_args(width, x, peaks)
- keep = _select_by_property(properties['widths'], wmin, wmax)
- peaks = peaks[keep]
- properties = {key: array[keep] for key, array in properties.items()}
- return peaks, properties
- def _identify_ridge_lines(matr, max_distances, gap_thresh):
- """
- Identify ridges in the 2-D matrix.
- Expect that the width of the wavelet feature increases with increasing row
- number.
- Parameters
- ----------
- matr : 2-D ndarray
- Matrix in which to identify ridge lines.
- max_distances : 1-D sequence
- At each row, a ridge line is only connected
- if the relative max at row[n] is within
- `max_distances`[n] from the relative max at row[n+1].
- gap_thresh : int
- If a relative maximum is not found within `max_distances`,
- there will be a gap. A ridge line is discontinued if
- there are more than `gap_thresh` points without connecting
- a new relative maximum.
- Returns
- -------
- ridge_lines : tuple
- Tuple of 2 1-D sequences. `ridge_lines`[ii][0] are the rows of the
- ii-th ridge-line, `ridge_lines`[ii][1] are the columns. Empty if none
- found. Each ridge-line will be sorted by row (increasing), but the
- order of the ridge lines is not specified.
- References
- ----------
- .. [1] Bioinformatics (2006) 22 (17): 2059-2065.
- :doi:`10.1093/bioinformatics/btl355`
- Examples
- --------
- >>> import numpy as np
- >>> rng = np.random.default_rng()
- >>> data = rng.random((5,5))
- >>> max_dist = 3
- >>> max_distances = np.full(20, max_dist)
- >>> ridge_lines = _identify_ridge_lines(data, max_distances, 1)
- Notes
- -----
- This function is intended to be used in conjunction with `cwt`
- as part of `find_peaks_cwt`.
- """
- if len(max_distances) < matr.shape[0]:
- raise ValueError('Max_distances must have at least as many rows '
- 'as matr')
- all_max_cols = _boolrelextrema(matr, np.greater, axis=1, order=1)
- # Highest row for which there are any relative maxima
- has_relmax = np.nonzero(all_max_cols.any(axis=1))[0]
- if len(has_relmax) == 0:
- return []
- start_row = has_relmax[-1]
- # Each ridge line is a 3-tuple:
- # rows, cols,Gap number
- ridge_lines = [[[start_row],
- [col],
- 0] for col in np.nonzero(all_max_cols[start_row])[0]]
- final_lines = []
- rows = np.arange(start_row - 1, -1, -1)
- cols = np.arange(0, matr.shape[1])
- for row in rows:
- this_max_cols = cols[all_max_cols[row]]
- # Increment gap number of each line,
- # set it to zero later if appropriate
- for line in ridge_lines:
- line[2] += 1
- # XXX These should always be all_max_cols[row]
- # But the order might be different. Might be an efficiency gain
- # to make sure the order is the same and avoid this iteration
- prev_ridge_cols = np.array([line[1][-1] for line in ridge_lines])
- # Look through every relative maximum found at current row
- # Attempt to connect them with existing ridge lines.
- for ind, col in enumerate(this_max_cols):
- # If there is a previous ridge line within
- # the max_distance to connect to, do so.
- # Otherwise start a new one.
- line = None
- if len(prev_ridge_cols) > 0:
- diffs = np.abs(col - prev_ridge_cols)
- closest = np.argmin(diffs)
- if diffs[closest] <= max_distances[row]:
- line = ridge_lines[closest]
- if line is not None:
- # Found a point close enough, extend current ridge line
- line[1].append(col)
- line[0].append(row)
- line[2] = 0
- else:
- new_line = [[row],
- [col],
- 0]
- ridge_lines.append(new_line)
- # Remove the ridge lines with gap_number too high
- # XXX Modifying a list while iterating over it.
- # Should be safe, since we iterate backwards, but
- # still tacky.
- for ind in range(len(ridge_lines) - 1, -1, -1):
- line = ridge_lines[ind]
- if line[2] > gap_thresh:
- final_lines.append(line)
- del ridge_lines[ind]
- out_lines = []
- for line in (final_lines + ridge_lines):
- sortargs = np.array(np.argsort(line[0]))
- rows, cols = np.zeros_like(sortargs), np.zeros_like(sortargs)
- rows[sortargs] = line[0]
- cols[sortargs] = line[1]
- out_lines.append([rows, cols])
- return out_lines
- def _filter_ridge_lines(cwt, ridge_lines, window_size=None, min_length=None,
- min_snr=1, noise_perc=10):
- """
- Filter ridge lines according to prescribed criteria. Intended
- to be used for finding relative maxima.
- Parameters
- ----------
- cwt : 2-D ndarray
- Continuous wavelet transform from which the `ridge_lines` were defined.
- ridge_lines : 1-D sequence
- Each element should contain 2 sequences, the rows and columns
- of the ridge line (respectively).
- window_size : int, optional
- Size of window to use to calculate noise floor.
- Default is ``cwt.shape[1] / 20``.
- min_length : int, optional
- Minimum length a ridge line needs to be acceptable.
- Default is ``cwt.shape[0] / 4``, ie 1/4-th the number of widths.
- min_snr : float, optional
- Minimum SNR ratio. Default 1. The signal is the value of
- the cwt matrix at the shortest length scale (``cwt[0, loc]``), the
- noise is the `noise_perc`th percentile of datapoints contained within a
- window of `window_size` around ``cwt[0, loc]``.
- noise_perc : float, optional
- When calculating the noise floor, percentile of data points
- examined below which to consider noise. Calculated using
- scipy.stats.scoreatpercentile.
- References
- ----------
- .. [1] Bioinformatics (2006) 22 (17): 2059-2065.
- :doi:`10.1093/bioinformatics/btl355`
- """
- num_points = cwt.shape[1]
- if min_length is None:
- min_length = np.ceil(cwt.shape[0] / 4)
- if window_size is None:
- window_size = np.ceil(num_points / 20)
- window_size = int(window_size)
- hf_window, odd = divmod(window_size, 2)
- # Filter based on SNR
- row_one = cwt[0, :]
- noises = np.empty_like(row_one)
- for ind, val in enumerate(row_one):
- window_start = max(ind - hf_window, 0)
- window_end = min(ind + hf_window + odd, num_points)
- noises[ind] = scoreatpercentile(row_one[window_start:window_end],
- per=noise_perc)
- def filt_func(line):
- if len(line[0]) < min_length:
- return False
- snr = abs(cwt[line[0][0], line[1][0]] / noises[line[1][0]])
- if snr < min_snr:
- return False
- return True
- return list(filter(filt_func, ridge_lines))
- def find_peaks_cwt(vector, widths, wavelet=None, max_distances=None,
- gap_thresh=None, min_length=None,
- min_snr=1, noise_perc=10, window_size=None):
- """
- Find peaks in a 1-D array with wavelet transformation.
- The general approach is to smooth `vector` by convolving it with
- `wavelet(width)` for each width in `widths`. Relative maxima which
- appear at enough length scales, and with sufficiently high SNR, are
- accepted.
- Parameters
- ----------
- vector : ndarray
- 1-D array in which to find the peaks.
- widths : float or sequence
- Single width or 1-D array-like of widths to use for calculating
- the CWT matrix. In general,
- this range should cover the expected width of peaks of interest.
- wavelet : callable, optional
- Should take two parameters and return a 1-D array to convolve
- with `vector`. The first parameter determines the number of points
- of the returned wavelet array, the second parameter is the scale
- (`width`) of the wavelet. Should be normalized and symmetric.
- Default is the ricker wavelet.
- max_distances : ndarray, optional
- At each row, a ridge line is only connected if the relative max at
- row[n] is within ``max_distances[n]`` from the relative max at
- ``row[n+1]``. Default value is ``widths/4``.
- gap_thresh : float, optional
- If a relative maximum is not found within `max_distances`,
- there will be a gap. A ridge line is discontinued if there are more
- than `gap_thresh` points without connecting a new relative maximum.
- Default is the first value of the widths array i.e. widths[0].
- min_length : int, optional
- Minimum length a ridge line needs to be acceptable.
- Default is ``cwt.shape[0] / 4``, ie 1/4-th the number of widths.
- min_snr : float, optional
- Minimum SNR ratio. Default 1. The signal is the maximum CWT coefficient
- on the largest ridge line. The noise is `noise_perc` th percentile of
- datapoints contained within the same ridge line.
- noise_perc : float, optional
- When calculating the noise floor, percentile of data points
- examined below which to consider noise. Calculated using
- `stats.scoreatpercentile`. Default is 10.
- window_size : int, optional
- Size of window to use to calculate noise floor.
- Default is ``cwt.shape[1] / 20``.
- Returns
- -------
- peaks_indices : ndarray
- Indices of the locations in the `vector` where peaks were found.
- The list is sorted.
- See Also
- --------
- cwt
- Continuous wavelet transform.
- find_peaks
- Find peaks inside a signal based on peak properties.
- Notes
- -----
- This approach was designed for finding sharp peaks among noisy data,
- however with proper parameter selection it should function well for
- different peak shapes.
- The algorithm is as follows:
- 1. Perform a continuous wavelet transform on `vector`, for the supplied
- `widths`. This is a convolution of `vector` with `wavelet(width)` for
- each width in `widths`. See `cwt`.
- 2. Identify "ridge lines" in the cwt matrix. These are relative maxima
- at each row, connected across adjacent rows. See identify_ridge_lines
- 3. Filter the ridge_lines using filter_ridge_lines.
- .. versionadded:: 0.11.0
- References
- ----------
- .. [1] Bioinformatics (2006) 22 (17): 2059-2065.
- :doi:`10.1093/bioinformatics/btl355`
- Examples
- --------
- >>> import numpy as np
- >>> from scipy import signal
- >>> xs = np.arange(0, np.pi, 0.05)
- >>> data = np.sin(xs)
- >>> peakind = signal.find_peaks_cwt(data, np.arange(1,10))
- >>> peakind, xs[peakind], data[peakind]
- ([32], array([ 1.6]), array([ 0.9995736]))
- """
- widths = np.array(widths, copy=False, ndmin=1)
- if gap_thresh is None:
- gap_thresh = np.ceil(widths[0])
- if max_distances is None:
- max_distances = widths / 4.0
- if wavelet is None:
- wavelet = ricker
- cwt_dat = cwt(vector, wavelet, widths)
- ridge_lines = _identify_ridge_lines(cwt_dat, max_distances, gap_thresh)
- filtered = _filter_ridge_lines(cwt_dat, ridge_lines, min_length=min_length,
- window_size=window_size, min_snr=min_snr,
- noise_perc=noise_perc)
- max_locs = np.asarray([x[1][0] for x in filtered])
- max_locs.sort()
- return max_locs
|