Search code examples
pythonarraysnumpythresholdwindowing

Find the first index for which an array goes below a certain threshold (and stay below for some time)


Let A be a 1D numpy array, a threshold t, and a window length K.

How to find the minimal index j, such that A[j:j+K] < t? (i.e. the first time A stays below the threshold on a full window of width K).

I've tried (unfinished) things with a loop, but it seemed far from optimal, and I thought maybe there's a clever "numpy way" to do it.


Sidenote: the fact we want to test if we're below the threshold during a certain window length instead of ponctual value, is useful to avoid on/off/on/off/on/off artefacts near the threshold (see also Hysteresis: "hysteresis is intentionally added to an electronic circuit to prevent unwanted rapid switching [...] compensate for contact bounce in switches, or noise in an electrical signal.").


Solution

  • Approach #1

    We can use 1D convolution -

    np.flatnonzero(np.convolve(A<t, np.ones(K,dtype=int))==K)[0]-K+1
    

    The idea is to get the boolean array after comparison against threshold and then run a 1D convolution with a kernel of length same as window, filled with 1s. This gives us sum of each sliding window. So, all windows that have sum of K are the ones we are after. Use flatnonzero to get the starting indices for the valid windows. Finally, select the first one.

    Approach #2

    With binary-erosion -

    from scipy.ndimage.morphology import binary_erosion
    
    np.flatnonzero(binary_erosion(A<t, np.ones(K), origin=-(K//2)))[0]
    

    This runs a sliding kernel with a length same as window and erodes away all windows that don't have window length True ones in sequence, leaving us with the valid ones. Again, use flatnonzero to get the indices and finally select the first one. We need to use the arg origin with the binary-erosion, so that we select the starts.

    Approach #3

    Here's another with island finding -

    # Get mask of valid elements with comparison against thresh
    mask = np.r_[False,A<t,False]
    
    # Get indices of starts and ends for the valid islands
    idx = np.flatnonzero(mask[:-1] != mask[1:])
    start,stop = idx[::2],idx[1::2]
    
    # Get the island lengths and check for lengths >=K and mask  start indices
    # and select the first one among them
    out = start[(stop - start)>=K][0]