Skip to content

biquad_filter

Biquad Filter Layer API

This module provides classes to build biquad filter layers.

Functions:

Classes:

Classes

CascadedBiquadFilter

CascadedBiquadFilter(lowcut: float | None = None, highcut: float | None = None, sample_rate: float = 1000, order: int = 3, forward_backward: bool = False, **kwargs)

Implements a 2nd order cascaded biquad filter using direct form 1 structure.

See here for more information on the direct form 1 structure.

Parameters:

  • lowcut (float | None, default: None ) –

    Lower cutoff in Hz. Defaults to None.

  • highcut (float | None, default: None ) –

    Upper cutoff in Hz. Defaults to None.

  • sample_rate (float, default: 1000 ) –

    Sampling rate in Hz. Defaults to 1000 Hz.

  • order (int, default: 3 ) –

    Filter order. Defaults to 3.

  • forward_backward (bool, default: False ) –

    Apply filter forward and backward.

Example:

# Create sine wave at 10 Hz with 1000 Hz sampling rate
t = np.linspace(0, 1, 1000, endpoint=False)
x = np.sin(2 * np.pi * 10 * t)
# Add noise at 100 Hz and 2 Hz
x_noise = x + 0.5 * np.sin(2 * np.pi * 100 * t) + 0.5 * np.sin(2 * np.pi * 2 * t)
x_noise = x_noise.reshape(-1, 1).astype(np.float32)
x_noise = keras.ops.convert_to_tensor(x_noise)
# Create bandpass filter
lyr = nse.layers.preprocessing.CascadedBiquadFilter(lowcut=5, highcut=15, sample_rate=1000, forward_backward=True)
y = lyr(x_noise).numpy().squeeze()
x_noise = x_noise.numpy().squeeze()
plt.plot(x, label="Original")
plt.plot(x_noise, label="Noisy")
plt.plot(y, label="Filtered")
plt.legend()
plt.show()
Source code in neuralspot_edge/layers/preprocessing/biquad_filter.py
def __init__(
    self,
    lowcut: float | None = None,
    highcut: float | None = None,
    sample_rate: float = 1000,
    order: int = 3,
    forward_backward: bool = False,
    **kwargs,
):
    """Implements a 2nd order cascaded biquad filter using direct form 1 structure.

    See [here](https://en.wikipedia.org/wiki/Digital_biquad_filter) for more information
    on the direct form 1 structure.

    Args:
        lowcut (float|None): Lower cutoff in Hz. Defaults to None.
        highcut (float|None): Upper cutoff in Hz. Defaults to None.
        sample_rate (float): Sampling rate in Hz. Defaults to 1000 Hz.
        order (int, optional): Filter order. Defaults to 3.
        forward_backward (bool): Apply filter forward and backward.

    Example:

    ```python
    # Create sine wave at 10 Hz with 1000 Hz sampling rate
    t = np.linspace(0, 1, 1000, endpoint=False)
    x = np.sin(2 * np.pi * 10 * t)
    # Add noise at 100 Hz and 2 Hz
    x_noise = x + 0.5 * np.sin(2 * np.pi * 100 * t) + 0.5 * np.sin(2 * np.pi * 2 * t)
    x_noise = x_noise.reshape(-1, 1).astype(np.float32)
    x_noise = keras.ops.convert_to_tensor(x_noise)
    # Create bandpass filter
    lyr = nse.layers.preprocessing.CascadedBiquadFilter(lowcut=5, highcut=15, sample_rate=1000, forward_backward=True)
    y = lyr(x_noise).numpy().squeeze()
    x_noise = x_noise.numpy().squeeze()
    plt.plot(x, label="Original")
    plt.plot(x_noise, label="Noisy")
    plt.plot(y, label="Filtered")
    plt.legend()
    plt.show()
    ```

    """

    super().__init__(**kwargs)

    sos = get_butter_sos(lowcut, highcut, sample_rate, order)

    # These are the second order coefficients arranged as 2D tensor (n_sections x 6)
    # We remap each section from [b0, b1, b2, a0, a1, a2] to [b0, b1, b2, -a2, -a1, a0]
    sos = sos[:, [0, 1, 2, 5, 4, 3]] * [1, 1, 1, -1, -1, 1]
    self.sos = self.add_weight(
        name="sos",
        shape=sos.shape,
        trainable=False,
    )
    self.sos.assign(sos)
    self.num_stages = keras.ops.shape(self.sos)[0]
    self.forward_backward = forward_backward

Functions

augment_sample
augment_sample(inputs) -> keras.KerasTensor

Applies the cascaded biquad filter to the input samples.

Source code in neuralspot_edge/layers/preprocessing/biquad_filter.py
def augment_sample(self, inputs) -> keras.KerasTensor:
    """Applies the cascaded biquad filter to the input samples."""
    samples = inputs[self.SAMPLES]
    # inputs have shape (time, channels)

    # Force to be channels_last
    if self.data_format == "channels_first":
        samples = keras.ops.transpose(samples, perm=[1, 0])

    # Iterate across second order sections
    samples = keras.ops.fori_loop(lower=0, upper=self.num_stages, body_fun=self._apply_sos, init_val=samples)

    if self.forward_backward:
        samples = keras.ops.flip(samples, axis=self.data_axis)
        samples = keras.ops.fori_loop(lower=0, upper=self.num_stages, body_fun=self._apply_sos, init_val=samples)
        samples = keras.ops.flip(samples, axis=self.data_axis)
    # END IF

    # Undo the transpose if needed
    if self.data_format == "channels_first":
        samples = keras.ops.transpose(samples, axes=[1, 0])

    return samples

Functions

get_butter_sos

get_butter_sos(lowcut: float | None = None, highcut: float | None = None, sample_rate: float = 1000, order: int = 3) -> npt.NDArray

Compute biquad filter coefficients as SOS. This function caches. For lowpass, lowcut is required and highcut is ignored. For highpass, highcut is required and lowcut is ignored. For bandpass, both lowcut and highcut are required.

Parameters:

  • lowcut (float | None, default: None ) –

    Lower cutoff in Hz. Defaults to None.

  • highcut (float | None, default: None ) –

    Upper cutoff in Hz. Defaults to None.

  • sample_rate (float, default: 1000 ) –

    Sampling rate in Hz. Defaults to 1000 Hz.

  • order (int, default: 3 ) –

    Filter order. Defaults to 3.

Returns:

Source code in neuralspot_edge/layers/preprocessing/biquad_filter.py
def get_butter_sos(
    lowcut: float | None = None,
    highcut: float | None = None,
    sample_rate: float = 1000,
    order: int = 3,
) -> npt.NDArray:
    """Compute biquad filter coefficients as SOS. This function caches.
    For lowpass, lowcut is required and highcut is ignored.
    For highpass, highcut is required and lowcut is ignored.
    For bandpass, both lowcut and highcut are required.

    Args:
        lowcut (float|None): Lower cutoff in Hz. Defaults to None.
        highcut (float|None): Upper cutoff in Hz. Defaults to None.
        sample_rate (float): Sampling rate in Hz. Defaults to 1000 Hz.
        order (int, optional): Filter order. Defaults to 3.

    Returns:
        npt.NDArray: SOS
    """
    nyq = sample_rate / 2
    if lowcut is not None and highcut is not None:
        freqs = [lowcut / nyq, highcut / nyq]
        btype = "bandpass"
    elif lowcut is not None:
        freqs = lowcut / nyq
        btype = "highpass"
    elif highcut is not None:
        freqs = highcut / nyq
        btype = "lowpass"
    else:
        raise ValueError("At least one of lowcut or highcut must be specified")
    sos = scipy.signal.butter(order, freqs, btype=btype, output="sos")
    return sos