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)