Add a Biosignal Feature#

This example shows how to create and register a new feature-extraction transform in MyoGestic. Features are applied to raw EMG data during dataset creation and real-time prediction to produce the input that models consume.

All features must inherit from the Transform base class in MyoVerse. The core logic goes in the _apply method, which operates on PyTorch tensors with named dimensions (e.g., "channels", "time").

Temporal vs Non-Temporal Features#

MyoGestic distinguishes two categories of features:

  • Non-temporal (default, requires_temporal_preservation=False): Collapses the time axis into a single value per channel. Examples: RMS, MAV, Variance. Compatible with sklearn/CatBoost models.

  • Temporal (requires_temporal_preservation=True): Preserves the time dimension by computing a value for each time step (or sliding window). Examples: Identity (raw signal), RMS Small Window. Required by CNN-based models like RaulNet.

When you register a feature, set requires_temporal_preservation=True if it preserves the time axis. The Training UI uses this flag to show only compatible features for the selected model.

Add your feature in user_config

Keep custom feature registrations in user_config to stay separate from core MyoGestic code.

Example Overview#

  1. Define a new feature class inheriting from Transform.

  2. Implement the _apply method.

  3. Register the feature in CONFIG_REGISTRY.

import torch
from myoverse.transforms import Transform
from myoverse.transforms.base import get_dim_index

from myogestic.utils.config import CONFIG_REGISTRY

Step 1: Define a Non-Temporal Feature (Variance)#

This feature computes the variance along the time axis, collapsing it to a single value per channel. It is compatible with standard ML models (sklearn, CatBoost).

class MyVarianceFeature(Transform):
    """Compute the variance of the input signal along a given dimension.

    Parameters
    ----------
    dim : str, optional
        The dimension along which to compute variance. Default is ``"time"``.
    keepdim : bool, optional
        Whether to keep the reduced dimension. Default is ``False``.
    """

    def __init__(self, dim: str = "time", keepdim: bool = False, **kwargs):
        super().__init__(dim=dim, **kwargs)
        self.keepdim = keepdim

    def _apply(self, x: torch.Tensor) -> torch.Tensor:
        """Compute variance along the specified dimension.

        Parameters
        ----------
        x : torch.Tensor
            Input tensor with named dimensions (e.g., ``("channels", "time")``).

        Returns
        -------
        torch.Tensor
            Variance computed along the specified dimension.
        """
        dim_idx = get_dim_index(x, self.dim)
        names = x.names

        result = torch.var(x.rename(None), dim=dim_idx, keepdim=self.keepdim)

        if names[0] is None:
            return result

        if self.keepdim:
            return result.rename(*names)

        new_names = [n for i, n in enumerate(names) if i != dim_idx]
        if new_names:
            return result.rename(*new_names)

        return result


# Register as a non-temporal feature (default)
CONFIG_REGISTRY.register_feature("My Variance Feature", MyVarianceFeature)

Step 2: Define a Temporal Feature (Sliding Window RMS)#

This feature computes RMS over a sliding window, preserving the time dimension. It is compatible with CNN-based models like RaulNet.

For a buffer of 360 samples with window_size=120 and stride=1, this produces 241 time steps.

class MySlidingRMS(Transform):
    """RMS computed over a sliding window, preserving temporal resolution.

    Parameters
    ----------
    dim : str, optional
        The dimension to slide over. Default is ``"time"``.
    window_size : int, optional
        Size of the sliding window. Default is ``120``.
    stride : int, optional
        Stride of the sliding window. Default is ``1``.
    """

    def __init__(self, dim: str = "time", window_size: int = 120, stride: int = 1, **kwargs):
        super().__init__(dim=dim, **kwargs)
        self.window_size = window_size
        self.stride = stride

    def _apply(self, x: torch.Tensor) -> torch.Tensor:
        dim_idx = x.names.index(self.dim) if x.names[0] is not None else -1
        x_unnamed = x.rename(None)

        # unfold creates sliding windows: (channels, n_windows, window_size)
        windows = x_unnamed.unfold(dim_idx, self.window_size, self.stride)
        rms = torch.sqrt(torch.mean(windows ** 2, dim=-1))

        return rms.rename(*x.names)


# Register as a temporal feature (preserves time dimension)
CONFIG_REGISTRY.register_feature(
    "My Sliding RMS", MySlidingRMS, requires_temporal_preservation=True
)

Reference: Built-in Features#

The following features are registered in default_config:

Non-temporal (collapse time axis):

  • Root Mean Squaremyoverse.transforms.RMS

  • Mean Absolute Valuemyoverse.transforms.MAV

  • Variancemyoverse.transforms.VAR

  • Waveform Lengthmyoverse.transforms.WaveformLength

  • Zero Crossingsmyoverse.transforms.ZeroCrossings

  • Slope Sign Changemyoverse.transforms.SlopeSignChanges

Temporal (preserve time axis):

  • Identitymyoverse.transforms.Identity (passes raw signal through unchanged)

  • RMS Small Window – custom sliding-window RMS defined in user_config (window_size=120, stride=1)

Example Usage (standalone test)#

if __name__ == "__main__":
    sample_data = torch.tensor([1.2, 2.5, 2.7, 2.8, 3.1])
    sample_data = sample_data.rename("time")

    feature_instance = MyVarianceFeature(dim="time")
    variance_value = feature_instance(sample_data)

    print(f"Variance of {sample_data.rename(None).numpy()} = {variance_value.item():.4f}")

Gallery generated by Sphinx-Gallery