"""
Upcrossing Analysis Functions
=============================
This module contains a collection of functions that facilitate upcrossing
analyses.
Key Functions:
--------------
- `upcrossing`: Finds the zero upcrossing points.
- `peaks`: Finds the peaks between zero crossings.
- `troughs`: Finds the troughs between zero crossings.
- `heights`: Calculates the height between zero crossings.
- `periods`: Calculates the period between zero crossings.
- `custom`: Applies a custom, user-defined function between zero crossings.
Author:
-------
mbruggs
akeeste
Date:
-----
2023-10-10
"""
from typing import Callable, Optional
import numpy as np
def _apply(
t: np.ndarray,
data: np.ndarray,
f: Callable[[int, int], float],
inds: Optional[np.ndarray] = None,
) -> np.ndarray:
"""
Apply a function `f` over intervals defined by `inds`. If `inds` is None,
compute the indices using the upcrossing function.
Parameters
----------
t : np.ndarray
Time array.
data : np.ndarray
Data array.
f : Callable[[int, int], float]
A function to apply to pairs of indices (start, end).
inds : np.ndarray, optional
Indices that define the intervals. If None, `upcrossing` is used to generate them.
Returns
-------
np.ndarray
Array of values resulting from applying `f` over the intervals.
"""
if inds is None:
inds = upcrossing(t, data)
n = inds.size - 1
vals = np.empty(n)
for i in range(n):
vals[i] = f(inds[i], inds[i + 1])
return vals
[docs]
def upcrossing(t: np.ndarray, data: np.ndarray) -> np.ndarray:
"""
Finds the zero upcrossing points.
Parameters
----------
t: np.array
Time array.
data: np.array
Signal time series.
Returns
-------
inds: np.array
Zero crossing indices
"""
# Check data types
if not isinstance(t, np.ndarray):
raise TypeError(f"t must be of type np.ndarray. Got: {type(t)}")
if not isinstance(data, np.ndarray):
raise TypeError(f"data must be of type np.ndarray. Got: {type(data)}")
if len(data.shape) != 1:
raise ValueError("only 1D data supported, try calling squeeze()")
# eliminate zeros
zero_mask = data == 0
data[zero_mask] = 0.5 * np.min(np.abs(data))
# zero up-crossings
diff = np.diff(np.sign(data))
zero_upcrossings_mask = (diff == 2) | (diff == 1)
zero_upcrossings_index = np.where(zero_upcrossings_mask)[0]
return zero_upcrossings_index
[docs]
def peaks(
t: np.ndarray, data: np.ndarray, inds: Optional[np.ndarray] = None
) -> np.ndarray:
"""
Finds the peaks between zero crossings.
Parameters
----------
t: np.array
Time array.
data: np.array
Signal time-series.
inds : np.ndarray, optional
Optional indices for the upcrossing. Useful
when using several of the upcrossing methods
to avoid repeating the upcrossing analysis
each time.
Returns
-------
peaks: np.array
Peak values of the time-series
"""
# Check data types
if not isinstance(t, np.ndarray):
raise TypeError(f"t must be of type np.ndarray. Got: {type(t)}")
if not isinstance(data, np.ndarray):
raise TypeError(f"data must be of type np.ndarray. Got: {type(data)}")
return _apply(t, data, lambda ind1, ind2: np.max(data[ind1:ind2]), inds)
[docs]
def troughs(
t: np.ndarray, data: np.ndarray, inds: Optional[np.ndarray] = None
) -> np.ndarray:
"""
Finds the troughs between zero crossings.
Parameters
----------
t: np.array
Time array.
data: np.array
Signal time-series.
inds: np.array, optional
Optional indices for the upcrossing. Useful
when using several of the upcrossing methods
to avoid repeating the upcrossing analysis
each time.
Returns
-------
troughs: np.array
Trough values of the time-series
"""
# Check data types
if not isinstance(t, np.ndarray):
raise TypeError(f"t must be of type np.ndarray. Got: {type(t)}")
if not isinstance(data, np.ndarray):
raise TypeError(f"data must be of type np.ndarray. Got: {type(data)}")
return _apply(t, data, lambda ind1, ind2: np.min(data[ind1:ind2]), inds)
[docs]
def heights(
t: np.ndarray, data: np.ndarray, inds: Optional[np.ndarray] = None
) -> np.ndarray:
"""
Calculates the height between zero crossings.
The height is defined as the max value - min value
between the zero crossing points.
Parameters
----------
t: np.array
Time array.
data: np.array
Signal time-series.
inds: np.array, optional
Optional indices for the upcrossing. Useful
when using several of the upcrossing methods
to avoid repeating the upcrossing analysis
each time.
Returns
-------
heights: np.array
Height values of the time-series
"""
# Check data types
if not isinstance(t, np.ndarray):
raise TypeError(f"t must be of type np.ndarray. Got: {type(t)}")
if not isinstance(data, np.ndarray):
raise TypeError(f"data must be of type np.ndarray. Got: {type(data)}")
def func(ind1, ind2):
return np.max(data[ind1:ind2]) - np.min(data[ind1:ind2])
return _apply(t, data, func, inds)
[docs]
def periods(
t: np.ndarray, data: np.ndarray, inds: Optional[np.ndarray] = None
) -> np.ndarray:
"""
Calculates the period between zero crossings.
Parameters
----------
t: np.array
Time array.
data: np.array
Signal time-series.
inds: np.array, optional
Optional indices for the upcrossing. Useful
when using several of the upcrossing methods
to avoid repeating the upcrossing analysis
each time.
Returns
-------
periods: np.array
Period values of the time-series
"""
# Check data types
if not isinstance(t, np.ndarray):
raise TypeError(f"t must be of type np.ndarray. Got: {type(t)}")
if not isinstance(data, np.ndarray):
raise TypeError(f"data must be of type np.ndarray. Got: {type(data)}")
return _apply(t, data, lambda ind1, ind2: t[ind2] - t[ind1], inds)
[docs]
def custom(
t: np.ndarray,
data: np.ndarray,
func: Callable[[int, int], np.ndarray],
inds: Optional[np.ndarray] = None,
) -> np.ndarray:
"""
Applies a custom function to the timeseries data between upcrossing points.
Parameters
----------
t: np.array
Time array.
data: np.array
Signal time-series.
func: Callable[[int, int], np.ndarray]
Function to apply between the zero crossing periods
given t[ind1], t[ind2], where ind1 < ind2, correspond
to the start and end of an upcrossing section.
inds: np.array, optional
Optional indices for the upcrossing. Useful
when using several of the upcrossing methods
to avoid repeating the upcrossing analysis
each time.
Returns
-------
values: np.array
Custom values of the time-series
"""
# Check data types
if not isinstance(t, np.ndarray):
raise TypeError(f"t must be of type np.ndarray. Got: {type(t)}")
if not isinstance(data, np.ndarray):
raise TypeError(f"data must be of type np.ndarray. Got: {type(data)}")
if not callable(func):
raise ValueError("func must be callable")
return _apply(t, data, func, inds)