U
    g»{jÅB  ã                   @   s¤   d Z ddlZddlmZ ddlmZmZ ddlm	Z	 ddd	d
dddgZ
ddd„Zdd„ Zdi fdd	„Zddi fdd
„Zdi fdd„Zdi fdd„Zdi fdd„ZdS )z0
functions for peak detection and related tasks
é    N)Úresampleé   )Úcalc_rrÚ	update_rr)ÚBadSignalWarningÚmake_windowsÚappend_dictÚdetect_peaksÚ	fit_peaksÚcheck_peaksÚcheck_binary_qualityÚinterpolate_peakséx   é   c                 C   s¤   t | ƒ}|| }d| | }d}|}	g }
|	t | ƒk rT|
 ||	f¡ ||7 }|	|7 }	q(|dkrv|
d d t | ƒf|
d< n|| | |kr”|
 ||f¡ tj|
tjd�S )aS  slices data into windows

    Funcion that slices data into windows for concurrent analysis.
    Used by process_segmentwise wrapper function.

    Parameters
    ----------
    data : 1-d array
        array containing heart rate sensor data

    sample_rate : int or float
        sample rate of the data stream in 'data'

    windowsize : int
        size of the window that is sliced in seconds

    overlap : float
        fraction of overlap between two adjacent windows: 0 <= float < 1.0

    min_size : int
        the minimum size for the last (partial) window to be included. Very short windows
        might not stable for peak fitting, especially when significant noise is present.
        Slightly longer windows are likely stable but don't make much sense from a
        signal analysis perspective.

    Returns
    -------
    out : array
        tuples of window indices

    Examples
    --------
    Assuming a given example data file:

    >>> import heartpy as hp
    >>> data, _ = hp.load_exampledata(1)

    We can split the data into windows:

    >>> indices = make_windows(data, 100.0, windowsize = 30, overlap = 0.5, min_size = 20)
    >>> indices.shape
    (9, 2)

    Specifying min_size = -1 will include the last window no matter what:

    >>> indices = make_windows(data, 100.0, windowsize = 30, overlap = 0.5, min_size = -1)
    r   r   éÿÿÿÿ)Zdtype)ÚlenÚappendÚnpÚarrayZint32)ÚdataÚsample_rateZ
windowsizeZoverlapZmin_sizeÚlnZwindowZstepsizeÚstartÚendZslices© r   úT/var/www/html/bwave/python/venv/lib/python3.8/site-packages/heartpy/peakdetection.pyr      s    0
c                 C   s6   z| |   |¡ W n tk
r0   |g| |< Y nX | S )aê  appends data to keyed dict.

    Function that appends key to continuous dict, creates if doesn't exist. EAFP

    Parameters
    ----------
    dict_obj : dict
        dictionary object that contains continuous output measures

    measure_key : str
        key for the measure to be stored in continuous_dict

    measure_value : any data container
        value to be appended to dictionary

    Returns
    -------
    dict_obj : dict
        dictionary object passed to function, with specified data container appended

    Examples
    --------
    Given a dict object 'example' with some data in it:

    >>> example = {}
    >>> example['call'] = ['hello']

    We can use the function to append it:

    >>> example = append_dict(example, 'call', 'world')
    >>> example['call']
    ['hello', 'world']

    A new key will be created if it doesn't exist:

    >>> example = append_dict(example, 'different_key', 'hello there!')
    >>> sorted(example.keys())
    ['call', 'different_key']
    )r   ÚKeyError)Údict_objZmeasure_keyZmeasure_valuer   r   r   r   Y   s
    (Tc              	      s^  t  |¡}t  |d ¡| }|| }t  ˆ |k¡d }ˆ | }	t  t  dg¡t  t  |¡dk¡d t  t|ƒg¡f¡}
g }tdt|
ƒd ƒD ]R}z@|	|
| |
|d  …  ¡ }| 	||
| | 
t|ƒ¡  ¡ W qŒ   Y qŒX qŒ|�rR||d< ‡ fdd„|D ƒ|d< ||d< t|d ||d	�}t|d
 ƒdk�rDt  |d
 ¡|d< n
t j|d< |S ||fS dS )aa  detect peaks in signal

    Function that detects heartrate peaks in the given dataset.

    Parameters
    ----------
    hr data : 1-d numpy array or list
        array or list containing the heart rate data

    rol_mean : 1-d numpy array
        array containing the rolling mean of the heart rate signal

    ma_perc : int or float
        the percentage with which to raise the rolling mean,
        used for fitting detection solutions to data

    sample_rate : int or float
        the sample rate of the provided data set

    update_dict : bool
        whether to update the peak information in the module's data structure
        Settable to False to allow this function to be re-used for example by
        the breath analysis module.
        default : True

    Examples
    --------
    Normally part of the peak detection pipeline. Given the first example data
    it would work like this:

    >>> import heartpy as hp
    >>> from heartpy.datautils import rolling_mean, _sliding_window
    >>> data, _ = hp.load_exampledata(0)
    >>> rol_mean = rolling_mean(data, windowsize = 0.75, sample_rate = 100.0)
    >>> wd = detect_peaks(data, rol_mean, ma_perc = 20, sample_rate = 100.0)

    Now the peaklist has been appended to the working data dict. Let's look
    at the first five peak positions:

    >>> wd['peaklist'][0:5]
    [63, 165, 264, 360, 460]
    éd   r   r   Úpeaklistc                    s   g | ]}ˆ | ‘qS r   r   ©Ú.0Úx©Úhrdatar   r   Ú
<listcomp>É   s     z detect_peaks.<locals>.<listcomp>ÚybeatZrolling_mean©Úworking_dataZRR_listÚrrsdN)r   r   ÚmeanÚwhereZconcatenateÚdiffr   ÚrangeÚtolistr   ÚindexÚmaxr   ZstdÚinf)r$   Úrol_meanÚma_percr   Úupdate_dictr(   ZrmeanZmnZpeaksxZpeaksyZ	peakedgesr   ÚiZy_valuesr   r#   r   r	   ˆ   s8    +
þ$
ÿ
é(   é´   c                 C   s  ddddddddd	d
ddddddddg}g }g }|D ]H}	t | ||	|d|d�}t|d ƒt| ƒ|  d	 }
| |d |
|	g¡ q4|D ]8\}}}|dkr‚||  kr¨|kr‚n q‚| ||g¡ q‚t|ƒdk�r
t|dd„ d�d |d< t | |t|dd„ d�d |d|d�}|S tdƒ‚d S )!a¥  optimize for best peak detection

    Function that runs fitting with varying peak detection thresholds given a
    heart rate signal.

    Parameters
    ----------
    hrdata : 1d array or list
        array or list containing the heart rate data

    rol_mean : 1-d array
        array containing the rolling mean of the heart rate signal

    sample_rate : int or float
        the sample rate of the data set

    bpmmin : int
        minimum value of bpm to see as likely
        default : 40

    bpmmax : int
        maximum value of bpm to see as likely
        default : 180

    Returns
    -------
    working_data : dict
        dictionary object that contains all heartpy's working data (temp) objects.
        will be created if not passed to function

    Examples
    --------
    Part of peak detection pipeline. Uses moving average as a peak detection
    threshold and rises it stepwise. Determines best fit by minimising
    standard deviation of peak-peak distances as well as getting a bpm that
    lies within the expected range.

    Given included example data let's show how this works

    >>> import heartpy as hp
    >>> from heartpy.datautils import rolling_mean, _sliding_window
    >>> data, _ = hp.load_exampledata(0)
    >>> rol_mean = rolling_mean(data, windowsize = 0.75, sample_rate = 100.0)

    We can then call this function and let the optimizer do its work:

    >>> wd = fit_peaks(data, rol_mean, sample_rate = 100.0)

    Now the wd dict contains the best fit paramater(s):

    >>> wd['best']
    20

    This indicates the best fit can be obtained by raising the moving average
    with 20%.

    The results of the peak detection using these parameters are included too.
    To illustrate, these are the first five detected peaks:

    >>> wd['peaklist'][0:5]
    [63, 165, 264, 360, 460]

    and the corresponding peak-peak intervals:

    >>> wd['RR_list'][0:4]
    array([1020.,  990.,  960., 1000.])
    é   é
   é   r   é   é   r6   é2   é<   éF   éP   éZ   r   én   r   é–   éÈ   é,  T)r4   r(   r   r)   çš™™™™™¹?r   c                 S   s   | d S ©Nr   r   ©Útr   r   r   Ú<lambda>,  ó    zfit_peaks.<locals>.<lambda>)Úkeyr   Úbestc                 S   s   | d S rG   r   rH   r   r   r   rJ   -  rK   a…  
----------------
Could not determine best fit for given signal. Please check the source signal.
 Probable causes:
- detected heart rate falls outside of bpmmin<->bpmmax constraints
- no detectable heart rate present in signal
- very noisy signal (consider filtering and scaling)
If you're sure the signal contains heartrate data, consider filtering and/or scaling first.
----------------
N)r	   r   r   Úminr   )r$   r2   r   ZbpmminZbpmmaxr(   Zma_perc_listr)   Zvalid_mar3   ZbpmZ_rrsdZ_bpmZ_ma_percr   r   r   r
   Ö   s,    F(
 ÿ   ÿFc           
         sÐ   t  | ¡} t  |¡}t  |¡}t  | ¡}d| }|dkrJ|d }|d }n|| }|| }t  | |k| |kB ¡d d }	||	 ˆ d< ||	 ˆ d< t  ‡ fdd„|D ƒ¡ˆ d	< |rÂt|ˆ d	 ˆ d
�‰ tˆ d
�‰ ˆ S )a‰  find anomalous peaks.

    Funcion that checks peaks for outliers based on anomalous peak-peak distances and corrects
    by excluding them from further analysis.

    Parameters
    ----------
    rr_arr : 1d array or list
        list or array containing peak-peak intervals

    peaklist : 1d array or list
        list or array containing detected peak positions

    ybeat : 1d array or list
        list or array containing corresponding signal values at
        detected peak positions. Used for plotting functionality
        later on.

    reject_segmentwise : bool
        if set, checks segments per 10 detected peaks. Marks segment
        as rejected if 30% of peaks are rejected.
        default : False

    working_data : dict
        dictionary object that contains all heartpy's working data (temp) objects.
        will be created if not passed to function

    Returns
    -------
    working_data : dict
        working_data dictionary object containing all of heartpy's temp objects

    Examples
    --------
    Part of peak detection pipeline. No standalone examples exist. See docstring
    for hp.process() function for more info
    g333333Ó?rE   r   r   Úremoved_beatsZremoved_beats_yc                    s    g | ]}|ˆ d  krdnd‘qS )rO   r   r   r   r    r'   r   r   r%   r  s   ÿzcheck_peaks.<locals>.<listcomp>Úbinary_peaklistr'   )r   r   r*   r+   Zasarrayr   r   )
Zrr_arrr   r&   Zreject_segmentwiser(   Zmean_rrZthirty_percZupper_thresholdZlower_thresholdZrem_idxr   r'   r   r   8  s,    '




ÿ
ÿ
é   c                 C   sÈ   d}g |d< t tt|ƒd ƒƒD ]¢}t |||d … ¡d |krºdd„ t t|||d … ƒƒD ƒ|||d …< |d t| ƒk r |d  | | | |d  f¡ n|d  | | | d f¡ |d7 }q |S )a–  checks signal in chunks of 10 beats.

    Function that checks signal in chunks of 10 beats. It zeros out chunk if
    number of rejected peaks > maxrejects. Also marks rejected segment coordinates
    in tuples (x[0], x[1] in working_data['rejected_segments']

    Parameters
    ----------
    peaklist : 1d array or list
        list or array containing detected peak positions

    binary_peaklist : 1d array or list
        list or array containing mask for peaklist, coding which peaks are rejected

    maxjerects : int
        maximum number of rejected peaks per 10-beat window
        default : 3

    working_data : dict
        dictionary object that contains all heartpy's working data (temp) objects.
        will be created if not passed to function

    Returns
    -------
    working_data : dict
        working_data dictionary object containing all of heartpy's temp objects

    Examples
    --------
    Part of peak detection pipeline. No standalone examples exist. See docstring
    for hp.process() function for more info

    Given some peaklist and binary mask:
    >>> peaklist = [30, 60, 90, 110, 130, 140, 160, 170, 200, 220]
    >>> binary_peaklist = [0, 1, 1, 0, 0, 1, 0, 1, 0, 0]
    >>> wd = check_binary_quality(peaklist, binary_peaklist)
    >>> wd['rejected_segments']
    [(30, 220)]

    The whole segment is rejected as it contains more than the specified 3 rejections
    per 10 beats.
    r   Zrejected_segmentsr9   c                 S   s   g | ]}d ‘qS )r   r   )r!   r5   r   r   r   r%   ­  s     z(check_binary_quality.<locals>.<listcomp>r   )r-   Úintr   r   Zbincountr   )r   rP   Z
maxrejectsr(   Úidxr5   r   r   r   r   ~  s    +. 
g     @�@c                    sž   ||kst dƒ‚td| ƒ‰ || }‡ fdd„|D ƒ}g }|D ]R}| |d |d … }t|tt|ƒ||  ƒƒ}	t |	¡}
| |d |
|  ¡ q>||d< |S )a­  interpolate detected peak positions and surrounding data points

    Function that enables high-precision mode by taking the estimated peak position,
    then upsampling the peak position +/- 100ms to the specified sampling rate, subsequently
    estimating the peak position with higher accuracy.

    Parameters
    ----------
    data : 1d list or array
        list or array containing heart rate data

    peaks : 1d list or array
        list or array containing x-positions of peaks in signal

    sample_rate : int or float
        the sample rate of the signal (in Hz)

    desired_sampled-rate : int or float
        the sample rate to which to upsample.
        Must be sample_rate < desired_sample_rate

    Returns
    -------
    working_data : dict
        working_data dictionary object containing all of heartpy's temp objects

    Examples
    --------
    Given the output of a normal analysis and the first five peak-peak intervals:

    >>> import heartpy as hp
    >>> data, _ = hp.load_exampledata(0)
    >>> wd, m = hp.process(data, 100.0)
    >>> wd['peaklist'][0:5]
    [63, 165, 264, 360, 460]

    Now, the resolution is at max 10ms as that's the distance between data points.
    We can use the high precision mode for example to approximate a more precise
    position, for example if we had recorded at 1000Hz:

    >>> wd = interpolate_peaks(data = data, peaks = wd['peaklist'],
    ... sample_rate = 100.0, desired_sample_rate = 1000.0, working_data = wd)
    >>> wd['peaklist'][0:5]
    [63.5, 165.4, 263.6, 360.4, 460.2]

    As you can see the accuracy of peak positions has increased.
    Note that you cannot magically upsample nothing into something. Be reasonable.
    zpdesired sample rate is lower than actual sample rate this would result in downsampling which will hurt accuracy.rF   c                    s   g | ]}|ˆ  |ˆ  f‘qS r   r   r    ©Znum_samplesr   r   r%   ì  s     z%interpolate_peaks.<locals>.<listcomp>r   r   r   )ÚAssertionErrorrR   r   r   r   Zargmaxr   )r   Zpeaksr   Zdesired_sample_rater(   ÚratioZinterpolation_slicesr5   ÚsliceZ	resampledZpeakposr   rT   r   r   ¶  s    1
)r   r   r   )Ú__doc__Únumpyr   Zscipy.signalr   Zanalysisr   r   Ú
exceptionsr   Ú__all__r   r   r	   r
   r   r   r   r   r   r   r   Ú<module>   s&   ú	
D/NbF8