Skip to content

nan_filtfilt

biosigpy.tools.nan_filtfilt.nan_filtfilt

nan_filtfilt(b: ArrayLike, a: ArrayLike, x: ArrayLike, max_gap: int = 0) -> np.ndarray

Zero-phase filter a signal with NaN-aware gap handling.

Parameters:

Name Type Description Default
b array_like

Numerator filter coefficients. Values must be finite.

required
a array_like

Denominator filter coefficients. Values must be finite.

required
x array_like

One-dimensional real numeric signal. NaN values are allowed. Infinite values are invalid.

required
max_gap int

Maximum internal NaN gap length to interpolate before filtering.

0

Returns:

Type Description
ndarray

One-dimensional zero-phase filtered signal with the same length as x.

Raises:

Type Description
TypeError

If b, a, or x is not real numeric data.

ValueError

If filter coefficients are empty or non-finite, if x contains infinite values, or if max_gap is negative.

Notes

This function implements the Biosiglib tools.nan_filtfilt specification. Filtering is zero-phase and uses :func:scipy.signal.filtfilt on each valid finite segment.

Boundary NaN gaps are preserved. Internal short gaps with length less than or equal to max_gap are interpolated before filtering. Internal long gaps with length greater than max_gap are preserved and split the signal into separate segments. Segments that are too short for zero-phase filtering return NaN.

Examples:

>>> import numpy as np
>>> from biosigpy.tools import nan_filtfilt
>>> b = np.array([0.25, 0.5, 0.25])
>>> a = np.array([1.0])
>>> x = np.linspace(0.0, 1.0, 20)
>>> x[8] = np.nan
>>> nan_filtfilt(b, a, x, max_gap=1).shape
(20,)
Source code in src/biosigpy/tools/nan_filtfilt.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def nan_filtfilt(
    b: ArrayLike, a: ArrayLike, x: ArrayLike, max_gap: int = 0
) -> np.ndarray:
    """Zero-phase filter a signal with NaN-aware gap handling.

    Parameters
    ----------
    b : array_like
        Numerator filter coefficients. Values must be finite.
    a : array_like
        Denominator filter coefficients. Values must be finite.
    x : array_like
        One-dimensional real numeric signal. ``NaN`` values are allowed.
        Infinite values are invalid.
    max_gap : int, optional
        Maximum internal ``NaN`` gap length to interpolate before filtering.

    Returns
    -------
    numpy.ndarray
        One-dimensional zero-phase filtered signal with the same length as
        ``x``.

    Raises
    ------
    TypeError
        If ``b``, ``a``, or ``x`` is not real numeric data.
    ValueError
        If filter coefficients are empty or non-finite, if ``x`` contains
        infinite values, or if ``max_gap`` is negative.

    Notes
    -----
    This function implements the Biosiglib ``tools.nan_filtfilt``
    specification. Filtering is zero-phase and uses
    :func:`scipy.signal.filtfilt` on each valid finite segment.

    Boundary ``NaN`` gaps are preserved. Internal short gaps with length less
    than or equal to ``max_gap`` are interpolated before filtering. Internal
    long gaps with length greater than ``max_gap`` are preserved and split the
    signal into separate segments. Segments that are too short for zero-phase
    filtering return ``NaN``.

    Examples
    --------
    >>> import numpy as np
    >>> from biosigpy.tools import nan_filtfilt
    >>> b = np.array([0.25, 0.5, 0.25])
    >>> a = np.array([1.0])
    >>> x = np.linspace(0.0, 1.0, 20)
    >>> x[8] = np.nan
    >>> nan_filtfilt(b, a, x, max_gap=1).shape
    (20,)
    """

    b, a, x, max_gap = parse_nan_filtering(b, a, x, max_gap)
    filter_order = max(b.size - 1, a.size - 1)
    minimum_segment_length = 3 * filter_order + 1
    padlen = 3 * filter_order

    if filter_order == 0:
        if a[0] == 0:
            raise ValueError("a[0] must be nonzero")
        zero_phase_gain = (b[0] / a[0]) ** 2

        def filter_func(
            b: np.ndarray, a: np.ndarray, segment_filled: np.ndarray
        ) -> np.ndarray:
            return segment_filled * zero_phase_gain

    else:

        def filter_func(
            b: np.ndarray, a: np.ndarray, segment_filled: np.ndarray
        ) -> np.ndarray:
            return signal.filtfilt(b, a, segment_filled, padlen=padlen)

    return process_nan_signal(
        b, a, x, max_gap, filter_func, minimum_segment_length
    )