ezmsg.sigproc.filter#

Core IIR/FIR filtering infrastructure with BA and SOS coefficient support.

Functions

filtergen(axis, coefs, coef_type)[source]#

Filter data using the provided coefficients.

Returns:

FilterTransformer.

Parameters:
Return type:

FilterTransformer

Classes

class BaseFilterByDesignTransformerUnit(*args, settings=None, **kwargs)[source]#

Bases: BaseTransformerUnit[SettingsType, AxisArray, AxisArray, FilterByDesignTransformer], Generic[SettingsType, TransformerType]

Parameters:

settings (Settings | None)

class Filter(*args, settings=None, **kwargs)[source]#

Bases: BaseTransformerUnit[FilterSettings, AxisArray, AxisArray, FilterTransformer]

Parameters:

settings (Settings | None)

SETTINGS#

alias of FilterSettings

class FilterBaseSettings(axis: str | None = None, coef_type: str = 'ba', use_fast_sosfilt: bool = True, use_mlx_metal: bool = True, mlx_metal_chunk_sizes: tuple[int, ...] = (512,), thread_min_bytes: int = 1048576, fir_fft_min_taps: int = 64)[source]#

Bases: Settings

Parameters:
  • axis (str | None)

  • coef_type (str)

  • use_fast_sosfilt (bool)

  • use_mlx_metal (bool)

  • mlx_metal_chunk_sizes (tuple[int, ...])

  • thread_min_bytes (int)

  • fir_fft_min_taps (int)

axis: str | None = None#

The name of the axis to operate on.

coef_type: str = 'ba'#

The type of filter coefficients. One of “ba” or “sos”.

use_fast_sosfilt: bool = True#

If True (default), numpy SOS filtering calls scipy’s Cython kernel directly instead of going through scipy.signal.sosfilt.

Output is bit-identical – same kernel, same dtype promotion, same order of operations – so this is purely a latency optimization. It removes scipy’s fixed ~12-18 us of per-call validation and bookkeeping, which is invisible on offline-sized chunks but dominant online: measured 59.9% of the call at 16 channels x 30 samples, 51.0% at 32x30, 16.6% at 256x30, 4.1% at 256x128.

The fast path depends on a private scipy entry point. It is verified against the public function once per process, and falls back automatically if the private kernel is missing or disagrees. Set False to force the public path.

use_mlx_metal: bool = True#

If True (default), SOS filtering on MLX inputs runs on the GPU via the bundled Metal kernel (sosfilt_mlx_metal) instead of round-tripping through scipy. Set to False to fall back to the scipy path (bit-exact with numpy at the cost of CPU round-trips and ~5-8x slowdown).

mlx_metal_chunk_sizes: tuple[int, ...] = (512,)#

Allowable compile-time chunk sizes for SOS Metal kernels. The smallest size that fits the remaining samples is selected on each launch; otherwise the largest size is repeated. Specializations compile lazily on first use. Values must be in [1, 512].

thread_min_bytes: int = 1048576#

Chunk size (bytes) at or above which scipy IIR filtering is split across threads on a non-sample axis. Channels are independent recurrences, so the result is bit-identical to the single-threaded call.

Only large chunks benefit: scipy’s filters are single-threaded C loops, and below ~1 MB the dispatch cost makes threading a net loss (measured 0.23x at 30 samples x 256 channels, 4.5x at 8192). The default keeps online-sized chunks single-threaded while letting offline blocks use the cores. Set to 0 to disable threading entirely.

fir_fft_min_taps: int = 64#

Tap count at or above which a numpy FIR filter is applied by FFT convolution instead of scipy.ndimage’s C-level correlation.

Neither wins everywhere. Measured on 256 channels (best FFT vs ndimage), the FFT is 1.3-6.8x faster at 129-513 taps while ndimage is 1.6-3.0x faster at 17-33 taps, with the crossover near 64 taps.

The two differ in more than speed, and the difference matters if you rely on an offline run reproducing an online one exactly:

  • The time-domain path is chunk-size invariant – filtering in chunks of 250, chunks of 333, or one shot gives bit-identical output.

  • The FFT path is not. The transform length depends on the chunk length, so a different chunking gives a different rounding: measured 2.6e-07 relative in float32 (~7e-16 in float64).

Both match scipy’s lfilter to roundoff, so this is about reproducibility across chunkings rather than accuracy. Set this very high to force the time-domain path everywhere and keep FIR output chunk-invariant.

__init__(axis=None, coef_type='ba', use_fast_sosfilt=True, use_mlx_metal=True, mlx_metal_chunk_sizes=(512,), thread_min_bytes=1048576, fir_fft_min_taps=64)#
Parameters:
  • axis (str | None)

  • coef_type (str)

  • use_fast_sosfilt (bool)

  • use_mlx_metal (bool)

  • mlx_metal_chunk_sizes (tuple[int, ...])

  • thread_min_bytes (int)

  • fir_fft_min_taps (int)

Return type:

None

class FilterByDesignState[source]#

Bases: object

filter: FilterTransformer | None = None#
needs_redesign: bool = False#
class FilterByDesignTransformer(*args, **kwargs)[source]#

Bases: BaseStatefulTransformer[SettingsType, AxisArray, AxisArray, FilterByDesignState], ABC, Generic[SettingsType, FilterCoefsType]

Abstract base class for filter design transformers.

classmethod get_message_type(dir)[source]#
Parameters:

dir (str)

Return type:

type[AxisArray]

abstractmethod get_design_function()[source]#

Return a function that takes sampling frequency and returns filter coefficients.

Return type:

Callable[[float], FilterCoefsType | None]

update_settings(new_settings=None, **kwargs)[source]#

Update settings and mark that filter coefficients need to be recalculated.

Parameters:
  • new_settings (SettingsType | None) – Complete new settings object to replace current settings

  • **kwargs – Individual settings to update

Return type:

None

class FilterCoefficients(b: ndarray = <factory>, a: ndarray = <factory>)[source]#

Bases: object

Parameters:
b: ndarray#
a: ndarray#
__init__(b=<factory>, a=<factory>)#
Parameters:
Return type:

None

class FilterSettings(axis: str | None = None, coef_type: str = 'ba', use_fast_sosfilt: bool = True, use_mlx_metal: bool = True, mlx_metal_chunk_sizes: tuple[int, ...] = (512,), thread_min_bytes: int = 1048576, fir_fft_min_taps: int = 64, coefs: FilterCoefficients | None = None)[source]#

Bases: FilterBaseSettings

Parameters:
coefs: FilterCoefficients | None = None#

The pre-calculated filter coefficients.

__init__(axis=None, coef_type='ba', use_fast_sosfilt=True, use_mlx_metal=True, mlx_metal_chunk_sizes=(512,), thread_min_bytes=1048576, fir_fft_min_taps=64, coefs=None)#
Parameters:
Return type:

None

class FilterState[source]#

Bases: object

zi: ndarray[tuple[Any, ...], dtype[_ScalarT]] | None = None#
fir_b: Any | None = None#
fir_b_1d: Any | None = None#
fir_method: str | None = None#
sos_method: str | None = None#
sos_mx: Any | None = None#
sos_direct: Any | None = None#
sos_direct_dtype: Any | None = None#
class FilterTransformer(*args, **kwargs)[source]#

Bases: BaseStatefulTransformer[FilterSettings, AxisArray, AxisArray, FilterState]

Filter data using the provided coefficients.

NONRESET_SETTINGS_FIELDS: ClassVar[frozenset[str]] = frozenset({'mlx_metal_chunk_sizes'})#
update_coefficients(coefs, coef_type=None)[source]#

Update filter coefficients.

If the new coefficients have the same length as the current ones, only the coefficients are updated. If the lengths differ, the filter state is also reset to handle the new filter order.

Parameters:
Return type:

None