1
$\begingroup$

I have posted a summary of what I am seeing, I just made a pulse train and testing a notch filter on it as an example only.

I also have a complex baseband signal centered on 0 Hz, this complex baseband signal has a complex exponential in it that is only on the positive frequency - this is an interference I need to remove - hence a notch filter to only remove a positive frequency. I cant post this code here because there is too much so i posted a pulse train and a notch filter instead.

My notch filter is applied on both the positive and negative frequency. I cannot see why using iirfilter and lfilter in scipy is not doing it correctly on a complex signal. It applies the notch on the positive and negative frequencies instead of just one.

What am I missing here? Any advice appreciated!

import numpy as np
import matplotlib.pyplot as plt
from scipy import signal

plt.close('all')

#Sampling
Fs = 40e3 # samples per second
Ts = 1/Fs
Ns = int(Fs)            #if =FS then resolution = 1
t = np.arange(Ns) * Ts # time vector for carrier 
fftsize = Ns #Full length FFT
resolution = Fs / fftsize
f = np.arange(-Fs/2, Fs/2,resolution)


#Pulses
pulse_span = 50
pulse_duration = pulse_span*Ts
data = np.random.randint(0,2,int(Ns/pulse_span))
data = (data - 0.5) 

x = np.zeros((Ns))
for i in range(len(data)):
    
    increment_low = i*pulse_span
    increment_high = increment_low + pulse_span
    x[increment_low:increment_high] = data[i]
    
    
#Filter Function
def Implement_Notch_Filter(time, band, freq, ripple, order, filter_type, data):
    from scipy.signal import iirfilter
    fs   = 1/time
    nyq  = fs/2.0
    low  = freq - band/2.0
    high = freq + band/2.0
    low  = low/nyq
    high = high/nyq
    b, a = signal.iirfilter(order, [low, high], rp=ripple, btype='bandstop',
                     analog=False, ftype=filter_type)
    filtered_data = signal.lfilter(b, a, data)
    return filtered_data
x = Implement_Notch_Filter(1/Fs,50,200,0.5,2,'butter',x)



#Spectrum
X = np.fft.fft(x,fftsize)/fftsize
X = np.fft.fftshift(X)
X = abs(X)
X_PSD = 10*np.log10(              abs((X))               **2)
    

fig = plt.figure(2)
ax = fig.add_subplot(111)
ax.title.set_text('Frequency: Spectrum dB ')
ax.plot(f,X_PSD)
plt.ylabel('Power (dBW/' + str(int(resolution)) + ' Hz)')
plt.xlabel('Frequency')
plt.ylim([-80, -20 ])

enter image description here

$\endgroup$
3
  • 1
    $\begingroup$ You're missing the point that the negative frequencies are a property of the DFT of the (real) time domain signal, not of the time-domain signal itself. Since the time-domain signal is real, its DFT is symmetric around frequency $0$ and contains redundant information for negative frequencies. It's probably most useful to only consider frequencies from 0 to half the sampling frequency in this case. $\endgroup$
    – applesoup
    Commented Oct 25, 2021 at 11:06
  • $\begingroup$ @applesoup Hi, thank you for the reply. I should also add regardless of whether my time domain signal is real or not. This actually isnt my full code. I try to apply this notch filter on a complex baseband signal to remove a complex exponential from it, where that complex exponential is not symmetrical in the spectrum and only appears in the positive frequencies. Hence i need a notch filter that only removes the positive part of the spectrum at the notch frequency. $\endgroup$ Commented Oct 25, 2021 at 11:10
  • $\begingroup$ Ah, I see. You'll need a complex filter, then. Maybe this insight serves as a starting point for some more investigation on your side. $\endgroup$
    – applesoup
    Commented Oct 25, 2021 at 15:00

3 Answers 3

3
$\begingroup$

any real-valued system, such as a filter as yours, is symmetrical in spectrum. So, what you observe is inevitable. This is kind of a direct result from the fact that real signals are always symmetrical in spectrum, and a real system can't make a complex signal out of a real signal.

If you need a one-sided filter, it needs to be complex. So, instead of designing a real notch filter with two notch frequencies $\pm f_\text{notch}$, you'd simply design a DC cancelling notch filter ($f_\text{notch}=0$) and shift that up in frequency (either by modulating the impulse response, but that's not as easily done with IIRs as it is for FIRs, or by shifting the signal down, filtering it, and then shifting it back up – all that modulation/shifting would need to be done with a complex sinusoidal $e^{jf_\text{shift} t}$.

You could also directly design a complex notch filter, instead of a real-valued one.

$\endgroup$
2
  • $\begingroup$ Great thanks. So the Scipy signal processing filters are real valued filters and not complex, Do you know of functions that make complex valued filters? i.e. a complex version of scipy.signal.iirfilter() ? $\endgroup$ Commented Oct 25, 2021 at 12:30
  • $\begingroup$ nope; I'd use the frequency shifting design approach $\endgroup$ Commented Oct 25, 2021 at 12:31
3
$\begingroup$

Here is one way to design a complex notch IIR filter

  1. Start with a real IIR filter, determine poles, zeros and gain
  2. Remove all the poles and zeros at negative frequencies, take the root of the gain
  3. Turn back into polynomials and run as a complex filter.

I don't know whether scipy.signal.iirfilter() supports complex filter coefficients, but Matlab's filter() sure does. If not, it's easy enough to write your own.

If you have a higher order filter and/or poles close to the unit circle, you probably want to break this into sections. Since you don't have a real value constraint you don't need to do second order sections, you can simply do all first order sections: just pair any pole with a closest unused zero and apply the pole/zero pairs sequentially.

A notch filter is a good candidate for this, since it's flat at Nyuist, so you don't run into discontinuities caused by periodic repetition. A high pass or low pass would be tricky.

You can also do an FIR design.

  1. Design the real valued FFR
  2. Perform high resolution FFT
  3. Set all negative frequencies to 1
  4. Do an inverse FFT and window the real and imaginary parts to the desired length/ accuracy.
$\endgroup$
2
  • 1
    $\begingroup$ nice, you're right – doing a complex notch "from scratch" is anything but prohibitively complicated here. $\endgroup$ Commented Oct 25, 2021 at 14:02
  • $\begingroup$ Use output='zpk' in iirfilter to get the poles/zeros directly $\endgroup$
    – endolith
    Commented Oct 25, 2021 at 15:22
2
$\begingroup$

Why not just implement a complex notch filter as described on page 20 of these notes.

Not filter equations.

This gives the following frequency response.

Frequency response example.

You might want to change the 0.9 figure to something closer to 1. And I'm not clear on what the frequency you're trying to knock out is.


Code below

Full Jupyter Notebook here.

# Page 20 of https://courses.engr.illinois.edu/ece401/fa2020/slides/lec14.pdf
import cmath
def Implement_Simple_Notch_Filter(time, freq, data):
    b = [1, -cmath.exp(1j*freq*time*2*3.13145926)]
    a = [1, -0.9*cmath.exp(1j*freq*time*2*3.13145926)]
    filtered_data = signal.lfilter(b, a, data)
    return filtered_data,b,a    
$\endgroup$
3
  • 1
    $\begingroup$ Nice! If one wants to experiment with such filter designs, pyfda is neat, and installed via python3 -m venv pyfda-venv, then run via source pyfda-venv/bin/activate; pyfdax; you can then enter the zero and pole in the "P/Z" table, load it into the analyzer and get nice plots for frequency and time domain properties of the filter. $\endgroup$ Commented Oct 25, 2021 at 15:42
  • $\begingroup$ Cool! Thanks, Marcus. I'll look into that. $\endgroup$
    – Peter K.
    Commented Oct 25, 2021 at 15:43
  • $\begingroup$ typo: installation via python3 -m venv pyfda-venv;source pyfda-venv/bin/activate; pip3 install pyfda $\endgroup$ Commented Oct 25, 2021 at 16:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.