summaryrefslogtreecommitdiff
path: root/silx/math/fit/peaks/peaks.pyx
blob: 02753b0ec717743faf94d7fa39a00b40216d050f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# coding: utf-8
#/*##########################################################################
# Copyright (C) 2016-2017 European Synchrotron Radiation Facility
#
# 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.
#
#############################################################################*/
"""This module provides a peak search function and tools related to peak
analysis.
"""

__authors__ = ["P. Knobel"]
__license__ = "MIT"
__date__ = "17/06/2016"

import logging
import numpy

from . import filters

logging.basicConfig()
_logger = logging.getLogger(__name__)

cimport cython
from libc.stdlib cimport free

cimport peaks_wrapper


def peak_search(y, fwhm, sensitivity=3.5,
                begin_index=None, end_index=None,
                debug=False, relevance_info=False):
    """Find peaks in a curve.

    :param y: Data array
    :type y: numpy.ndarray
    :param fwhm: Estimated full width at half maximum of the typical peaks we
        are interested in (expressed in number of samples)
    :param sensitivity: Threshold factor used for peak detection. Only peaks
        with amplitudes higher than ``σ * sensitivity`` - where ``σ`` is the
        standard deviation of the noise - qualify as peaks.
    :param begin_index: Index of the first sample of the region of interest
         in the ``y`` array. If ``None``, start from the first sample.
    :param end_index: Index of the last sample of the region of interest in
        the ``y`` array. If ``None``, process until the last sample.
    :param debug: If ``True``, print debug messages. Default: ``False``
    :param relevance_info: If ``True``, add a second dimension with relevance
        information to the output array. Default: ``False``
    :return: 1D sequence with indices of peaks in the data
        if ``relevance_info`` is ``False``.
        Else, sequence of  ``(peak_index, peak_relevance)`` tuples (one tuple
        per peak).
    :raise: ``IndexError`` if the number of peaks is too large to fit in the
        output array.
    """
    cdef:
        int i
        double[::1] y_c
        double* peaks_c
        double* relevances_c

    y_c = numpy.array(y,
                      copy=True,
                      dtype=numpy.float64,
                      order='C').reshape(-1)
    if debug:
        debug = 1
    else:
        debug = 0

    if begin_index is None:
        begin_index = 0
    if end_index is None:
        end_index = y_c.size - 1

    n_peaks = peaks_wrapper.seek(begin_index, end_index, y_c.size,
                                 fwhm, sensitivity, debug,
                                 &y_c[0], &peaks_c, &relevances_c)


    # A negative return value means that peaks were found but not enough
    # memory could be allocated for all
    if n_peaks < 0 and n_peaks != -123456:
        msg = "Before memory allocation error happened, "
        msg += "we found %d peaks.\n" % abs(n_peaks)
        _logger.debug(msg)
        msg = ""
        for i in range(abs(n_peaks)):
            msg += "peak index %f, " % peaks_c[i]
            msg += "relevance %f\n" % relevances_c[i]
        _logger.debug(msg)
        free(peaks_c)
        free(relevances_c)
        raise MemoryError("Failed to reallocate memory for output arrays")
    # Special value -123456 is returned if the initial memory allocation
    # fails, before any search could be performed
    elif n_peaks == -123456:
        raise MemoryError("Failed to allocate initial memory for " +
                          "output arrays")

    peaks = numpy.empty(shape=(n_peaks,),
                        dtype=numpy.float64)
    relevances = numpy.empty(shape=(n_peaks,),
                             dtype=numpy.float64)

    for i in range(n_peaks):
        peaks[i] = peaks_c[i]
        relevances[i] = relevances_c[i]

    free(peaks_c)
    free(relevances_c)

    if not relevance_info:
        return peaks
    else:
        return list(zip(peaks, relevances))


def guess_fwhm(y):
    """Return the full-width at half maximum for the largest peak in
    the data array.

    The algorithm removes the background, then finds a global maximum
    and its corresponding FWHM.

    This value can be used as an initial fit parameter, used as input for
    an iterative fit function.

    :param y: Data to be used for guessing the fwhm.
    :return: Estimation of full-width at half maximum, based on fwhm of
        the global maximum.
    """
    # set at a minimum value for the fwhm
    fwhm_min = 4

    # remove data background (computed with a strip filter)
    background = filters.strip(y, w=1, niterations=1000)
    yfit = y - background

    # basic peak search: find the global maximum
    maximum = max(yfit)
    # find indices of all values == maximum
    idx = numpy.nonzero(yfit == maximum)[0]
    # take the last one (if any)
    if not len(idx):
        return 0
    posindex = idx[-1]
    height = yfit[posindex]

    # now find the width of the peak at half maximum
    imin = posindex
    while yfit[imin] > 0.5 * height and imin > 0:
        imin -= 1
    imax = posindex
    while yfit[imax] > 0.5 * height and imax < len(yfit) - 1:
        imax += 1

    fwhm = max(imax - imin - 1, fwhm_min)

    return fwhm