DSP

implements necessary DSP functionality using torch

IIR filters

Utility Functions


source

pole_or_zero_to_iir_coeff

 pole_or_zero_to_iir_coeff (pole_or_zero:torch.Tensor)

Converts a complex pole or zero to IIR coefficients


source

constrain_complex_pole_or_zero

 constrain_complex_pole_or_zero (pole_or_zero:torch.Tensor,
                                 floor_eps:float=1e-10, ceil_eps:float=0,
                                 c:float=1.0, d:float=1.0)

Saturates the absolute value of the complex pole or zero as it approaches the unit circle, preventing filter instability.


source

mtanh

 mtanh (x:torch.Tensor, c:float=1.0, d:float=1.0)

source

biquad_freqz

 biquad_freqz (b:torch.Tensor, a:torch.Tensor,
               n:Union[int,torch.Tensor]=512)
Type Default Details
b Tensor Tensor of shape (…, 3)
a Tensor Tensor of shape (…, 3)
n typing.Union[int, torch.Tensor] 512 Number of points to evaluate the frequency response at
Returns Tensor Tensor of shape (…, n // 2 + 1)
# create a pole zero pair
pole = torch.complex(real=torch.tensor(0.5), imag=torch.tensor(0.3))
zero = torch.complex(real=torch.tensor(0.8), imag=torch.tensor(-0.4))

# convert to IIR coefficients
b = pole_or_zero_to_iir_coeff(zero)
a = pole_or_zero_to_iir_coeff(constrain_complex_pole_or_zero(pole))

# compute magnitude response
H = biquad_freqz(b, a, 129)
H_mag = H.abs()

# plot
plt.stem(H_mag)
H.shape: torch.Size([65])
<StemContainer object of 3 artists>

Main Interface


source

IIRMethod

 IIRMethod (value, names=None, module=None, qualname=None, type=None,
            start=1)

An enumeration over IIR computation backends. Available options:

  • IIRMethod.FFT: Uses the FFT frequency sampling method.
  • IIRMethod.TDFII: Uses the TDF-II algorithm.

source

IIRParameters

 IIRParameters (b:Optional[torch.Tensor]=None,
                a:Optional[torch.Tensor]=None,
                poles:Optional[torch.Tensor]=None,
                zeros:Optional[torch.Tensor]=None,
                gains:Optional[torch.Tensor]=None,
                constrain_poles:bool=True, constrain_zeros:bool=True,
                constraint_eps:float=0.0)

A class for storing IIR filter parameters.

Type Default Details
b typing.Optional[torch.Tensor] None
a typing.Optional[torch.Tensor] None The numerator coefficients (…, n_parallel, n_biquad, 3)
poles typing.Optional[torch.Tensor] None The denominator coefficients (…, n_parallel, n_biquad, 3)
zeros typing.Optional[torch.Tensor] None The complex poles (…, n_parallel, n_biquad)
gains typing.Optional[torch.Tensor] None The complex zeros (…, n_parallel, n_biquad)
constrain_poles bool True Whether to constrain the poles to the unit circle
constrain_zeros bool True Whether to constrain the zeros to the unit circle
constraint_eps float 0.0 The epsilon value to use for pole/zero constraint

We can demonstrate hybrid freqz computation by creating a network of passthrough filters:

n_biquads = 15
n_parallel = 8
n_samples = 1000

b = torch.zeros(n_parallel, n_biquads, 3)
b[:, :, 0] = 1.0
a = torch.zeros(n_parallel, n_biquads, 3)
a[:, :, 0] = 1.0

gains = torch.ones(n_parallel, n_biquads) / (n_parallel ** (1 / n_biquads))
params = IIRParameters(b=b, a=a, gains=gains)

plt.stem(params.freqz(128).abs())
plt.show()


source

apply_iir

 apply_iir (x:torch.Tensor, parameters:__main__.IIRParameters,
            method:__main__.IIRMethod=<function _apply_biquad_fft>,
            expand_x:bool=True, reduce_x:bool=True)

Applies a biquad filter to a signal. The filter can be specified in two ways:

  1. Directly as the biquad coefficients (b, a)
  2. As the complex poles and zeros (poles, zeros)

In both cases, the filter can be specified as a parallel (n_biquad = 1), cascade (n_parallel = 1), or hybrid ((n_biquad > 1) and (n_parallel > 1)).

The method parameter specifies the method to use for filtering. The FFT method is computed without recursion by frequency sampling, while the TDFII method is computed recursively using the transposed direct form II structure.

Type Default Details
x Tensor The input signal (…, time)
parameters IIRParameters The IIR filter parameters
method IIRMethod _apply_biquad_fft The method to use for filtering
expand_x bool True Whether to expand the input signal to match the number of parallel filters
reduce_x bool True Whether to sum the output signal along the parallel filter dimension

Test that expected shape is returned:

n_biquads = 2
n_parallel = 9
n_samples = 1000
batch_size = 4

poles = torch.exp(-1j * torch.rand(batch_size, n_parallel, n_biquads) * np.pi)
zeros = torch.exp(-1j * torch.rand(batch_size, n_parallel, n_biquads) * np.pi)
gains = torch.rand(batch_size, n_parallel, n_biquads)
params = IIRParameters(poles=poles, zeros=zeros, gains=gains)

x = torch.rand(batch_size, n_samples)

assert apply_iir(x, params, IIRMethod.FFT).shape == x.shape
n_biquads = 2
n_parallel = 9
n_samples = 1000
batch_size = 4

b = torch.rand(batch_size, n_parallel, n_biquads, 3)
a = torch.rand(batch_size, n_parallel, n_biquads, 3)
gains = None
params = IIRParameters(b=b, a=a, gains=gains)

x = torch.rand(batch_size, n_samples)

assert apply_iir(x, params, IIRMethod.TDFII).shape == x.shape

We can also have an optimized version of the above


source

apply_filter

 apply_filter (x_arr:numpy.ndarray, a:numpy.ndarray, b:numpy.ndarray,
               state:Optional[numpy.ndarray]=None)
Type Default Details
x_arr ndarray [n_samples]
a ndarray [n_parallel, n_biquads, 3]
b ndarray [n_parallel, n_biquads, 3]
state typing.Optional[numpy.ndarray] None [n_parallel, n_biquads, 2]

Test fast filter

# create a pole zero pair
pole = torch.complex(real=torch.tensor(0.5), imag=torch.tensor(0.3))
zero = torch.complex(real=torch.tensor(0.8), imag=torch.tensor(-0.4))

# convert to IIR coefficients
b = pole_or_zero_to_iir_coeff(zero)
a = pole_or_zero_to_iir_coeff(constrain_complex_pole_or_zero(pole))

b = b.view(1, 1, 3)
a = a.view(1, 1, 3)

impulse = torch.zeros(256)
impulse[0] = 1.0

gains = None
params = IIRParameters(b=b, a=a)
filtered_torch = apply_iir(impulse.unsqueeze(0), params, IIRMethod.TDFII)

filtered = apply_filter(
    x_arr=impulse.cpu().numpy(),
    a = a.cpu().numpy(),
    b = b.cpu().numpy(),
)

# check that the output is the same
assert np.allclose(filtered, filtered_torch[0].detach().cpu().numpy())