Note
Go to the end to download the full example code.
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#
Define a new feature class inheriting from
Transform.Implement the
_applymethod.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 Square–myoverse.transforms.RMSMean Absolute Value–myoverse.transforms.MAVVariance–myoverse.transforms.VARWaveform Length–myoverse.transforms.WaveformLengthZero Crossings–myoverse.transforms.ZeroCrossingsSlope Sign Change–myoverse.transforms.SlopeSignChanges
Temporal (preserve time axis):
Identity–myoverse.transforms.Identity(passes raw signal through unchanged)RMS Small Window– custom sliding-window RMS defined inuser_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}")