Skip to content

Smoothness

Movement smoothness analysis module.

This module provides tools for quantifying the smoothness of movement signals using multiple metrics including SPARC (Spectral Arc Length) and Jerk RMS. Designed for real time analysis of motion capture or sensor data.

Smoothness metrics are important indicators of movement quality in: 1. Motor control assessment 2. Rehabilitation monitoring 3. Skill learning evaluation 4. Neurological disorder diagnosis

Smoothness

Compute movement smoothness metrics from signal data.

This class analyzes movement smoothness using SPARC (Spectral Arc Length) and Jerk RMS metrics. It can optionally apply Savitzky-Golay filtering to reduce noise before analysis.

Read more in the User Guide

Parameters:

Name Type Description Default
rate_hz float

Sampling rate of the signal in Hz (default: 50.0).

50.0
use_filter bool

Whether to apply Savitzky-Golay filtering before analysis (default: True).

True

Attributes:

Name Type Description
rate_hz float

Signal sampling rate.

use_filter bool

Filter application flag.

Examples:

>>> from pyeyesweb.mid_level.smoothness import Smoothness
>>> from pyeyesweb.data_models.sliding_window import SlidingWindow
>>>
>>> smooth = Smoothness(rate_hz=100.0, use_filter=True)
>>> window = SlidingWindow(max_length=200, n_columns=1)
>>>
>>> # Add movement data
>>> for value in movement_data:
...     window.append([value])
>>>
>>> result = smooth(window)
>>> print(f"SPARC: {result['sparc']:.3f}, Jerk RMS: {result['jerk_rms']:.3f}")
Notes
  1. SPARC: More negative values indicate smoother movement
  2. Jerk RMS: Lower values indicate smoother movement
  3. Requires at least 5 samples for meaningful analysis
Source code in pyeyesweb/mid_level/smoothness.py
class Smoothness:
    """Compute movement smoothness metrics from signal data.

    This class analyzes movement smoothness using SPARC (Spectral Arc Length)
    and Jerk RMS metrics. It can optionally apply Savitzky-Golay filtering
    to reduce noise before analysis.

    Read more in the [User Guide](/PyEyesWeb/user_guide/theoretical_framework/low_level/smoothness/)

    Parameters
    ----------
    rate_hz : float, optional
        Sampling rate of the signal in Hz (default: 50.0).
    use_filter : bool, optional
        Whether to apply Savitzky-Golay filtering before analysis (default: True).

    Attributes
    ----------
    rate_hz : float
        Signal sampling rate.
    use_filter : bool
        Filter application flag.

    Examples
    --------
    >>> from pyeyesweb.mid_level.smoothness import Smoothness
    >>> from pyeyesweb.data_models.sliding_window import SlidingWindow
    >>>
    >>> smooth = Smoothness(rate_hz=100.0, use_filter=True)
    >>> window = SlidingWindow(max_length=200, n_columns=1)
    >>>
    >>> # Add movement data
    >>> for value in movement_data:
    ...     window.append([value])
    >>>
    >>> result = smooth(window)
    >>> print(f"SPARC: {result['sparc']:.3f}, Jerk RMS: {result['jerk_rms']:.3f}")

    Notes
    -----
    1. SPARC: More negative values indicate smoother movement
    2. Jerk RMS: Lower values indicate smoother movement
    3. Requires at least 5 samples for meaningful analysis
    """

    def __init__(self, rate_hz=50.0, use_filter=True):
        # Validate rate_hz using centralized validator
        self.rate_hz = validate_numeric(rate_hz, 'rate_hz', min_val=0.01, max_val=100000)

        # Validate use_filter using centralized validator
        self.use_filter = validate_boolean(use_filter, 'use_filter')

    def _filter_signal(self, signal):
        """Apply Savitzky-Golay filter if enabled and enough data.

        Parameters
        ----------
        signal : array-like
            Input signal to filter.

        Returns
        -------
        ndarray
            Filtered signal or original if filtering disabled/not possible.
        """
        if not self.use_filter:
            return np.array(signal)
        return apply_savgol_filter(signal, self.rate_hz)

    def __call__(self, sliding_window: SlidingWindow):
        """Compute smoothness metrics from windowed signal data.

        Parameters
        ----------
        sliding_window : SlidingWindow
            Buffer containing signal data to analyze.

        Returns
        -------
        dict
            Dictionary containing:
            - 'sparc': Spectral Arc Length (more negative = smoother).
                      Returns NaN if insufficient data.
            - 'jerk_rms': RMS of jerk (third derivative).
                         Returns NaN if insufficient data.
        """
        if len(sliding_window) < 5:
            return {"sparc": float("nan"), "jerk_rms": float("nan")}

        signal, _ = sliding_window.to_array()

        # If multi-channel, compute for first channel only
        if signal.ndim > 1 and signal.shape[1] > 1:
            signal = signal[:, 0]

        filtered = self._filter_signal(signal.squeeze())
        normalized = normalize_signal(filtered)

        sparc = compute_sparc(normalized, self.rate_hz)
        jerk = compute_jerk_rms(filtered, self.rate_hz)

        return {"sparc": sparc, "jerk_rms": jerk}

__call__(sliding_window)

Compute smoothness metrics from windowed signal data.

Parameters:

Name Type Description Default
sliding_window SlidingWindow

Buffer containing signal data to analyze.

required

Returns:

Type Description
dict

Dictionary containing: - 'sparc': Spectral Arc Length (more negative = smoother). Returns NaN if insufficient data. - 'jerk_rms': RMS of jerk (third derivative). Returns NaN if insufficient data.

Source code in pyeyesweb/mid_level/smoothness.py
def __call__(self, sliding_window: SlidingWindow):
    """Compute smoothness metrics from windowed signal data.

    Parameters
    ----------
    sliding_window : SlidingWindow
        Buffer containing signal data to analyze.

    Returns
    -------
    dict
        Dictionary containing:
        - 'sparc': Spectral Arc Length (more negative = smoother).
                  Returns NaN if insufficient data.
        - 'jerk_rms': RMS of jerk (third derivative).
                     Returns NaN if insufficient data.
    """
    if len(sliding_window) < 5:
        return {"sparc": float("nan"), "jerk_rms": float("nan")}

    signal, _ = sliding_window.to_array()

    # If multi-channel, compute for first channel only
    if signal.ndim > 1 and signal.shape[1] > 1:
        signal = signal[:, 0]

    filtered = self._filter_signal(signal.squeeze())
    normalized = normalize_signal(filtered)

    sparc = compute_sparc(normalized, self.rate_hz)
    jerk = compute_jerk_rms(filtered, self.rate_hz)

    return {"sparc": sparc, "jerk_rms": jerk}