Repository: scipy/scipy

Issue:
 I'd like to add a peak finding function to scipy.signal
Hello,

### TL;DR

I have created a [peak finding and filtering algorithm](http://nbviewer.jupyter.org/urls/gitlab.com/lagru/peakfinder-demo/raw/master/peak_finder_demo.ipynb) not unlike Matlab's [findpeaks](https://de.mathworks.com/help/signal/ref/findpeaks.html) function and would like to know if you think it worth adding to SciPy's signal module.


### Long form

It has bugged me for some time that SciPy and other scientific Python libraries lack a function for simple peak finding and filtering like Matlab's findpeaks function.

So I have come up with a solution for one of my own projects. I'm quite happy with my approach and thought it might be worth a try to find out if you would be interested in adding this in one form or another to SciPy's signal module. I have uploaded a [Jupyter notebook](http://nbviewer.jupyter.org/urls/gitlab.com/lagru/peakfinder-demo/raw/master/peak_finder_demo.ipynb) (source code [here](https://gitlab.com/lagru/peakfinder-demo/blob/master/peak_finder.py)) so you guys can see for yourself.

In my opinion the use case (simple peak finding and filtering) is currently not very well covered by SciPy and its find_peaks_cwt function. I think it would fit well into SciPy's signal module and its potential usefulness in many domains is underlined by Matlab's implementation which got extended and improved over the years.
Therefore I'd be happy to create a pull request with my work or help with something related to that.

But before I start working I'd like to know if this is actually wanted. If so, there is one more thing to discuss. I have written the algorithm as a class (as you can see in the example) because it allows a greater introspection of intermediate calculation results (e.g. the peak prominence) and a greater flexibility. However the class-based approach is rarely used througout SciPy so I'd like to know if you guys would prefer to wrap the peak finding and filtering in a single "simple" function.

I am pretty new to contributing to an open source project and I'm not claiming to be an expert in anything yet 😉 so I'd be grateful for constructive feedback in any form and color. Tips and improvements to the code itself are, of course, welcome!


### Worth noting

I haven't been able to find a discussion of this elsewhere apart from this related issue #7393. It seems like there is a general interest but not any concrete work done yet.

There are already some solutions and alternatives for peak finding out there:

* [peakutils](http://pythonhosted.org/PeakUtils/) - Python library with useful tools for peak fitting but lacks filtering functions
* [find_peaks_cwt](https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.find_peaks_cwt.html) - Uses wavelet transform for peak detection, not very intuitive
* [detect_peaks](http://nbviewer.jupyter.org/github/demotu/BMC/blob/master/notebooks/DetectPeaks.ipynb) by Marcos Duarte - simple but not integrated into a library, lacks arguments to filter by shape and seems to be a little bit slower compared to my approach

(all mentioned in [this](https://blog.ytotech.com/2015/11/01/findpeaks-in-python/) blog post)

However compared to Matlab's solution these are either more complicated to use or lack basic features like peak shape filtering or integration into a common library.


Discussion:
I think it would be a great addition to ``scipy.signal``!

The current implementation, ``find_peaks_cwt``, is kind of confusing. The time I was in need of finding peaks I end up using "detec_peaks" (by Marcos Duarte), but it would be great to have a better implementation integrated to SciPy.
I'm happy to hear that.

Concerning my second question: would you prefer this algorithm to be implemented as a single class or as a single / multiple functions? In my opinion there are pros and cons for both approaches:

As a class:
- Introspection and interactive data exploration is easier as intermediate results can be stored as class members
- Higher control over workflow of algorithm: order of filtering can be changed, repeated filtering with new criteria is possible
- Classmethod can act as replacement for the single-function-implementation (see `get()` method in example)
- Testing is easier (each method can be evaluated as a unit)

As one function
-  Use case is simpler but input grows more complex (all criteria need to be specified at the beginning)
- Cluttered output if more than peak locations are desired (e.g. peak statistics: prominence, width, height ...)
- If peak statistics are desired as output do I calculate everything all the time or only if used by filters?
- Matches other stuff in SciPy (I can't find many algorithms that are implemented as classes)

In my opinion the class-based approach has more advantages and the only real disadvantage is that it doesn't match the classic approach in SciPy. Would you agree?

Edit: "advantage" -> "disadvantage"
I agree with you.

Many SciPy functions doesn't use a class structure but there are exceptions (e.g. ``scipy.integrate.ode``). And  I like yours more "Pythonic" implementation
and I do not see any reason for not using it.  The class structure allows a nice workflow and I 
think I would prefer to use it than a matlab-like ``find_peaks`` function.



That sounds nice indeed. For what its worth, you can do the same with a Python dictionary if the only thing needed is to have a container. Then all class- and static- methods become simple functions. 

I think it is an art to decide whether classes are the way to go or not. If the interface is not complicated then simple dictionaries might be preferable in the sense that 
```
mypeakfinder = Peaks()
mypeakfinder.data = myarray[3, :]
mypeakfinder.prefilters('savgol')
mypeakfinder.findpeaks()
z = mypeakfinder.peakvals
```
seems a bit strange. I am definitely not saying that this is what it would look like but I guess it is better to judge by the actual complexity instead of deciding prematurely.

Do you have a rough sketch of the general API; what the options would be, how the signature looks like and so on? It would save time and effort once that is settled instead of discussing afterwards and realizing a problem.
Hi @ilayn he have posted the code [here](https://gitlab.com/lagru/peakfinder-demo/blob/master/peak_finder.py).
Oh sorry, the browser messed up the link colors. I missed them completely. I'll check
That's a really nice example. Looks good to me :-) Let's wait what `scipy.signal` residents say. 
Here are my thoughts on the API design:
 * Having multiple `filter_xx` methods isn't needed if you have a way to compute peak statistics and a generic way to filter.
 * Plotting is likely out of scope for a scipy API.
 * State management (removing peaks, restoring them, etc) is likely out of scope as well.

Basically, I'd recommend providing the bare minimum API, making sure that whatever you return is easily compatible with the standard tools. For instance, it might look like:
```python
peaks = scipy.signal.find_peaks(myarray)
peaks.centers # 1-d array
peaks.valleys  # 1-d array
heights = myarray[peaks.centers]  # or np.interp1d(myarray, peaks.centers), or just peaks.heights()
peaks.widths()
peaks.prominences()
```

The returned `peaks` object can be a custom class that knows how do compute prominences and widths and such, which are all plain numpy arrays. This gives the user all the pieces, which they can choose to assemble as needed.
My 2c -- I'd prefer a function to a class. To avoid the return bloat, you can yield a `NamedTuple` or even a dict. I'd leave prefiltering and so forth to other `scipy.signal` functions.
@perimosocordiae 

> Plotting is likely out of scope for a scipy API.

I agree. Although it would be useful, I only included it for the demo.

> Having multiple filter_xx methods isn't needed if you have a way to compute peak statistics and a generic way to filter. [...] State management (removing peaks, restoring them, etc) is likely out of scope as well.

You have a point there and I especially like your alternative suggestion. One way to remove those filter functions and the need for state management would be to make the returned object indexable not unlike a classic `numpy.ndarray`. That way one would give the user all the tools needed. 

Example:

```python
>>> peaks = scipy.signal.PeakFinder(vector)  # whether find_peaks or PeakFinder shouldn't matter
>>> peaks.indexes  # return positions of peaks in vector
>>> peaks.prominences  # return prominences of peaks
>>> peaks = peaks[peaks.prominences > 1.2]  # filter by prominences
>>> peaks = peaks[peaks.height < 3]  # filter by absolute height
```

* Basically `PeakFinder` would accept boolean arrays (and maybe index-arrays) and return a subset of itself with the matching peaks.
* Peak stats (like prominence, width, ...) would only be computed if the user tried to access the attribute.
* This approach could store expensive intermediate results and wouldn't need to compute the same thing more than once (e.g. calculating width requires calculating prominences).
* The only case where this approach wouldn't work ist filtering by distance because that requires an iterative approach where peaks are compared with one another largest to smallest.

@larsoner 

> I'd leave prefiltering and so forth to other scipy.signal functions.

What do you mean by prefiltering? The input vector is not filtered in any way that is not required to compute all included "raw" peak positions (see function [extrema](https://gitlab.com/lagru/peakfinder-demo/blob/master/peak_finder.py#L430)). Filtering of the _peaks_ is only done after all peaks are found.

> I'd prefer a function to a class. To avoid the return bloat, you can yield a NamedTuple or even a dict. 

Why? I agree that one could return a stats object like so:

```python
>>> positions, stats = scipy.signal.find_peaks(vec, ...)
>>> stats.prominences  # or stats["prominences"]
```

But how would we determine what to include in `stats`? Do we require the user to pass an argument telling us what to compute? Because computing everything all the time when not required would make the function unnecessary slow (computing widths, prominences can be quite expensive for a large number of peaks). One could decide to only return things like prominences if those had to be calculated for filtering any way but what if a user would want those stats without filtering by them?
If would find this behavior much more confusing and difficult to describe.





> What do you mean by prefiltering? 

I was referring to the `mypeakfinder.prefilters` stuff @ilayn posted. 

> > I'd prefer a function to a class. To avoid the return bloat, you can yield a NamedTuple or even a dict.

> Why?

Your post above has a good start on some of the advantages. As you already mention, it's more consistent with how things are done in `scipy.signal`. But to that I'll add that returning standard Python / NumPy objects often simplifies downstream operations (e.g., saving with `np.savez`), the barriers for entry and access are (sometimes) lower, and so forth.

> But how would we determine what to include in stats? Do we require the user to pass an argument telling us what to compute? Because computing everything all the time when not required would make the function unnecessary slow (computing widths, prominences can be quite expensive for a large number of peaks).

Many interfaces in SciPy work via such arguments, yes. If we can cover 95% of typical use cases easily using a function with a simple interface (always return peaks, return widths if they are provided by the algorithm else None, and return prominences if asked for), that sounds better to me than implementing a class.

For example, in the context of keeping things simple yet extensible, I don't see much value in supporting peak filtering as an option directly in SciPy, at least based on the [GitLab linked code](https://gitlab.com/lagru/peakfinder-demo/blob/master/peak_finder.py). It appears to subselect found peaks using a few options for boolean masks, each computed in a handful of lines. However, in general there are many potentially useful ways to do this sort of thing (combining criteria, robust threshold choosing, etc.), and the NumPy code to do the selection step appears simple. It would be better to show users how to do this sort of filtering in examples (e.g., you now have the peaks, widths, and prominences, here is how you can subselect/filter peaks using these quantities in a few lines!) rather than support these with SciPy wrappers.
Ah, so basically the new function would be responsible for finding peak positions and potentially computing additional stats for those found peaks:

```python
peaks, prominences, widths = scipy.signal.find_peaks(vec, proms=True, widths_at="halfprom")
```
The problem here is that for noisy signals computing prominences / widths would be quite expensive without limiting the peak number by filtering with cheaper criteria first.

Or another more verbose / extensible approach which could avoid the problem above:

```python
minima, maxima = scipy.signal.extrema(vec)
maxima = scipy.signal.minimal_peak_distance(vec, maxima, min_=200)
prominences = scipy.signal.peak_prominence(vec, maxima)
widths = scipy.signal.peak_width(vec, maxima, at="halfprom")  
# prominences computed again if not provided
```

I can see how would simplify the the implementation and still provide the user with the tools for calculating peak prominence, etc.

However that would make the main use case peak detection and filtering (only a subset of all local maxima are wanted peaks) more complicated and we'd have to provide extensive examples to even showcase how to filter peaks. And we would still have to provide a function for filtering by minimal peak distance as we can't expect every user to implement this himself.  I'm not convinced this sacrifice would be worth it "just" to avoid using a class.

I think you guys have way more experience in judging these things and you are the ones that have to accept and review the potential pull request. All suggested APIs would be "easy" to implement with the code that is provided in the demo.
So how do you usually go about deciding this? And what is the opinion of the majority?
> One way to remove those filter functions and the need for state management would be to make the returned object indexable not unlike a classic numpy.ndarray. That way one would give the user all the tools needed.
> - Basically PeakFinder would accept boolean arrays (and maybe index-arrays) and return a subset of itself with the matching peaks.
> - Peak stats (like prominence, width, ...) would only be computed if the user tried to access the attribute.
> - This approach could store expensive intermediate results and wouldn't need to compute the same thing more than once (e.g. calculating width requires calculating prominences).
> - The only case where this approach wouldn't work ist filtering by distance because that requires an iterative approach where peaks are compared with one another largest to smallest.

@lagru this seems like an elegant implementation for me. And it seems a much better approach than the two others you proposed latter trying to use only numpy arrays.

My general feeling is that having a implementation without using standard python objects may be reasonable if it result in a much easier usage and cleaner implementation.
> Or another more verbose / extensible approach which could avoid the problem above: [snip: function-based version]

This looks reasonable start to me. I don't see a need for `minimal_peak_distance`, though.

> However that would make the main use case peak detection and filtering (only a subset of all local maxima are wanted peaks) more complicated and we'd have to provide extensive examples to even showcase how to filter peaks. And we would still have to provide a function for filtering by minimal peak distance as we can't expect every user to implement this himself. 

I disagree here. I think that peak filtering is something we can and should expect users to be able to do.
We can say something like:
```
To filter peaks based on some minimum distance criteria, you can do for example::

    >>> maxima = maxima[np.concatenate(([True], np.diff(maxima) >= 200))`
```
This doesn't seem too onerous. Again users can want to index and subselect in all sort of ways. I'd rather teach them the general principle of how to do it -- especially if it only takes a handful (or one!) line -- than try to implement every option in SciPy.

> My general feeling is that having a implementation without using standard python objects may be reasonable if it result in a much easier usage and cleaner implementation.

I agree with this sentiment, but I don't think it applies here. I think that the usage is arguably simpler in the function case because it operates on standard objects (that incur no "how do I use this thing" overhead).

In the class-based indexing proposal, for example, having a class that says "you can index me like a NumPy array!" but isn't actually a NumPy array will cause more problems than it solves. You will likely not support every way one expects to operate on NumPy arrays (slicting, boolean indexing, expansion of dimensions, resizing, recasting to types, etc.) so now you have a partial implementation with some unexpected surprises during use. Users find out what works and what doesn't by trial and error. You have lost the ability to save it with `np.save` or `h5py` or any other standard utility.

> So how do you usually go about deciding this? And what is the opinion of the majority?

Ideally people will become convinced one way or the other and we can reach a consensus.
> We can say something like: [code snippet: `>>> maxima = ...`]

If you look at the [implementation of distance filtering](https://gitlab.com/lagru/peakfinder-demo/blob/master/peak_finder.py#L151) you'll notice that in contrast to your one line version the peak elimination order is depending on their amplitude. If two peaks are to close to another the lower one is removed. That's actually one of the more useful filter options (e.g. noisy biosignals like an EKG where  a certain peak periodicity is expected). And while one doesn't need to invoke black magic to implement that I didn't find it trivial either.

>[...] now you have a partial implementation with some unexpected surprises [...] "you can index me like a NumPy array!" but isn't actually a NumPy array!" [...]

If we would use an extra method perhaps named `.filter(index_arg)` instead of Pythons `__getitem__` it would be clear that the returned class is not in any way an array. That way we would clearly differentiate the class from a numpy array while enabling the user and keeping the extensibility.

> You have lost the ability to save it with np.save or h5py or any other standard utility.

Why? Sure we can't save the direct class itself with those functions but we can save all relevant results (peak positions, prominence, ...). I would see the class more as a fancy function with state management and don't really see the use case as common where a user wants to save a `PeakFinder` object to later reload and resume usage of that class. 

----

Maybe an API that all could agree on would look like this:

* `minima, maxima = signal.extrema(vec)` - returns all (unfiltered) positions of extreme points in `vec`
* `proms = signal.peak_prominences(peaks, vec)` - computes prominences of provided local maxima
* `widths = signal.peak_widths(peaks, vec, at="halfprom")` - calculates with at a certain height relative to peak height or prominence (no one-liner)
* `peaks = signal.filter_by_peak_distance(peaks, dmin=200)` - as described above, I don't like the name but can't think of a better one

These functions would be self contained and part of the official API. Compared to the above functions threshold and height filtering are trivial so we could expect the user to do that himself. 

Now the reason for this entire issue was to provide the user with a convenient solution equivalent to Matlab's findpeaks function and other implemenations. So I would propose that we wrap the above functions together with another function: 
`peaks, stats = signal.find_peaks(vec, ..., stats=True)`. 
If `stats=True`, intermediate results that had to be calculated anyway for filtering (prominences, widths, ...) would be returned as well.

----

This would:
* enable peak finding & filtering in one line in Python making it convenient
* provide the user with all the moving parts for data exploration and more complex use cases that aren't covered by `find_peaks`.
* keep unit tests relatively simple because no state management needs to be covered.
* keep the API consistent.

Also, `find_peaks` could be implemented as a class  as detailed in the above discussion which I think would be the more pythonic and convenient solution. But considering the context I think the proposed solution above fits the scope of SciPy better.
>  having a class that says "you can index me like a NumPy array!" but isn't actually a NumPy array will cause more problems than it solves. You will likely not support every way one expects to operate on NumPy arrays (slicting, boolean indexing, expansion of dimensions, resizing, recasting to types, etc.) so now you have a partial implementation with some unexpected surprises during use. 

I agree with you there @larsoner . I have not though about this.

-----------------------------------

> If you look at the implementation of distance filtering you'll notice that in contrast to your one line version the peak elimination order is depending on their amplitude. If two peaks are to close to another the lower one is removed. That's actually one of the more useful filter options (e.g. noisy biosignals like an EKG where a certain peak periodicity is expected). And while one doesn't need to invoke black magic to implement that I didn't find it trivial either.

In this case I think @lagru have a good point. I would like to have this already implemented. It is not so trivial to implement and, besides that, it is needed often when dealing with signal processing of noisy signals.

 -------------------

I kind of disagree with the others that you should let the user implement filtering options using numpy operations. This options are not so trivial to implement and since they are used frequently it would be nice to have them implemented in SciPy. Matlab does gives these options to the user and I think it does a great job with its ``findpeaks`` function.

Hence I think it would be good to include more options to ``find_peaks`` function you propose. Something like you have implemented in your current ``__init__`` function:
```
peaks, stats = signal.find_peaks(vec,  filter_by_weight=(min, max), filter_by_threshold=tmin, filter_by_distance=dmin, filter_by_proeminence=(pmin, pmax, wlen), filter_by_width=(wmin, wmax, wheight), return_width=True, return_proeminence=False)
```

I do not see any harm in include filtering options  like ``filter_by_weight`` or ``filter_by_distance``. If the user do not want to use them he can just left this options untouched. It will not warm his ability of using custom filters on the return data neither.








It sounds like you would agree with the following **API** draft:
- A basic function to find local maxima (/minima) in a vector. It seems like something like this is already provided with [argrelextrema](https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.argrelextrema.html#scipy.signal.argrelextrema) which I could use for my first pull request. Later one could look at my own implementation of `extrema` and evaluate which approach is faster / more efficient.
- Coherent functions to compute peak properties, meaning:
  - peak prominence
  - peak width
  - peak threshold, quite simple to do oneself but may still be worth it
- A function that filters peaks by their distance (priority by peak height). I can't decide whether this function should be public or underscored. In any case users would have access to with the next function...
- A `find_peaks` function that wraps peak finding and filtering in one convenient function call.

This has the following **Advantages**:
-  All basic use cases would be covered with `find_peaks` which would provide an interface coherent with the rest of SciPy and would provide minimal surprises for users coming from Matlab. 
- For more complicated use cases and workflows the underlying functions would be easily accessible.

@larsoner would you agree? 

I encourage any additional feedback and thoughts before I start working on a pull request!
Sounds like a good plan to me. Feel free to open a PR as soon as you're ready for feedback.
Sorry for the long delay, was away for a bit, yes this sounds good to me

Resolving patch:
diff --git a/scipy/signal/__init__.py b/scipy/signal/__init__.py
--- a/scipy/signal/__init__.py
+++ b/scipy/signal/__init__.py
@@ -278,10 +278,13 @@
 .. autosummary::
    :toctree: generated/
 
-   find_peaks_cwt -- Attempt to find the peaks in the given 1-D array
-   argrelmin      -- Calculate the relative minima of data
-   argrelmax      -- Calculate the relative maxima of data
-   argrelextrema  -- Calculate the relative extrema of data
+   argrelmin        -- Calculate the relative minima of data
+   argrelmax        -- Calculate the relative maxima of data
+   argrelextrema    -- Calculate the relative extrema of data
+   find_peaks       -- Find a subset of peaks inside a signal.
+   find_peaks_cwt   -- Find peaks in a 1-D array with wavelet transformation.
+   peak_prominences -- Calculate the prominence of each peak in a signal.
+   peak_widths      -- Calculate the width of each peak in a signal.
 
 Spectral Analysis
 =================
diff --git a/scipy/signal/_peak_finding.py b/scipy/signal/_peak_finding.py
--- a/scipy/signal/_peak_finding.py
+++ b/scipy/signal/_peak_finding.py
@@ -9,8 +9,11 @@
 from scipy.signal.wavelets import cwt, ricker
 from scipy.stats import scoreatpercentile
 
+from ._peak_finding_utils import _argmaxima1d
 
-__all__ = ['argrelmin', 'argrelmax', 'argrelextrema', 'find_peaks_cwt']
+
+__all__ = ['argrelmin', 'argrelmax', 'argrelextrema', 'peak_prominences',
+           'peak_widths', 'find_peaks', 'find_peaks_cwt']
 
 
 def _boolrelextrema(data, comparator, axis=0, order=1, mode='clip'):
@@ -100,11 +103,15 @@ def argrelmin(data, axis=0, order=1, mode='clip'):
 
     See Also
     --------
-    argrelextrema, argrelmax
+    argrelextrema, argrelmax, find_peaks
 
     Notes
     -----
-    This function uses `argrelextrema` with np.less as comparator.
+    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 one-dimensional `data` `find_peaks` can be used to detect all
+    local minima, including flat ones, by calling it with negated `data`.
 
     .. versionadded:: 0.11.0
 
@@ -153,11 +160,15 @@ def argrelmax(data, axis=0, order=1, mode='clip'):
 
     See Also
     --------
-    argrelextrema, argrelmin
+    argrelextrema, argrelmin, find_peaks
 
     Notes
     -----
-    This function uses `argrelextrema` with np.greater as comparator.
+    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 one-dimensional `data` `find_peaks` can be used to detect all
+    local maxima, including flat ones.
 
     .. versionadded:: 0.11.0
 
@@ -233,6 +244,769 @@ def argrelextrema(data, comparator, axis=0, order=1, mode='clip'):
     return np.where(results)
 
 
+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 : number, optional
+        A window length in samples that limits the search for the lowest
+        contour line to a symmetric interval around the evaluated peak. If not
+        given the entire signal `x` is used. Use this parameter to speed up the
+        calculation significantly for large vectors (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 (see Notes
+        for a more details).
+
+    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:
+
+    * Extend a horizontal line from the current peak to the left and right until
+      the line either reaches the window end (see `wlen`) or intersects the
+      signal again at the slope of a higher peak.
+    * On each side find the minimal signal value within the interval defined
+      above. These points are the peak's bases.
+    * 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` because the full
+    signal needs to be evaluated for each peak. 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 significantly.
+    However this may stop the algorithm from finding the true global contour
+    line if the peak's 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 largest set of peaks
+    in vector. 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
+    --------
+    >>> 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()
+    """
+    x = np.asarray(x)
+    peaks = np.asarray(peaks)
+    if peaks.size == 0:
+        # Handle empty peaks
+        return np.array([]), np.array([], dtype=int), np.array([], dtype=int)
+
+    if x.ndim != 1:
+        raise ValueError('`x` must have exactly one dimension')
+    if peaks.ndim != 1:
+        raise ValueError('`peaks` must have exactly one dimension')
+    if x.size <= peaks.max():
+        raise ValueError('an index in `peaks` exceeds the size of `x`')
+    if not np.issubdtype(peaks.dtype, np.integer):
+        raise ValueError('`peaks` must be an array of integers')
+    if wlen is not None and wlen < 3:
+        raise ValueError('`wlen` must be at least 3')
+
+    # Prepare return arguments
+    prominences = np.zeros(peaks.size)
+    left_bases = np.zeros(peaks.size, dtype=int)
+    right_bases = np.zeros(peaks.size, dtype=int)
+
+    for i, peak in enumerate(peaks):
+        # If wlen is twice the size of x the symmetric window always covers
+        # the full x
+        if wlen is not None and wlen < x.size * 2:
+            wlen = int(wlen)
+            # Calculate window borders around the evaluated peak
+            wleft = peak - wlen // 2
+            wright = peak + wlen // 2
+            # Handle border cases
+            wleft = 0 if wleft < 0 else wleft
+            wright = x.size if x.size < wright else wright
+            # Use slice for prominence calculation
+            window = x[wleft:wright]
+            # Correct peak position in x
+            peak -= wleft
+        else:
+            # Use full x for prominence calculation
+            window = x
+            wleft = 0
+
+        # Positions where window is larger than current peak height
+        greater_peak = np.where(window > window[peak])[0]
+
+        try:
+            # Nearest position to the left of peak with
+            # window[left] > window[peak]
+            left = greater_peak[greater_peak < peak].max()
+        except ValueError:
+            left = 0
+        try:
+            # Nearest position to right of peak with
+            # window[right] > window[peak]
+            right = greater_peak[greater_peak > peak].min()
+        except ValueError:
+            right = None
+
+        # Base indices to the left and right of peak in window
+        left_bases[i] = window[left:peak].argmin() + left
+        right_bases[i] = window[peak:right].argmin() + peak
+
+        # Calculate lowest contour and its vertical distance to peak
+        lowest_contour = max(window[left_bases[i]], window[right_bases[i]])
+        prominences[i] = window[peak] - lowest_contour
+
+        # Correct window offset
+        left_bases[i] += wleft
+        right_bases[i] += wleft
+
+    return prominences, left_bases, right_bases
+
+
+def peak_widths(x, peaks, rel_height=0.5, prominence_data=None, wlen=None):
+    """
+    Calculate the width of each 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 a number greater 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 for `x` and `peaks`. This data is
+        calculated internally if not provided (see `wlen`).
+    wlen : int, optional
+        A window length in samples (see `peak_prominences`). This argument is
+        only used if `prominence_data` is not given in which case the missing
+        data is calculated using `wlen`.
+
+    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.
+
+    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 intersection
+      points on both sides.
+
+    As shown above to calculate a peaks width its prominence must be known. You
+    can supply these data yourself with the arguments `prominences`, `left_bases`
+    and `right_bases`. Otherwise they are internally calculated using `wlen` if
+    supplied (see `peak_prominences`).
+
+    .. versionadded:: 1.1.0
+
+    Examples
+    --------
+    >>> from scipy.signal import chirp, find_peaks, peak_widths
+    >>> import matplotlib.pyplot as plt
+
+    Create a test signal with growing peak widths
+
+    >>> x = np.linspace(0, 500, 500)
+    >>> x = abs(chirp(x, 1e-4, x.max(), 1.1e-2)) + 2.0 * x / x.max()
+
+    Find all peaks and calculate their widths at the relative height of 0.5
+
+    >>> peaks, _ = find_peaks(x)
+    >>> widths, heights, lpos, rpos = peak_widths(x, peaks, rel_height=0.5)
+    >>> widths
+    array([77.7462348 , 62.19574776, 45.57709222, 37.902356  , 33.33210357,
+           29.81097122])
+
+    Plot signal, peaks and contour lines at which the widths where calculated
+
+    >>> plt.plot(x)
+    >>> plt.plot(peaks, x[peaks], "x", color="C1")
+    >>> plt.hlines(y=heights, xmin=lpos, xmax=rpos, color="C1")
+    >>> plt.show()
+    """
+    x = np.asarray(x)
+    peaks = np.asarray(peaks)
+
+    if peaks.size == 0:
+        # Handle empty peaks
+        return tuple(np.array([]) for _ in range(4))
+
+    if x.ndim != 1:
+        raise ValueError('`x` must have exactly one dimension')
+    if peaks.ndim != 1:
+        raise ValueError('`peaks` must have exactly one dimension')
+    if x.size <= peaks.max():
+        raise ValueError('an index in `peaks` exceeds the size of `x`')
+    if not np.issubdtype(peaks.dtype, np.integer):
+        raise ValueError('`peaks` must be an array of integers')
+    if rel_height < 0.0:
+        raise ValueError('`rel_height` must be greater or equal 0.0')
+
+    if prominence_data is None:
+        # Calculate prominence if not supplied and use wlen if supplied
+        prominences, left_bases, right_bases = peak_prominences(x, peaks, wlen)
+    else:
+        prominences, left_bases, right_bases = prominence_data
+
+    # Calculate evaluation height for each peak
+    width_heights = x[peaks] - np.asarray(prominences) * rel_height
+
+    widths = np.zeros(peaks.size)
+    left_ips = np.zeros(peaks.size)
+    right_ips = np.zeros(peaks.size)
+    for i, (peak, height) in enumerate(zip(peaks, width_heights)):
+
+        # Maximal peak width is from base to base
+        window = x[left_bases[i]:right_bases[i] + 1]
+        peak -= left_bases[i]
+        # Positions where `window` is smaller reference height
+        is_smaller = np.where(window < height)[0]
+
+        try:
+            # Nearest position to the left of peak with
+            # x[left] > x[peak]
+            left_ip = is_smaller[is_smaller < peak].max()
+        except ValueError:
+            left_ip = None
+        try:
+            # Nearest position to right of peak with
+            # x[right] > x[peak]
+            right_ip = is_smaller[is_smaller > peak].min()
+        except ValueError:
+            right_ip = None
+
+        # If not at window border (ip is None), interpolate sub-sample position
+        # to get reasonable precision for steep slopes, do for both sides
+        if left_ip is None:
+            left_ip = 0
+        else:
+            y1, y2 = window[left_ip], window[left_ip + 1]
+            left_ip += (height - y1) / (y2 - y1)
+        if right_ip is None:
+            right_ip = window.size - 1
+        else:
+            y1, y2 = window[right_ip], window[right_ip - 1]
+            right_ip -= (height - y1) / (y2 - y1)
+
+        widths[i] = right_ip - left_ip
+
+        # Correct window offset
+        left_ips[i] = left_ip + left_bases[i]
+        right_ips[i] = right_ip + left_bases[i]
+
+    return widths, width_heights, left_ips, right_ips
+
+
+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 one-dimensional 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]
+
+
+# Code for _select_by_peak_distance was adapted from
+# https://github.com/demotu/BMC/blob/master/functions/detect_peaks.py
+# by Marcos Duarte under the MIT license:
+#
+#     Copyright (c) 2013 Marcos Duarte
+#
+#     Permission is hereby granted, free of charge, to any person
+#     obtaining a copy of this software and associated documentation
+#     files (the "Software"), to deal in the Software without
+#     restriction, including without limitation the rights to use,
+#     copy, modify, merge, publish, distribute, sublicense, and/or sell
+#     copies of the Software, and to permit persons to whom the
+#     Software is furnished to do so, subject to the following
+#     conditions:
+#
+#     The above copyright notice and this permission notice shall be
+#     included in all copies or substantial portions of the Software.
+#
+#     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+#     EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+#     OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+#     NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+#     HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+#     WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+#     FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+#     OTHER DEALINGS IN THE SOFTWARE.
+
+def _select_by_peak_distance(peaks, priority, dmin):
+    """
+    Evaluate which peaks fulfill the distance condition.
+
+    Parameters
+    ----------
+    peaks : ndarray
+        Indices of peaks in `vector`.
+    priority : ndarray
+        An array with priorities matching `peaks` used to determine priority of
+        peaks. A peak with a higher priority value is kept over one with a lower
+        one.
+    dmin : number
+        Minimal distance that peaks must be spaced.
+
+    Returns
+    -------
+    keep : ndarray[bool]
+        A boolean mask evaluating to true where `peaks` fulfill the distance
+        condition.
+
+    Notes
+    -----
+
+    .. versionadded:: 1.1.0
+    """
+    # Peaks are evaluated by priority (larger first)
+    eval_peaks = peaks[np.argsort(priority)][::-1]
+
+    # Flag peaks for deletion
+    del_flag = np.zeros(eval_peaks.size, dtype=bool)
+    for i in range(eval_peaks.size):
+        if not del_flag[i]:
+            # Flag peaks in intervall +-distance around current peak
+            del_flag |= (eval_peaks > eval_peaks[i] - dmin) \
+                        & (eval_peaks < eval_peaks[i] + dmin)
+            # Keep current peak
+            del_flag[i] = False
+
+    keep = ~del_flag[np.argsort(eval_peaks)]
+
+    return keep
+
+
+def find_peaks(x, height=None, threshold=None, distance=None,
+               prominence=None, width=None, wlen=None, rel_height=0.5):
+    """
+    Find peaks inside a signal based on peak properties.
+
+    This function takes a one-dimensional array and finds all local maxima by
+    simple comparison of neighbouring 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  minimum and the second, if supplied, as the
+        maximum required height.
+    threshold : number or ndarray or sequence, optional
+        Required threshold of peaks, the vertical distance to its neighbouring
+        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  minimum and the second, if supplied, as the maximum
+        required threshold.
+    distance : number, optional
+        Required minimal horizontal distance (>= 1) in samples between
+        neighbouring peaks. The removal order is explained in the notes section.
+    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  minimum and the second, if
+        supplied, as the maximum 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  minimum and the second, if
+        supplied, as the maximum required prominence.
+    wlen : number, 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.
+
+    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.
+        * 'peak_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.
+
+        To calculate and return properties without excluding peaks, provide the
+        open interval ``(None, None)`` as a value to the appropriate argument
+        (excluding `distance`).
+
+    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
+    -----
+    Because this function searches for local maxima by direct sample comparison,
+    the determined peak locations can be off for noisy signals if the noise
+    changes the position of a local maximum. In those cases consider smoothing
+    the signal before searching for peaks or using 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 order of arguments given in the function definition above mirrors the
+      actual order in which conditions are evaluated. 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.
+    * Satisfying the distance condition is accomplished by iterating over all
+      peaks in descending order based on their height and removing all lower
+      peaks that are too close. This option can be quite slow if many peaks need
+      to be evaluated. Try to reduce the number of peaks beforehand by
+      specifying conditions that are evaluated before this one (`height` and
+      `threshold`).
+    * 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
+    --------
+    >>> from scipy.signal import find_peaks
+    >>> import matplotlib.pyplot as plt
+
+    Create test signal `x` using 7 harmonics:
+
+    >>> gains = [1, -1, 0.6, 0.5, 0.4, 0.3, 0.1]
+    >>> freqs = [2, 3.5, 6, 7.1, 11.1, 12, 20]
+    >>> t = np.linspace(0, 6, 1000)
+    >>> x = sum(g * np.sin(f * t) for g, f in zip(gains, freqs))
+
+    Find all peaks (local maxima) in `x`
+
+    >>> peaks, _ = find_peaks(x)
+    >>> peaks
+    array([ 25, 119, 205, 275, 381, 495, 586, 782, 892, 952])
+
+    and plot the results
+
+    >>> plt.figure()
+    >>> plt.plot(x)
+    >>> plt.plot(peaks, x[peaks], 'x')
+    >>> plt.show()
+
+    This time, find peaks that have a minimal prominence of 0.5 and whose peak
+    base is not more than 150 samples wide (``width=(None, 150)``). For this we
+    need to use the option ``rel_height=1.0`` which will evaluate the width at
+    the peak's base.
+
+    >>> peaks, prop = find_peaks(
+    ...     x, prominence=0.5, width=(None, 150), rel_height=1.0)
+    >>> peaks, prop['prominences'], prop['widths']
+    (array([ 25, 381, 892]),
+     array([1.40613266, 0.50503469, 1.23546031]),
+     array([59.57018554, 57.9368999 , 86.11729375]))
+
+    and plot the results including the calculated peak properties
+
+    >>> plt.figure()
+    >>> plt.plot(x)
+    >>> plt.plot(peaks, x[peaks], 'x')
+    >>> plt.vlines(x=peaks, ymin=prop['width_heights'], ymax=x[peaks])
+    >>> plt.hlines(y=prop['width_heights'], xmin=prop['left_ips'],
+    ...            xmax=prop['right_ips'])
+    >>> plt.show()
+    """
+    # _argmaxima1d expects array of dtype 'float64'
+    x = np.asarray(x, dtype=np.float64)
+    if x.ndim != 1:
+        raise ValueError('`x` must have exactly one dimension')
+    if distance is not None and distance < 1:
+        raise ValueError('`distance` must be greater or equal to 1')
+
+    peaks = _argmaxima1d(x)
+    properties = {}
+
+    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[keep]
+
+    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)
+        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.
@@ -420,7 +1194,7 @@ def filt_func(line):
 def find_peaks_cwt(vector, widths, wavelet=None, max_distances=None,
                    gap_thresh=None, min_length=None, min_snr=1, noise_perc=10):
     """
-    Attempt to find the peaks in a 1-D array.
+    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
@@ -436,8 +1210,8 @@ def find_peaks_cwt(vector, widths, wavelet=None, max_distances=None,
         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 
+        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
@@ -471,6 +1245,9 @@ def find_peaks_cwt(vector, widths, wavelet=None, max_distances=None,
     See Also
     --------
     cwt
+        Continuous wavelet transform.
+    find_peaks
+        Find peaks inside a signal based on peak properties.
 
     Notes
     -----


Test patch:
diff --git a/scipy/signal/tests/test_peak_finding.py b/scipy/signal/tests/test_peak_finding.py
--- a/scipy/signal/tests/test_peak_finding.py
+++ b/scipy/signal/tests/test_peak_finding.py
@@ -2,14 +2,14 @@
 
 import copy
 
+import numpy as np
+from numpy.testing import (assert_equal, assert_array_equal, assert_)
 import pytest
 from pytest import raises
 
-import numpy as np
-from numpy.testing import assert_equal, assert_array_equal, assert_
-
 from scipy._lib.six import xrange
 from scipy.signal._peak_finding import (argrelmax, argrelmin,
+    peak_prominences, peak_widths, _unpack_condition_args, find_peaks,
     find_peaks_cwt, _identify_ridge_lines)
 from scipy.signal._peak_finding_utils import _argmaxima1d
 
@@ -301,8 +301,316 @@ def test_2d_gaussians(self):
             assert_((act_locs == (rel_max_cols[inds] - rot_factor*rw)).all())
 
 
+class TestPeakProminences(object):
+
+    def test_empty(self):
+        """
+        Test if an empty array is returned if no peaks are provided.
+        """
+        proms = peak_prominences([], [])[0]
+        assert_(isinstance(proms, np.ndarray))
+        assert_equal(proms.size, 0)
+        proms = peak_prominences([1, 2, 3], [])[0]
+        assert_(isinstance(proms, np.ndarray))
+        assert_equal(proms.size, 0)
+        out = peak_prominences([], [])
+        for arr in out:
+            assert_(isinstance(arr, np.ndarray))
+            assert_equal(arr.size, 0)
+
+    def test_basic(self):
+        """
+        Test if height of prominences is correctly calculated in signal with
+        rising baseline (peak widths are 1 sample).
+        """
+        x = np.linspace(1, 4, 7)  # Rising baseline
+        peak_heights = [2, 4, 1.2]  # Peak heights
+        peak_pos = [1, 3, 5]
+        desired = []
+        for h, p in zip(peak_heights, peak_pos):
+            x[p] += h
+            # Peak prominence is difference between vector[peak] and next
+            # sample to the right
+            desired.append(x[p] - x[p + 1])
+        actual = peak_prominences(x, [1, 3, 5])[0]
+        assert_equal(actual, desired)
+
+    def test_wlen(self):
+        """
+        Test if wlen actually shrinks the evaluation range.
+        """
+        t = np.linspace(0, 4 * np.pi, 1000)
+        x = abs(np.sin(t))
+        peaks = argrelmax(x)[0]
+        # Raise 2 baseline of peaks in the middle
+        x[250:750] += 0.3
+        # If entire x is used the middle two peaks should have a prominence
+        # of approx. 1 + 0.3, otherwise minima between second and third peak
+        # should be lowest contour line and prominence should be < 1.1
+        proms_wlen = peak_prominences(x, peaks, wlen=600)[0]
+        assert_(np.all(proms_wlen < 1.1))
+        # If window length is 2
+        assert_equal(peak_prominences(x, peaks)[0],
+                     peak_prominences(x, peaks, wlen=(x.size * 2))[0])
+
+    def test_raises(self):
+        """
+        Verfiy that argument validation works as intended.
+        """
+        with raises(ValueError, match='dimension'):
+            # x with dimension > 1
+            peak_prominences(np.zeros((3, 4)), np.ones(3))
+        with raises(ValueError, match='dimension'):
+            # x with dimension < 1
+            peak_prominences(3, [0,])
+        with raises(ValueError, match='dimension'):
+            # peaks with dimension > 1
+            peak_prominences(np.arange(10), np.ones((3, 2)))
+        with raises(ValueError, match='dimension'):
+            # peaks with dimension < 1
+            peak_prominences(np.arange(10), 3)
+        with raises(ValueError, match='index'):
+            # peak pos exceeds x.size
+            peak_prominences(np.arange(10), [8, 11])
+        with raises(ValueError, match='index'):
+            # empty x with peaks supplied
+            peak_prominences([], [1, 2])
+        with raises(ValueError, match='integers'):
+            # peak is not of subtype int
+            peak_prominences(np.arange(10), [1.1, 2.3])
+        with raises(ValueError, match='wlen'):
+            # wlen < 3
+            peak_prominences(np.arange(10), [3, 5], wlen=2)
+
+
+class TestPeakWidths(object):
+
+    def test_empty(self):
+        """
+        Test if an empty array is returned if no peaks are provided.
+        """
+        widths = peak_widths([], [])[0]
+        assert_(isinstance(widths, np.ndarray))
+        assert_equal(widths.size, 0)
+        widths = peak_widths([1, 2, 3], [])[0]
+        assert_(isinstance(widths, np.ndarray))
+        assert_equal(widths.size, 0)
+        out = peak_widths([], [])
+        for arr in out:
+            assert_(isinstance(arr, np.ndarray))
+            assert_equal(arr.size, 0)
+
+    def test_basic(self):
+        """
+        Test a simple use case with easy to verify results at different relative
+        heights.
+        """
+        x = np.array([1, 0, 1, 2, 1, 0, -1])
+        prominence = 2
+        iteration = [
+            # rh, w_true, lip_true, rip_true
+            (0., 0., 3., 3.),
+            (0.25, 1., 2.5, 3.5),
+            (0.5, 2., 2., 4.),
+            (0.75, 3., 1.5, 4.5),
+            (1., 4., 1., 5.),
+            (2., 5., 1., 6.),
+            (3., 5., 1., 6.)
+        ]
+        for rh, w_true, lip_true, rip_true in iteration:
+            w_calc, height, lip_calc, rip_calc = peak_widths(x, [3], rh)
+            assert_(w_calc == w_true)
+            assert_(height == 2 - rh * prominence)
+            assert_(lip_calc == lip_true)
+            assert_(rip_calc == rip_true)
+        # Additional test without argument ret_pos
+        assert_(peak_widths([1, 2, 1], [1])[0], 1)
+
+    def test_raises(self):
+        """
+        Verfiy that argument validation works as intended.
+        """
+        with raises(ValueError, match='dimension'):
+            # x with dimension > 1
+            peak_widths(np.zeros((3, 4)), np.ones(3))
+        with raises(ValueError, match='dimension'):
+            # x with dimension < 1
+            peak_widths(3, [0, ])
+        with raises(ValueError, match='dimension'):
+            # peaks with dimension > 1
+            peak_widths(np.arange(10), np.ones((3, 2)))
+        with raises(ValueError, match='dimension'):
+            # peaks with dimension < 1
+            peak_widths(np.arange(10), 3)
+        with raises(ValueError, match='index'):
+            # peak pos exceeds x.size
+            peak_widths(np.arange(10), [8, 11])
+        with raises(ValueError, match='index'):
+            # empty x with peaks supplied
+            peak_widths([], [1, 2])
+        with raises(ValueError, match='integers'):
+            # peak is not of subtype int
+            peak_widths(np.arange(10), [1.1, 2.3])
+        with raises(ValueError, match='rel_height'):
+            # rel_height is < 0
+            peak_widths(np.arange(10), [1, 2], rel_height=-1)
+
+
+def test_unpack_condition_args():
+    """
+    Verify parsing of condition arguments for `scipy.signal.find_peaks` function.
+    """
+    x = np.arange(10)
+    amin_true = x
+    amax_true = amin_true + 10
+    peaks = amin_true[1::2]
+
+    # Test unpacking with None or interval
+    assert_((None, None) == _unpack_condition_args((None, None), x, peaks))
+    assert_((1, None) == _unpack_condition_args(1, x, peaks))
+    assert_((1, None) == _unpack_condition_args((1, None), x, peaks))
+    assert_((None, 2) == _unpack_condition_args((None, 2), x, peaks))
+    assert_((3., 4.5) == _unpack_condition_args((3., 4.5), x, peaks))
+
+    # Test if borders are correctly reduced with `peaks`
+    amin_calc, amax_calc = _unpack_condition_args((amin_true, amax_true), x, peaks)
+    assert_equal(amin_calc, amin_true[peaks])
+    assert_equal(amax_calc, amax_true[peaks])
+
+    # Test raises if array borders don't match x
+    with raises(ValueError, match="array size of lower"):
+        _unpack_condition_args(amin_true, np.arange(11), peaks)
+    with raises(ValueError, match="array size of upper"):
+        _unpack_condition_args((None, amin_true), np.arange(11), peaks)
+
+
 class TestFindPeaks(object):
 
+    # Keys of optionally returned properties
+    property_keys = {'peak_heights', 'left_thresholds', 'right_thresholds',
+                     'prominences', 'left_bases', 'right_bases', 'widths',
+                     'width_heights', 'left_ips', 'right_ips'}
+
+    def test_constant(self):
+        """
+        Test behavior for signal without local maxima.
+        """
+        open_interval = (None, None)
+        peaks, props = find_peaks(np.ones(10),
+                                  height=open_interval, threshold=open_interval,
+                                  prominence=open_interval, width=open_interval)
+        assert_(peaks.size == 0)
+        for key in self.property_keys:
+            assert_(props[key].size == 0)
+
+    def test_height_condition(self):
+        """
+        Test height condition for peaks.
+        """
+        x = (0., 1/3, 0., 2.5, 0, 4., 0)
+        peaks, props = find_peaks(x, height=(None, None))
+        assert_equal(peaks, np.array([1, 3, 5]))
+        assert_equal(props['peak_heights'], np.array([1/3, 2.5, 4.]))
+        assert_equal(find_peaks(x, height=0.5)[0], np.array([3, 5]))
+        assert_equal(find_peaks(x, height=(None, 3))[0], np.array([1, 3]))
+        assert_equal(find_peaks(x, height=(2, 3))[0], np.array([3]))
+
+    def test_threshold_condition(self):
+        """
+        Test threshold condition for peaks.
+        """
+        x = (0, 2, 1, 4, -1)
+        peaks, props = find_peaks(x, threshold=(None, None))
+        assert_equal(peaks, np.array([1, 3]))
+        assert_equal(props['left_thresholds'], np.array([2, 3]))
+        assert_equal(props['right_thresholds'], np.array([1, 5]))
+        assert_equal(find_peaks(x, threshold=2)[0], np.array([3]))
+        assert_equal(find_peaks(x, threshold=3.5)[0], np.array([]))
+        assert_equal(find_peaks(x, threshold=(None, 5))[0], np.array([1, 3]))
+        assert_equal(find_peaks(x, threshold=(None, 4))[0], np.array([1]))
+        assert_equal(find_peaks(x, threshold=(2, 4))[0], np.array([]))
+
+    def test_distance_condition(self):
+        """
+        Test distance condition for peaks.
+        """
+        # Peaks of different height with constant distance
+        peaks_all = np.arange(1, 21, 3)
+        x = np.zeros(21)
+        x[peaks_all] += np.linspace(1, 2, peaks_all.size)
+        # Filter every second peak
+        peaks_subset = find_peaks(x, distance=4)[0]
+        # Test if peaks_subset is subset of peaks_all
+        assert_(
+            np.setdiff1d(peaks_subset, peaks_all, assume_unique=True).size == 0
+        )
+        # Test that every second peak was removed
+        assert_equal(np.diff(peaks_subset), 6)
+
+        # Test priority of peak removal
+        x = [-2, 1, -1, 0, -3]
+        peaks_subset = find_peaks(x, distance=10)[0]  # use distance > x size
+        assert_(peaks_subset.size == 1 and peaks_subset[0] == 1)
+
+    def test_prominence_condition(self):
+        """
+        Test prominence condition for peaks.
+        """
+        x = np.linspace(0, 10, 100)
+        peaks_true = np.arange(1, 99, 2)
+        offset = np.linspace(1, 10, peaks_true.size)
+        x[peaks_true] += offset
+        prominences = x[peaks_true] - x[peaks_true + 1]
+        interval = (3, 9)
+        keep = np.where(
+            (interval[0] <= prominences) & (prominences <= interval[1]))
+
+        peaks_calc, properties = find_peaks(x, prominence=interval)
+        assert_equal(peaks_calc, peaks_true[keep])
+        assert_equal(properties['prominences'], prominences[keep])
+        assert_equal(properties['left_bases'], 0)
+        assert_equal(properties['right_bases'], peaks_true[keep] + 1)
+
+    def test_width_condition(self):
+        """
+        Test width condition for peaks.
+        """
+        x = np.array([1, 0, 1, 2, 1, 0, -1, 4, 0])
+        peaks, props = find_peaks(x, width=(None, 2), rel_height=0.75)
+        assert_(peaks.size == 1)
+        assert_(peaks == 7)
+        assert_(props['widths'] == 1.35)
+        assert_(props['width_heights'] == 1.)
+        assert_(props['left_ips'] == 6.4)
+        assert_(props['right_ips'] == 7.75)
+
+    def test_properties(self):
+        """
+        Test returned properties.
+        """
+        open_interval = (None, None)
+        x = [0, 1, 0, 2, 1.5, 0, 3, 0, 5, 9]
+        peaks, props = find_peaks(x,
+                                  height=open_interval, threshold=open_interval,
+                                  prominence=open_interval, width=open_interval)
+        assert_(len(props) == len(self.property_keys))
+        for key in self.property_keys:
+            assert_(peaks.size == props[key].size)
+
+    def test_raises(self):
+        """
+        Test exceptions raised by function.
+        """
+        with raises(ValueError, match="dimension"):
+            find_peaks(np.array(1))
+        with raises(ValueError, match="dimension"):
+            find_peaks(np.ones((2, 2)))
+        with raises(ValueError, match="distance"):
+            find_peaks(np.arange(10), distance=-1)
+
+
+class TestFindPeaksCwt(object):
+
     def test_find_peaks_exact(self):
         """
         Generate a series of gaussians and attempt to find the peak locations.