coco_pipe.dim_reduction.evaluation.geometry =========================================== .. py:module:: coco_pipe.dim_reduction.evaluation.geometry .. autoapi-nested-parse:: Trajectory geometry metrics and smoothing utilities. This module provides small, generic helpers for analyzing ordered trajectories in embedded spaces. The functions are reducer-agnostic and operate on standard NumPy arrays rather than domain-specific container types. Functions --------- moving_average Smooth a one-dimensional timecourse with a valid-mode moving average. trajectory_acceleration Compute instantaneous acceleration magnitude from second-order derivatives. trajectory_jerk Compute instantaneous jerk magnitude (derivative of acceleration). trajectory_speed Compute instantaneous speed from first-order trajectory differences. trajectory_curvature Compute geometric curvature from first- and second-order derivatives. trajectory_dispersion Compute within-group trajectory spread across time. trajectory_displacement Compute displacement from the initial trajectory state across time. trajectory_path_length Compute total or cumulative path length. trajectory_separation Compute time-resolved group separation across time using a selected method. trajectory_tortuosity Compute the ratio between path length and net displacement. trajectory_turning_angle Compute local turning angles between consecutive trajectory segments. Author: Hamza Abdelhedi (hamza.abdelhedi@umontreal.ca) Functions --------- .. autoapisummary:: coco_pipe.dim_reduction.evaluation.geometry.moving_average coco_pipe.dim_reduction.evaluation.geometry.trajectory_acceleration coco_pipe.dim_reduction.evaluation.geometry.trajectory_jerk coco_pipe.dim_reduction.evaluation.geometry.trajectory_speed coco_pipe.dim_reduction.evaluation.geometry.trajectory_curvature coco_pipe.dim_reduction.evaluation.geometry.trajectory_path_length coco_pipe.dim_reduction.evaluation.geometry.trajectory_displacement coco_pipe.dim_reduction.evaluation.geometry.trajectory_tortuosity coco_pipe.dim_reduction.evaluation.geometry.trajectory_turning_angle coco_pipe.dim_reduction.evaluation.geometry.trajectory_dispersion coco_pipe.dim_reduction.evaluation.geometry.trajectory_separation coco_pipe.dim_reduction.evaluation.geometry.trajectory_distance_from_center coco_pipe.dim_reduction.evaluation.geometry.trajectory_cohesion coco_pipe.dim_reduction.evaluation.geometry.trajectory_intra_spread coco_pipe.dim_reduction.evaluation.geometry.trajectory_auc_speed Module Contents --------------- .. py:function:: moving_average(arr, window) Smooth a one-dimensional array with a valid-mode moving average. :param arr: Input array to smooth. :type arr: np.ndarray of shape (n_samples,) :param window: Size of the smoothing window. Must be a positive integer no larger than the array length. :type window: int :returns: Smoothed array. The output length is ``n_samples - window + 1``. If ``window == 1``, a copy of the input is returned. :rtype: np.ndarray :raises ValueError: If ``arr`` is not one-dimensional, if ``window`` is not positive, or if ``window`` is larger than the input length. .. seealso:: :py:obj:`trajectory_speed` First-order trajectory dynamics without smoothing. :py:obj:`trajectory_turning_angle` Local directional changes along a trajectory. .. rubric:: Examples >>> import numpy as np >>> moving_average(np.array([1, 2, 3, 4, 5]), window=3) array([2., 3., 4.]) .. py:function:: trajectory_acceleration(traj, dt = 1.0) Calculate instantaneous acceleration magnitude. :param traj: Trajectory array. The second-to-last axis is interpreted as time and the last axis as coordinates. :type traj: np.ndarray of shape (..., n_times, n_dims) :param dt: Uniform time step between consecutive samples. :type dt: float, default=1.0 :returns: Acceleration-magnitude timecourse aligned with the input time axis. :rtype: np.ndarray of shape (..., n_times) :raises ValueError: If ``traj`` has fewer than two dimensions, contains fewer than three time points, or if ``dt <= 0``. .. seealso:: :py:obj:`trajectory_speed` First-order trajectory dynamics. :py:obj:`trajectory_curvature` Geometric bending of a trajectory. :py:obj:`trajectory_turning_angle` Local directional changes between segments. .. rubric:: Examples >>> import numpy as np >>> t = np.linspace(0.0, 2.0, 3) >>> traj = np.stack([t**2, np.zeros_like(t)], axis=1) >>> trajectory_acceleration(traj, dt=1.0).shape (3,) .. py:function:: trajectory_jerk(traj, dt = 1.0) Calculate instantaneous jerk magnitude (derivative of acceleration). Jerk is the third derivative of position with respect to time, or equivalently the first derivative of the acceleration vector. A sudden spike in jerk means the trajectory "snapped" into a new movement, often correlating with discrete cognitive decisions or abrupt state transitions. :param traj: Trajectory array. The second-to-last axis is interpreted as time and the last axis as coordinates. :type traj: np.ndarray of shape (..., n_times, n_dims) :param dt: Uniform time step between consecutive samples. :type dt: float, default=1.0 :returns: Jerk-magnitude timecourse aligned with the input time axis. :rtype: np.ndarray of shape (..., n_times) :raises ValueError: If ``traj`` has fewer than two dimensions, contains fewer than four time points, or if ``dt <= 0``. .. seealso:: :py:obj:`trajectory_acceleration` Second-order trajectory dynamics. :py:obj:`trajectory_speed` First-order trajectory dynamics. :py:obj:`trajectory_curvature` Geometric bending of a trajectory. .. rubric:: Examples >>> import numpy as np >>> t = np.linspace(0.0, 3.0, 4) >>> traj = np.stack([t**3, np.zeros_like(t)], axis=1) >>> trajectory_jerk(traj, dt=1.0).shape (4,) .. py:function:: trajectory_speed(traj, dt = 1.0, time = None) Calculate instantaneous trajectory speed. :param traj: Trajectory array. The second-to-last axis is interpreted as time and the last axis as coordinates. :type traj: np.ndarray of shape (..., n_times, n_dims) :param dt: Uniform time step between consecutive samples. Ignored when ``time`` is provided. :type dt: float, default=1.0 :param time: Real timestamps (e.g. milliseconds) for non-uniform sampling. When provided, per-step ``dt`` values are derived from ``np.diff(time)`` and the output has shape ``(..., n_times - 1)`` — no padding is applied because the associated time axis is ``time[:-1]``. :type time: np.ndarray of shape (n_times,), optional :returns: - ``time=None``: shape ``(..., n_times)``, last value padded. - ``time`` provided: shape ``(..., n_times - 1)``, no padding. :rtype: np.ndarray :raises ValueError: If ``traj`` has fewer than two dimensions, fewer than two time points, ``dt <= 0`` (uniform mode), or ``time`` length mismatches ``traj``. .. rubric:: Notes Speed is the Euclidean norm of the first difference divided by the time step. .. seealso:: :py:obj:`trajectory_acceleration` Second-order trajectory dynamics. :py:obj:`trajectory_path_length` Total or cumulative traveled distance. :py:obj:`trajectory_displacement` Distance from the initial state across time. .. rubric:: Examples >>> import numpy as np >>> traj = np.array([[0.0, 0.0], [1.0, 0.0], [2.0, 0.0]]) >>> trajectory_speed(traj) array([1., 1., 1.]) >>> trajectory_speed(traj, time=np.array([0.0, 100.0, 250.0])) array([0.01 , 0.00666667]) .. py:function:: trajectory_curvature(traj, method = 'cosine') Calculate geometric curvature of a trajectory. :param traj: Trajectory array. The second-to-last axis is interpreted as time and the last axis as coordinates. :type traj: np.ndarray of shape (..., n_times, n_dims) :param method: Formula used to compute curvature. ``"cosine"`` — discrete turning-angle formula: the angle between consecutive step vectors divided by the step length. Output shape is ``(..., n_times - 2)`` because two differencing operations are required. ``"gradient"`` — continuous formula using first and second derivatives: ``sqrt(||v||^2 ||a||^2 - (v·a)^2) / ||v||^3``. Assumes uniformly spaced samples. Output shape is ``(..., n_times)``. :type method: {"cosine", "gradient"}, default="cosine" :returns: - ``method="cosine"``: shape ``(..., n_times - 2)`` - ``method="gradient"``: shape ``(..., n_times)`` :rtype: np.ndarray :raises ValueError: If ``traj`` has fewer than two dimensions, insufficient time points, or an unsupported ``method`` is given. .. seealso:: :py:obj:`trajectory_turning_angle` Discrete local turning angles (no curvature scaling). :py:obj:`trajectory_tortuosity` Path inefficiency relative to net displacement. :py:obj:`trajectory_speed` First-order trajectory dynamics. .. rubric:: Examples >>> import numpy as np >>> t = np.linspace(0, 2 * np.pi, 100) >>> traj = np.stack([np.cos(t), np.sin(t)], axis=1) >>> trajectory_curvature(traj, method="gradient").shape (100,) >>> trajectory_curvature(traj, method="cosine").shape (98,) .. py:function:: trajectory_path_length(traj, *, cumulative = False) Calculate trajectory path length. :param traj: Trajectory array. The second-to-last axis is interpreted as time and the last axis as coordinates. :type traj: np.ndarray of shape (..., n_times, n_dims) :param cumulative: If ``True``, return cumulative path length aligned with the input time axis. Otherwise return total path length for each trajectory. :type cumulative: bool, default=False :returns: Total path length with shape ``(...)`` when ``cumulative=False``, or cumulative path length with shape ``(..., n_times)`` when ``cumulative=True``. :rtype: np.ndarray .. rubric:: Notes NaN-valued steps are skipped in both the total and cumulative modes so that a single missing coordinate does not invalidate the whole result. .. seealso:: :py:obj:`trajectory_displacement` Distance from the initial state across time. :py:obj:`trajectory_tortuosity` Ratio of path length to net displacement. :py:obj:`trajectory_speed` First-order local motion magnitude. .. rubric:: Examples >>> import numpy as np >>> traj = np.array([[0.0, 0.0], [1.0, 0.0], [2.0, 0.0]]) >>> trajectory_path_length(traj) np.float64(2.0) .. py:function:: trajectory_displacement(traj, *, final = False) Calculate displacement from the initial state. :param traj: Trajectory array. The second-to-last axis is interpreted as time and the last axis as coordinates. :type traj: np.ndarray of shape (..., n_times, n_dims) :param final: If ``False`` (default), return the displacement timecourse from the first point at every time index — shape ``(..., n_times)``. If ``True``, return only the scalar net displacement from the first to the last point — shape ``(...)``. :type final: bool, default=False :returns: - ``final=False``: shape ``(..., n_times)`` - ``final=True``: shape ``(...)`` :rtype: np.ndarray .. seealso:: :py:obj:`trajectory_path_length` Total or cumulative traveled distance. :py:obj:`trajectory_tortuosity` Ratio of traveled distance to final displacement. .. rubric:: Examples >>> import numpy as np >>> traj = np.array([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0]]) >>> trajectory_displacement(traj) array([0. , 1. , 1.41421356]) >>> trajectory_displacement(traj, final=True) np.float64(1.4142135623730951) .. py:function:: trajectory_tortuosity(traj, eps = 1e-08) Calculate trajectory tortuosity. Tortuosity is defined as total path length divided by net displacement from the initial to the final state. :param traj: Trajectory array. The second-to-last axis is interpreted as time and the last axis as coordinates. :type traj: np.ndarray of shape (..., n_times, n_dims) :param eps: Small constant used to identify near-zero displacement. :type eps: float, default=1e-8 :returns: Tortuosity for each trajectory. Stationary trajectories return ``1.0``; trajectories with nonzero path length but near-zero net displacement return ``np.inf``. :rtype: np.ndarray of shape (...) .. seealso:: :py:obj:`trajectory_path_length` Total traveled distance along the path. :py:obj:`trajectory_displacement` Net displacement from start to end. :py:obj:`trajectory_curvature` Local geometric bending. .. rubric:: Examples >>> import numpy as np >>> traj = np.array([[0.0, 0.0], [1.0, 0.0], [2.0, 0.0]]) >>> trajectory_tortuosity(traj) np.float64(1.0) .. py:function:: trajectory_turning_angle(traj) Calculate local turning angles between consecutive trajectory segments. :param traj: Trajectory array. The second-to-last axis is interpreted as time and the last axis as coordinates. :type traj: np.ndarray of shape (..., n_times, n_dims) :returns: Turning-angle timecourse in radians. The output is shorter than the input by two points because each angle requires one predecessor and one successor segment. :rtype: np.ndarray of shape (..., n_times - 2) .. seealso:: :py:obj:`trajectory_curvature` Curvature computed from turning angles (cosine method). :py:obj:`trajectory_speed` Local motion magnitude. :py:obj:`trajectory_path_length` Total or cumulative traveled distance. .. rubric:: Examples >>> import numpy as np >>> traj = np.array([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0]]) >>> trajectory_turning_angle(traj) array([1.57079633]) .. py:function:: trajectory_dispersion(traj, labels = None) Calculate within-group trajectory dispersion across time. :param traj: Trial trajectory tensor. :type traj: np.ndarray of shape (n_trials, n_times, n_dims) :param labels: Optional group label for each trial. If omitted, a single global dispersion timecourse is returned. :type labels: np.ndarray of shape (n_trials,), optional :returns: Global dispersion timecourse when ``labels`` is omitted, otherwise a mapping from label to dispersion timecourse. :rtype: np.ndarray or dict[str, np.ndarray] .. seealso:: :py:obj:`trajectory_separation` Unified separation entrypoint. :py:obj:`trajectory_separation` Use ``method="within_between_ratio"`` for normalized separation. .. rubric:: Examples >>> import numpy as np >>> traj = np.zeros((2, 3, 2)) >>> traj[1, :, 0] = 1.0 >>> trajectory_dispersion(traj) array([0.5, 0.5, 0.5]) .. py:function:: trajectory_separation(traj, labels, method = 'centroid', **kwargs) Calculate time-resolved separation between labeled trajectory groups. :param traj: Trajectory tensor containing one trajectory per trial. :type traj: np.ndarray of shape (n_trials, n_times, n_dims) :param labels: Class label for each trial. :type labels: np.ndarray of shape (n_trials,) :param method: "distributional", "margin"}, default="centroid" Separation definition to compute. :type method: {"centroid", "within_between_ratio", "mahalanobis", :param \*\*kwargs: Additional keyword arguments forwarded to the selected separation method. :type \*\*kwargs: dict :returns: Mapping from label pairs to separation timecourses of shape ``(n_times,)``. :rtype: dict[tuple[str, str], np.ndarray] :raises ValueError: If the inputs are invalid or if an unsupported separation method is requested. .. rubric:: Notes This is the high-level separation entrypoint for trajectory-group comparison. It dispatches to the more specific separation primitives in this module. Supported methods: - ``"centroid"``: Euclidean distance between label centroids. - ``"within_between_ratio"``: Between-centroid distance normalized by within-group dispersion. - ``"mahalanobis"``: Covariance-aware centroid separation. - ``"distributional"``: Energy-distance separation between trial clouds. - ``"margin"``: Nearest-cross minus nearest-within margin separation. .. seealso:: :py:obj:`trajectory_dispersion` Within-group spread used by some separation methods. .. rubric:: Examples >>> import numpy as np >>> traj = np.zeros((4, 5, 2)) >>> labels = np.array(["A", "A", "B", "B"]) >>> sep = trajectory_separation(traj, labels, method="centroid") >>> list(sep.keys()) [('A', 'B')] .. py:function:: trajectory_distance_from_center(traj) Compute each point's Euclidean distance from the trajectory's own spatial centroid. The centroid is the mean position across all time points of a single trajectory. This is distinct from :func:`trajectory_dispersion`, which measures spread *across trials* at each fixed time point. :param traj: Trajectory array. The second-to-last axis is interpreted as time and the last axis as coordinates. :type traj: np.ndarray of shape (..., n_times, n_dims) :returns: Per-point distance from the temporal centroid. :rtype: np.ndarray of shape (..., n_times) .. seealso:: :py:obj:`trajectory_cohesion` Mean of this timecourse (compactness scalar). :py:obj:`trajectory_intra_spread` Std of this timecourse (variability scalar). :py:obj:`trajectory_dispersion` Across-trial spread at each time point. .. rubric:: Examples >>> import numpy as np >>> traj = np.array([[0.0, 0.0], [2.0, 0.0], [0.0, 0.0]]) >>> trajectory_distance_from_center(traj) array([0.66666667, 1.33333333, 0.66666667]) .. py:function:: trajectory_cohesion(traj) Mean distance from the trajectory's own spatial centroid. A small value means the trajectory stays near its average position (compact loop or oscillation); a large value means it sweeps far from its center. :param traj: Trajectory array. The second-to-last axis is interpreted as time and the last axis as coordinates. :type traj: np.ndarray of shape (..., n_times, n_dims) :returns: Cohesion scalar for each trajectory in the batch. :rtype: np.ndarray of shape (...) .. seealso:: :py:obj:`trajectory_distance_from_center` Full per-point timecourse. :py:obj:`trajectory_intra_spread` Complementary variability measure. .. rubric:: Examples >>> import numpy as np >>> traj = np.array([[0.0, 0.0], [2.0, 0.0], [0.0, 0.0]]) >>> trajectory_cohesion(traj) np.float64(0.888...) .. py:function:: trajectory_intra_spread(traj) Standard deviation of distances from the trajectory's own spatial centroid. Measures how variable the distance-from-center is across time: a trajectory that uniformly orbits its centroid has low intra-spread; one that starts close and ends far away has high intra-spread. Not to be confused with :func:`trajectory_separation`, which compares the centroids of *two separate trial groups*. :param traj: Trajectory array. The second-to-last axis is interpreted as time and the last axis as coordinates. :type traj: np.ndarray of shape (..., n_times, n_dims) :returns: Intra-spread scalar for each trajectory in the batch. :rtype: np.ndarray of shape (...) .. seealso:: :py:obj:`trajectory_distance_from_center` Full per-point timecourse. :py:obj:`trajectory_cohesion` Complementary mean measure. :py:obj:`trajectory_separation` Between-group separation (different concept). .. rubric:: Examples >>> import numpy as np >>> traj = np.array([[0.0, 0.0], [2.0, 0.0], [0.0, 0.0]]) >>> trajectory_intra_spread(traj) np.float64(0.314...) .. py:function:: trajectory_auc_speed(traj, dt = 1.0, time = None) Area under the instantaneous speed curve (trapezoidal integration). Integrates speed over time, giving a measure of total kinetic activity that weights fast periods more heavily than :func:`trajectory_path_length`. Units are ``[spatial_units · time_units]`` when ``time`` is provided, or ``[spatial_units · samples]`` with uniform ``dt=1``. :param traj: Trajectory array. The second-to-last axis is interpreted as time and the last axis as coordinates. :type traj: np.ndarray of shape (..., n_times, n_dims) :param dt: Uniform time step. Ignored when ``time`` is provided. :type dt: float, default=1.0 :param time: Real timestamps for non-uniform integration. When provided, per-step ``dt`` values are derived from ``np.diff(time)`` and the speed is integrated against ``time[:-1]``. :type time: np.ndarray of shape (n_times,), optional :returns: AUC-speed scalar for each trajectory in the batch. :rtype: np.ndarray of shape (...) :raises ValueError: If ``traj`` has fewer than two time points, ``dt <= 0`` (uniform mode), or ``time`` length mismatches ``traj``. .. seealso:: :py:obj:`trajectory_speed` Per-step speed timecourse. :py:obj:`trajectory_path_length` Total path length (uniform speed weighting). .. rubric:: Examples >>> import numpy as np >>> traj = np.array([[0.0, 0.0], [1.0, 0.0], [2.0, 0.0]]) >>> trajectory_auc_speed(traj, dt=1.0) np.float64(1.0) >>> trajectory_auc_speed(traj, time=np.array([0.0, 100.0, 200.0])) np.float64(100.0)