Source code for celldetective.utils.maths

import numpy as np
import pandas as pd
from typing import Union, List


from celldetective import get_logger

logger = get_logger()


[docs] def step_function(t: Union[np.ndarray, List], t_shift: float, dt: float) -> np.ndarray: """ Computes a step function using the logistic sigmoid function. This function calculates the value of a sigmoid function, which is often used to model a step change or transition. The sigmoid function is defined as: .. math:: f(t) = \\frac{1}{1 + \\exp{\\left( -\\frac{t - t_{shift}}{dt} \\right)}} where `t` is the input variable, `t_shift` is the point of the transition, and `dt` controls the steepness of the transition. Parameters ---------- t : array_like The input values for which the step function will be computed. t_shift : float The point in the `t` domain where the transition occurs. dt : float The parameter that controls the steepness of the transition. Smaller values make the transition steeper, while larger values make it smoother. Returns ------- array_like The computed values of the step function for each value in `t`. Examples -------- >>> import numpy as np >>> t = np.array([0, 1, 2, 3, 4, 5]) >>> t_shift = 2 >>> dt = 1 >>> step_function(t, t_shift, dt) array([0.26894142, 0.37754067, 0.5 , 0.62245933, 0.73105858, 0.81757448]) """ with np.errstate(over="raise", divide="raise"): try: return 1 / (1 + np.exp(-(t - t_shift) / dt)) except FloatingPointError as e: logger.warning( f"Math warning in step_function: {e}. t_shift={t_shift}, dt={dt}. Range of t: [{np.min(t)}, {np.max(t)}]" ) with np.errstate(over="ignore", divide="ignore"): return 1 / (1 + np.exp(-(t - t_shift) / dt))
[docs] def derivative( x: np.ndarray, timeline: np.ndarray, window: int, mode: str = "bi" ) -> np.ndarray: """ Compute the derivative of a given array of values with respect to time using a specified numerical differentiation method. Parameters ---------- x : array_like The input array of values. timeline : array_like The array representing the time points corresponding to the input values. window : int The size of the window used for numerical differentiation. Must be a positive odd integer. mode : {'bi', 'forward', 'backward'}, optional The numerical differentiation method to be used: - 'bi' (default): Bidirectional differentiation using a symmetric window. - 'forward': Forward differentiation using a one-sided window. - 'backward': Backward differentiation using a one-sided window. Returns ------- dxdt : ndarray The computed derivative values of the input array with respect to time. Raises ------ AssertionError If the window size is not an odd integer and mode is 'bi'. Notes ----- - For 'bi' mode, the window size must be an odd number. - For 'forward' mode, the derivative at the edge points may not be accurate due to the one-sided window. - For 'backward' mode, the derivative at the first few points may not be accurate due to the one-sided window. Examples -------- >>> import numpy as np >>> x = np.array([1, 2, 4, 7, 11]) >>> timeline = np.array([0, 1, 2, 3, 4]) >>> window = 3 >>> derivative(x, timeline, window, mode='bi') array([3., 3., 3.]) >>> derivative(x, timeline, window, mode='forward') array([1., 2., 3.]) >>> derivative(x, timeline, window, mode='backward') array([3., 3., 3., 3.]) """ # modes = bi, forward, backward dxdt = np.zeros(len(x)) dxdt[:] = np.nan if mode == "bi": assert window % 2 == 1, "Please set an odd window for the bidirectional mode" lower_bound = window // 2 upper_bound = len(x) - window // 2 elif mode == "forward": lower_bound = 0 upper_bound = len(x) - window elif mode == "backward": lower_bound = window upper_bound = len(x) for t in range(lower_bound, upper_bound): if mode == "bi": dxdt[t] = (x[t + window // 2] - x[t - window // 2]) / ( timeline[t + window // 2] - timeline[t - window // 2] ) elif mode == "forward": dxdt[t] = (x[t + window] - x[t]) / (timeline[t + window] - timeline[t]) elif mode == "backward": dxdt[t] = (x[t] - x[t - window]) / (timeline[t] - timeline[t - window]) return dxdt
[docs] def differentiate_per_track( tracks: pd.DataFrame, measurement: str, window_size: int = 3, mode: str = "bi" ) -> pd.DataFrame: """ Compute derivate of a measurement per track. Parameters ---------- tracks : DataFrame Tracking data. measurement : str Measurement column name. window_size : int, optional Window size for differentiation. Default is 3. mode : str, optional Differentiation mode. Default is "bi". Returns ------- DataFrame Tracking data with derivative column. """ groupby_cols = ["TRACK_ID"] if "position" in list(tracks.columns): groupby_cols = ["position"] + groupby_cols tracks = tracks.sort_values(by=groupby_cols + ["FRAME"], ignore_index=True) tracks = tracks.reset_index(drop=True) for tid, group in tracks.groupby(groupby_cols): indices = group.index timeline = group["FRAME"].values signal = group[measurement].values dsignal = derivative(signal, timeline, window_size, mode=mode) tracks.loc[indices, "d/dt." + measurement] = dsignal return tracks
[docs] def velocity_per_track( tracks: pd.DataFrame, window_size: int = 3, mode: str = "bi" ) -> pd.DataFrame: """ Compute velocity per track. Parameters ---------- tracks : DataFrame Tracking data. window_size : int, optional Window size for differentiation. Default is 3. mode : str, optional Differentiation mode. Default is "bi". Returns ------- DataFrame Tracking data with velocity column. """ groupby_cols = ["TRACK_ID"] if "position" in list(tracks.columns): groupby_cols = ["position"] + groupby_cols tracks = tracks.sort_values(by=groupby_cols + ["FRAME"], ignore_index=True) tracks = tracks.reset_index(drop=True) for tid, group in tracks.groupby(groupby_cols): indices = group.index timeline = group["FRAME"].values x = group["POSITION_X"].values y = group["POSITION_Y"].values v = velocity(x, y, timeline, window=window_size, mode=mode) v_abs = magnitude_velocity(v) tracks.loc[indices, "velocity"] = v_abs return tracks
[docs] def velocity( x: np.ndarray, y: np.ndarray, timeline: np.ndarray, window: int, mode: str = "bi" ) -> np.ndarray: """ Compute the velocity vector of a given 2D trajectory represented by arrays of x and y coordinates with respect to time using a specified numerical differentiation method. Parameters ---------- x : array_like The array of x-coordinates of the trajectory. y : array_like The array of y-coordinates of the trajectory. timeline : array_like The array representing the time points corresponding to the x and y coordinates. window : int The size of the window used for numerical differentiation. Must be a positive odd integer. mode : {'bi', 'forward', 'backward'}, optional The numerical differentiation method to be used: - 'bi' (default): Bidirectional differentiation using a symmetric window. - 'forward': Forward differentiation using a one-sided window. - 'backward': Backward differentiation using a one-sided window. Returns ------- v : ndarray The computed velocity vector of the 2D trajectory with respect to time. The first column represents the x-component of velocity, and the second column represents the y-component. Raises ------ AssertionError If the window size is not an odd integer and mode is 'bi'. Notes ----- - For 'bi' mode, the window size must be an odd number. - For 'forward' mode, the velocity at the edge points may not be accurate due to the one-sided window. - For 'backward' mode, the velocity at the first few points may not be accurate due to the one-sided window. Examples -------- >>> import numpy as np >>> x = np.array([1, 2, 4, 7, 11]) >>> y = np.array([0, 3, 5, 8, 10]) >>> timeline = np.array([0, 1, 2, 3, 4]) >>> window = 3 >>> velocity(x, y, timeline, window, mode='bi') array([[3., 3.], [3., 3.]]) >>> velocity(x, y, timeline, window, mode='forward') array([[2., 2.], [3., 3.]]) >>> velocity(x, y, timeline, window, mode='backward') array([[3., 3.], [3., 3.]]) """ v = np.zeros((len(x), 2)) v[:, :] = np.nan v[:, 0] = derivative(x, timeline, window, mode=mode) v[:, 1] = derivative(y, timeline, window, mode=mode) return v
[docs] def magnitude_velocity(v_matrix: np.ndarray) -> np.ndarray: """ Compute the magnitude of velocity vectors given a matrix representing 2D velocity vectors. Parameters ---------- v_matrix : array_like The matrix where each row represents a 2D velocity vector with the first column being the x-component and the second column being the y-component. Returns ------- magnitude : ndarray The computed magnitudes of the input velocity vectors. Notes ----- - If a velocity vector has NaN components, the corresponding magnitude will be NaN. - The function handles NaN values in the input matrix gracefully. Examples -------- >>> import numpy as np >>> v_matrix = np.array([[3, 4], ... [2, 2], ... [3, 3]]) >>> magnitude_velocity(v_matrix) array([5., 2.82842712, 4.24264069]) >>> v_matrix_with_nan = np.array([[3, 4], ... [np.nan, 2], ... [3, np.nan]]) >>> magnitude_velocity(v_matrix_with_nan) array([5., nan, nan]) """ magnitude = np.zeros(len(v_matrix)) magnitude[:] = np.nan for i in range(len(v_matrix)): if v_matrix[i, 0] == v_matrix[i, 0]: magnitude[i] = np.sqrt(v_matrix[i, 0] ** 2 + v_matrix[i, 1] ** 2) return magnitude
[docs] def orientation(v_matrix: np.ndarray) -> np.ndarray: """ Compute the orientation angles (in radians) of 2D velocity vectors given a matrix representing velocity vectors. Parameters ---------- v_matrix : array_like The matrix where each row represents a 2D velocity vector with the first column being the x-component and the second column being the y-component. Returns ------- orientation_array : ndarray The computed orientation angles of the input velocity vectors in radians. If a velocity vector has NaN components, the corresponding orientation angle will be NaN. Examples -------- >>> import numpy as np >>> v_matrix = np.array([[3, 4], ... [2, 2], ... [-3, -3]]) >>> orientation(v_matrix) array([0.92729522, 0.78539816, -2.35619449]) >>> v_matrix_with_nan = np.array([[3, 4], ... [np.nan, 2], ... [3, np.nan]]) >>> orientation(v_matrix_with_nan) array([0.92729522, nan, nan]) """ orientation_array = np.zeros(len(v_matrix)) for t in range(len(orientation_array)): if v_matrix[t, 0] == v_matrix[t, 0]: orientation_array[t] = np.arctan2(v_matrix[t, 0], v_matrix[t, 1]) return orientation_array
[docs] def safe_log(array: Union[int, float, List, np.ndarray]) -> Union[float, np.ndarray]: """ Safely computes the base-10 logarithm for numeric inputs, handling invalid or non-positive values. Parameters ---------- array : int, float, list, or numpy.ndarray The input value or array for which to compute the logarithm. Can be a single number (int or float), a list, or a numpy array. Returns ------- float or numpy.ndarray - If the input is a single numeric value, returns the base-10 logarithm as a float, or `np.nan` if the value is non-positive. - If the input is a list or numpy array, returns a numpy array with the base-10 logarithm of each element. Invalid or non-positive values are replaced with `np.nan`. Notes ----- - Non-positive values (`<= 0`) are considered invalid and will result in `np.nan`. - NaN values in the input array are preserved in the output. - If the input is a list, it is converted to a numpy array for processing. Examples -------- >>> safe_log(10) 1.0 >>> safe_log(-5) nan >>> safe_log([10, 0, -5, 100]) array([1.0, nan, nan, 2.0]) >>> import numpy as np >>> safe_log(np.array([1, 10, 100])) array([0.0, 1.0, 2.0]) """ array = np.asarray(array, dtype=float) result = np.where(array > 0, np.log10(array), np.nan) return result.item() if np.isscalar(array) else result