Smoothness

Smoothness

Bases: DynamicFeature

Calculates movement smoothness metrics from a 1D speed profile.

Tip

You can calculate smoothness via Spectral Arc Length (SPARC) or Jerk RMS.

Read more in the User Guide.

Parameters:
  • rate_hz (float, default: 50.0 ) –

    Sampling rate in Hz. Defaults to 50.0.

  • use_filter (bool, default: True ) –

    Whether to apply Savitzky-Golay filtering. Defaults to True.

  • metrics (list of {'sparc', 'jerk_rms'}, default: None ) –

    Metrics to calculate. Defaults to all allowed metrics.

  • sparc_amplitude_threshold (float, default: 0.05 ) –

    Amplitude threshold for SPARC. Defaults to 0.05.

  • sparc_min_fc (float, default: 2.0 ) –

    Minimum cutoff frequency for SPARC. Defaults to 2.0.

  • sparc_max_fc (float, default: 20.0 ) –

    Maximum cutoff frequency for SPARC. Defaults to 20.0.

Source code in pyeyesweb/low_level/smoothness.py
class Smoothness(DynamicFeature):
    """Calculates movement smoothness metrics from a 1D speed profile.

    !!! tip
        You can calculate smoothness via Spectral Arc Length (SPARC) or Jerk RMS.

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

    Parameters
    ----------
    rate_hz : float, optional
        Sampling rate in Hz. Defaults to `50.0`.
    use_filter : bool, optional
        Whether to apply Savitzky-Golay filtering. Defaults to `True`.
    metrics : list of {'sparc', 'jerk_rms'}, optional
        Metrics to calculate. Defaults to all allowed metrics.
    sparc_amplitude_threshold : float, optional
        Amplitude threshold for SPARC. Defaults to `0.05`.
    sparc_min_fc : float, optional
        Minimum cutoff frequency for SPARC. Defaults to `2.0`.
    sparc_max_fc : float, optional
        Maximum cutoff frequency for SPARC. Defaults to `20.0`.
    """

    _ALLOWED_METRICS = ["sparc", "jerk_rms"]

    def __init__(
            self,
            rate_hz: float = 50.0,
            use_filter: bool = True,
            metrics: List[Literal["sparc", "jerk_rms"]] = None,
            sparc_amplitude_threshold: float = 0.05,
            sparc_min_fc: float = 2.0,
            sparc_max_fc: float = 20.0
    ):
        super().__init__()
        # Initializing through setters natively routes the values to the validators
        self.rate_hz = rate_hz
        self.use_filter = use_filter

        self.sparc_min_fc = sparc_min_fc
        self.sparc_max_fc = sparc_max_fc
        self.sparc_threshold = sparc_amplitude_threshold

        self.metrics = metrics

    @property
    def rate_hz(self) -> float:
        return self._rate_hz

    @rate_hz.setter
    def rate_hz(self, value: float):
        self._rate_hz = validate_numeric(value, 'rate_hz', min_val=0.01, max_val=100000)

    @property
    def use_filter(self) -> bool:
        return self._use_filter

    @use_filter.setter
    def use_filter(self, value: bool):
        self._use_filter = validate_boolean(value, 'use_filter')

    @property
    def sparc_threshold(self) -> float:
        return self._sparc_threshold

    @sparc_threshold.setter
    def sparc_threshold(self, value: float):
        self._sparc_threshold = validate_numeric(value, 'sparc_threshold', min_val=0.0, max_val=1.0)

    @property
    def sparc_min_fc(self) -> float:
        return self._sparc_min_fc

    @sparc_min_fc.setter
    def sparc_min_fc(self, value: float):
        self._sparc_min_fc = validate_numeric(value, 'sparc_min_fc', min_val=0.1)

    @property
    def sparc_max_fc(self) -> float:
        return self._sparc_max_fc

    @sparc_max_fc.setter
    def sparc_max_fc(self, value: float):
        # We ensure that this validator uses the dynamic minimum
        min_fc = getattr(self, '_sparc_min_fc', 0.1)
        self._sparc_max_fc = validate_numeric(value, 'sparc_max_fc', min_val=min_fc)

    @property
    def metrics(self) -> List[str]:
        return self._metrics

    @metrics.setter
    def metrics(self, value: Optional[List[str]]):
        target_metrics = value if value is not None else self._ALLOWED_METRICS
        self._metrics = [validate_string(m, self._ALLOWED_METRICS) for m in target_metrics]

    def _filter_signal(self, signal: np.ndarray) -> np.ndarray:
        """Applies Savitzky-Golay filter if enabled."""
        if not self.use_filter:
            return signal
        return apply_savgol_filter(signal, self.rate_hz)

    def compute(self, window_data: np.ndarray) -> SmoothnessResult:
        """Executes smoothness calculation on the speed profile.

        Parameters
        ----------
        window_data : numpy.ndarray
            A 1D array representing the speed profile within the window.

        Returns
        -------
        SmoothnessResult
            The computed smoothness metrics.
        """
        if window_data.size != window_data.shape[0]:
            raise ValueError("Smoothness expects a 1D speed profile.")

        speed_profile = window_data.ravel()

        # Minimum sample threshold for FFT
        if len(speed_profile) < 10:
            return SmoothnessResult(is_valid=False)

        # 1. Preprocessing (Filtering)
        filtered_speed = self._filter_signal(speed_profile)

        # 2. Metrics calculation
        sparc_val = None
        jerk_val = None

        if "sparc" in self.metrics:
            # Pass custom parameters to the function in math_utils
            sparc_val = float(compute_sparc(
                filtered_speed, 
                rate_hz=self.rate_hz,
                amplitude_threshold=self.sparc_threshold,
                min_fc=self.sparc_min_fc,
                max_fc=self.sparc_max_fc
            ))

        if "jerk_rms" in self.metrics:
            # Standard Jerk RMS calculation from velocity
            jerk_val = float(compute_jerk_rms(filtered_speed, self.rate_hz, signal_type='velocity'))

        return SmoothnessResult(sparc=sparc_val, jerk_rms=jerk_val)

compute(window_data)

Executes smoothness calculation on the speed profile.

Parameters:
  • window_data (ndarray) –

    A 1D array representing the speed profile within the window.

Returns:
Source code in pyeyesweb/low_level/smoothness.py
def compute(self, window_data: np.ndarray) -> SmoothnessResult:
    """Executes smoothness calculation on the speed profile.

    Parameters
    ----------
    window_data : numpy.ndarray
        A 1D array representing the speed profile within the window.

    Returns
    -------
    SmoothnessResult
        The computed smoothness metrics.
    """
    if window_data.size != window_data.shape[0]:
        raise ValueError("Smoothness expects a 1D speed profile.")

    speed_profile = window_data.ravel()

    # Minimum sample threshold for FFT
    if len(speed_profile) < 10:
        return SmoothnessResult(is_valid=False)

    # 1. Preprocessing (Filtering)
    filtered_speed = self._filter_signal(speed_profile)

    # 2. Metrics calculation
    sparc_val = None
    jerk_val = None

    if "sparc" in self.metrics:
        # Pass custom parameters to the function in math_utils
        sparc_val = float(compute_sparc(
            filtered_speed, 
            rate_hz=self.rate_hz,
            amplitude_threshold=self.sparc_threshold,
            min_fc=self.sparc_min_fc,
            max_fc=self.sparc_max_fc
        ))

    if "jerk_rms" in self.metrics:
        # Standard Jerk RMS calculation from velocity
        jerk_val = float(compute_jerk_rms(filtered_speed, self.rate_hz, signal_type='velocity'))

    return SmoothnessResult(sparc=sparc_val, jerk_rms=jerk_val)

SmoothnessResult dataclass

Bases: FeatureResult

Output contract for Smoothness metrics.

Attributes:
  • sparc ((float, optional)) –

    The computed SPARC metric representing spectral arc length.

  • jerk_rms ((float, optional)) –

    The compute root mean square of jerk.

Source code in pyeyesweb/low_level/smoothness.py
@dataclass(slots=True)
class SmoothnessResult(FeatureResult):
    """Output contract for Smoothness metrics.

    Attributes
    ----------
    sparc : float, optional
        The computed SPARC metric representing spectral arc length.
    jerk_rms : float, optional
        The compute root mean square of jerk.
    """
    sparc: Optional[float] = None
    jerk_rms: Optional[float] = None