Skip to content

filter

Filter design and application.

Functions:

Name Description
fir_least_squares

Apply a least-squares optimised FIR filter to some data.

matched_filter

Apply a matched filter to some data.

fir_least_squares

fir_least_squares(data, low_edge, high_edge, transition_width, taps=151, dim='time')

Apply a least-squares optimised FIR filter to some data.

This uses scipy.signal.firls to calculate the coefficients for each tap. The filter is applied using scipy.signal.filtfilt which applies the filter both forward and backwards to avoid phase shifts from the filtering. Note that this results in a filter order of twice the original design.

Parameters:

Name Type Description Default
data DataArray

The data to apply the filter to.

required
low_edge float | None

The low edge of the passband in Hertz. If None, a low-pass filter is generated.

required
high_edge float | None

The high edge of the passband in Hertz. If None, a high-pass filter is generated. Cannot be None if low_edge is None.

required
transition_width float

The desired transition width at each edge in Hertz. The ideal filter would be zero below (low_edge - transition_width) and above (high_edge + transition_width).

required
taps int

The number of taps in the filter. This must be positive and odd.

151
dim str

The dimension to filter along. The coordinates are assumed to be equally spaced along this dimension.

'time'

Returns:

Name Type Description
filtered DataArray

The filtered data. This will have the same coordinates as the input data.

Source code in src/airsonar/processing/filter.py
 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
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
def fir_least_squares(
    data: xr.DataArray,
    low_edge: float | None,
    high_edge: float | None,
    transition_width: float,
    taps: int = 151,
    dim: str = "time",
) -> xr.DataArray:
    """Apply a least-squares optimised FIR filter to some data.

    This uses [scipy.signal.firls][] to calculate the coefficients for each tap. The
    filter is applied using [scipy.signal.filtfilt][] which applies the filter both
    forward and backwards to avoid phase shifts from the filtering. Note that this
    results in a filter order of twice the original design.

    Parameters
    ----------
    data
        The data to apply the filter to.
    low_edge
        The low edge of the passband in Hertz. If None, a low-pass filter is generated.
    high_edge
        The high edge of the passband in Hertz. If None, a high-pass filter is
        generated. Cannot be None if `low_edge` is None.
    transition_width
        The desired transition width at each edge in Hertz. The ideal filter would be
        zero below (low_edge - transition_width) and above (high_edge +
        transition_width).
    taps
        The number of taps in the filter. This must be positive and odd.
    dim
        The dimension to filter along. The coordinates are assumed to be equally spaced
        along this dimension.

    Returns
    -------
    filtered : xr.DataArray
        The filtered data. This will have the same coordinates as the input data.

    """
    if transition_width <= 0:
        raise ValueError("transition_width must be greater than zero")

    # Compute the sampling rate, assuming the coordinates are constantly spaced.
    fs = 1 / float(data[dim][1] - data[dim][0])
    nyquist = fs / 2

    # Low-pass filter.
    if low_edge is None:
        if high_edge is None:
            raise ValueError("only one of low_edge and high_edge can be None")
        if (high_edge + transition_width) > nyquist:
            raise ValueError(
                "high_edge + transition_width is higher than the Nyquist frequency"
            )

        bands = [0, high_edge, high_edge + transition_width, nyquist]
        desired = [1, 1, 0, 0]

    # High-pass filter.
    elif high_edge is None:
        if low_edge < transition_width:
            raise ValueError("low_edge cannot be smaller than transition_width")

        bands = [0, low_edge - transition_width, low_edge, nyquist]
        desired = [0, 0, 1, 1]

    # Bandpass filter.
    else:
        if low_edge < transition_width:
            raise ValueError("low_edge cannot be smaller than transition_width")
        if (high_edge + transition_width) > nyquist:
            raise ValueError(
                "high_edge + transition_width is higher than the Nyquist frequency"
            )

        bands = [
            0,
            low_edge - transition_width,
            low_edge,
            high_edge,
            high_edge + transition_width,
            nyquist,
        ]
        desired = [0, 0, 1, 1, 0, 0]

    # And generate.
    coeffs = signal.firls(taps, bands=bands, desired=desired, fs=fs)

    # Create a ufunc to apply the filter.
    def apply_fir_least_squares(values: np.ndarray) -> np.ndarray:
        return signal.filtfilt(coeffs, [1], values, axis=-1)

    # And apply that ufunc.
    filtered = xr.apply_ufunc(
        apply_fir_least_squares,
        data,
        input_core_dims=[[dim]],
        output_core_dims=[[dim]],
        keep_attrs=True,
        dask="parallelized",
        dask_gufunc_kwargs={"meta": np.empty(0, dtype=float)},
    )
    return filtered

matched_filter

matched_filter(data, signal, method='fft', data_dim='time', signal_dim='time')

Apply a matched filter to some data.

Parameters:

Name Type Description Default
data DataArray

The data to apply the matched filter to.

required
signal DataArray

The signal to match.

required
method Literal['fft']

The implementation method. Currently only fft is supported, which performs the filtering in the Fourier domain.

'fft'
data_dim str

The dimension of data to apply the filter along.

'time'
signal_dim str

The dimension of signal that the signal is defined along.

'time'

Returns:

Name Type Description
matched DataArray

An array with the broadcast dimensions of data and signal containing the matched-filtered data.

Source code in src/airsonar/processing/filter.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
def matched_filter(
    data: xr.DataArray,
    signal: xr.DataArray,
    method: Literal["fft"] = "fft",
    data_dim: str = "time",
    signal_dim: str = "time",
) -> xr.DataArray:
    """Apply a matched filter to some data.

    Parameters
    ----------
    data
        The data to apply the matched filter to.
    signal
        The signal to match.
    method
        The implementation method. Currently only `fft` is supported, which performs the
        filtering in the Fourier domain.
    data_dim
        The dimension of `data` to apply the filter along.
    signal_dim
        The dimension of `signal` that the signal is defined along.

    Returns
    -------
    matched : xr.DataArray
        An array with the broadcast dimensions of `data` and `signal` containing the
        matched-filtered data.

    """
    is_complex = data.dtype.kind == "c" or signal.dtype.kind == "c"

    # Assume that the signal and data will have different lengths along the dimension,
    # and rename the signal dimension to avoid coordinate mismatches.
    signal = signal.rename({signal_dim: "__signal_time__"})

    if method == "fft":
        return xr.apply_ufunc(
            _matched_filter_fft,
            data,
            signal,
            input_core_dims=[[data_dim], ["__signal_time__"]],
            output_core_dims=[[data_dim]],
            dask="parallelized",
            dask_gufunc_kwargs={
                "meta": np.empty(0, dtype=np.complex128 if is_complex else np.float64)
            },
            keep_attrs=True,
        )

    raise ValueError(f"unknown method '{method}'")