Skip to content

Filters

Output-side smoothing filters for prediction-output control vectors. Custom filters implement the VectorFilter protocol below. See Post-process predictions for tuning guidance.

The protocol

VectorFilter

Bases: Protocol

Stateful per-vector filter. Call once per output tick.

Built-in filters

OneEuroFilter

OneEuroFilter(freq: float = 50.0, min_cutoff: float = 1.0, beta: float = 0.02, d_cutoff: float = 1.0)

1€ Filter — adaptive low-pass for noisy interactive signals.

Trades latency for smoothness based on instantaneous velocity: fast motion → high cutoff (responsive), slow motion → low cutoff (smooth). Standard for hand tracking, controllers, gesture cursors.

Reference: https://gery.casiez.net/1euro/

Parameters:

Name Type Description Default
freq float

Expected sample rate (Hz). Used as a fallback dt when no timestamp is passed to __call__. Filter accuracy depends on this matching the actual call rate; pass t from your predict loop if the rate is jittery.

50.0
min_cutoff float

Cutoff (Hz) at zero velocity — controls baseline smoothing.

1.0
beta float

Velocity-to-cutoff gain. Larger → more responsive on fast moves.

0.02
d_cutoff float

Cutoff (Hz) for the velocity smoother.

1.0
Source code in myogestic/filters.py
def __init__(
    self,
    freq: float = 50.0,
    min_cutoff: float = 1.0,
    beta: float = 0.02,
    d_cutoff: float = 1.0,
):
    if freq <= 0:
        raise ValueError(f"freq must be > 0 (got {freq})")
    if min_cutoff <= 0:
        raise ValueError(f"min_cutoff must be > 0 (got {min_cutoff})")
    if d_cutoff <= 0:
        raise ValueError(f"d_cutoff must be > 0 (got {d_cutoff})")
    self.freq = freq
    self.min_cutoff = min_cutoff
    self.beta = beta
    self.d_cutoff = d_cutoff
    self._x_prev: np.ndarray | None = None
    self._dx_prev: np.ndarray | None = None
    self._t_prev: float | None = None

GaussianFilter

GaussianFilter(window: int = 5, sigma: float = 1.0)

Rolling temporal smoothing for 1-D vectors.

Keeps the last window vectors and returns their Gaussian-weighted mean (weights peak at the most recent sample). During warmup (buffer not yet full), weights are renormalized over the available history — no zero-padding bias.

Inputs must be 1-D arrays of consistent length (raises on first dimension mismatch).

Source code in myogestic/filters.py
def __init__(self, window: int = 5, sigma: float = 1.0):
    if window < 1:
        raise ValueError(f"window must be >= 1 (got {window})")
    if sigma <= 0:
        raise ValueError(f"sigma must be > 0 (got {sigma})")
    self.window = window
    self.sigma = sigma
    # Gaussian kernel centered on the most recent sample (last position).
    idx = np.arange(window, dtype=np.float64)
    weights = np.exp(-((idx - (window - 1)) ** 2) / (2.0 * sigma * sigma))
    self._weights = weights / weights.sum()
    self._buf: list[np.ndarray] = []

IdentityFilter

Passthrough — useful as a baseline or "off" toggle.

Factory

make_filter

make_filter(name: str, hz: float = 50.0, **kwargs: Any) -> VectorFilter

Construct a filter by name. Swap filters in an experiment by changing one string; pass extra kwargs to tune without instantiating the class directly.

Parameters:

Name Type Description Default
name str

"identity" | "gaussian" | "one_euro".

required
hz float

Expected sample rate. Forwarded as freq to one_euro; ignored by the others.

50.0
**kwargs Any

Forwarded to the filter constructor — e.g. make_filter("gaussian", window=10, sigma=2.0), make_filter("one_euro", hz=32, beta=0.05).

{}

Raises:

Type Description
ValueError

if the name isn't recognized.

TypeError

if a kwarg is unknown for the chosen filter.

Source code in myogestic/filters.py
def make_filter(name: str, hz: float = 50.0, **kwargs: Any) -> VectorFilter:
    """Construct a filter by name. Swap filters in an experiment by
    changing one string; pass extra kwargs to tune without instantiating
    the class directly.

    Args:
        name: ``"identity"`` | ``"gaussian"`` | ``"one_euro"``.
        hz: Expected sample rate. Forwarded as ``freq`` to ``one_euro``;
            ignored by the others.
        **kwargs: Forwarded to the filter constructor — e.g.
            ``make_filter("gaussian", window=10, sigma=2.0)``,
            ``make_filter("one_euro", hz=32, beta=0.05)``.

    Raises:
        ValueError: if the name isn't recognized.
        TypeError: if a kwarg is unknown for the chosen filter.
    """
    n = name.lower()
    if n == "identity":
        if kwargs:
            raise TypeError(f"identity takes no kwargs (got {list(kwargs)})")
        return IdentityFilter()
    if n == "gaussian":
        return GaussianFilter(**{"window": 5, "sigma": 1.0, **kwargs})
    if n == "one_euro":
        return OneEuroFilter(
            **{
                "freq": hz,
                "min_cutoff": 1.0,
                "beta": 0.02,
                "d_cutoff": 1.0,
                **kwargs,
            }
        )
    raise ValueError(
        f"Unknown filter {name!r}. Choose: 'identity', 'gaussian', 'one_euro'."
    )