This article shows how to convert from real-valued radio signals to complex IQ signals and back with some example code in Python.
Introduction to real and complex IQ signals
Complex IQ or quadrature signals are often used in radio technology. IQ signals provide a convenient way of assigning amplitude and phase information to each data sample. Thus, IQ signals are well suited for describing modulated signals that continuously change their phase, frequency or amplitude, or when image reject mixers (IQ mixers) for up and down-conversion are present (see also SSB Demodulation).
Real signals consist of ordinary real-valued data samples, whereas IQ signals consist of complex numbers, i.e. each sample has two values, one representing the “real” or I part and the second the “imaginary” or Q part.
In the frequency domain, the spectrum of a real signal consists only of positive frequencies. Negative frequencies may also be displayed, but they essentially mirror the positive part of the spectrum. Therefore negative frequencies have no meaning for real-valued signals (for an explanation of the meaning of negative frequencies, see Quadrature Signals: Complex, but not complicated). For a digital real-valued signal with sampling frequency fs, the possible frequency range is 0 to fs/2.
In contrast, the spectrum of complex IQ signals ranges from -f/2 to +fs/2. It also includes negative frequencies and therefore covers a bandwidth of fs, which is twice the bandwidth of real-valued signals.
Converting real-valued to complex IQ signals
A real-valued signal has a symmetric spectrum around f = 0, i.e. the negative frequency content mirrors the positive frequency content as shown above. To make the signal a complex IQ signal, we can aim to remove the symmetric negative frequencies. Note that in a complex signal the negative frequencies are in general independent from the positive frequencies and contain additional signal components. The resulting signal in the time domain is the complex-valued IQ signal. It is also called the “analytic signal” of the original real signal.
Zeroing out the negative frequencies is related to the Hilbert transform (see Analytic signal – Wikipedia for details). The Hilbert transform (HT) is a mathematical operation, that shifts the input signal by a phase shift of 90°. The real-valued signal and the imaginary part of its IQ signal are related by the Hilbert transform.
Therefore converting from real to IQ is straightforward. Simply apply the Hilbert transform to the real-valued signal and assemble the complex signal: IQ(t)=real(t)+j\cdot HT\left(real(t)\right)
Since the resulting IQ signal covers twice the bandwidth of a real signal (see above), the sampling rate fs is now excessive by a factor of 2. This allows the sampling rate to be halved after the spectrum has been centred around 0 Hz.
Python Code
In Python, the predefined function scipy.signal.hilbert generates the IQ signal from a real-valued input. Note, that scipy’s “hilbert” function does not implement the pure Hilbert transform, but already returns the full analytic (IQ) signal.
import numpy as np
from scipy.signal import hilbert
# define some real signal
fs_iq = 1e6
signal_real = np.sin(2*np.pi* 0.01e6 * np.arange(1000) * 1/fs_iq)
# convert to iq is as simple as
signal_iq = hilbert(signal_real)
Converting complex IQ to real-valued signals
Firstly, the conversion from complex IQ to real-valued signals is more than just taking the real parts of the complex IQ samples. This is because complex IQ signals contain positive and negative frequencies, that span from -fs/2 to fs/2. Taking only the real part would result in a superposition of negative and positive frequency content, which is not desireable. Therefore, in order to make the signal real, the negative frequencies must be shifted to positive frequencies first.
The following three-step approach safely turns IQ signals into their a real-valued counterparts:
From IQ to real in three steps
- Interpolate the signal by a factor of 2 in order to increase the sample frequency. Now a frequency range from -fs to +fs is possible, which is a prerequisite for step 2.
- Shift the IQ signal by fs/2 to the right in order to move it completely to positive frequencies. This is done by multiplying the signal by a complex sine of frequency fs/2 (i.e. a complex mixer).
- Obtain the real signal by simply discarding the imaginary part of each complex sample. Note, that for the resulting real-valued signal the sample rate is twice the original one.
Python Code
import numpy as np
from scipy.signal import resample_poly
# define some IQ signal
fs_iq = 1e6
signal_iq = np.exp(1j*2*np.pi* 0.01e6 * np.arange(1000) * 1/fs_iq)
# step 1: interpolation
signal_iq_interp_real = resample_poly(signal_iq.real, up=2, down=1, padtype='line')
signal_iq_interp_imag = resample_poly(signal_iq.imag, up=2, down=1, padtype='line')
signal_iq_interp = signal_iq_interp_real + 1j * signal_iq_interp_imag
# step 2: shift IQ signal to positive frequencies
freq_shift = fs_iq/2
fs_real = fs_iq * 2
time_vector = np.arange(len(signal_iq_interp))
complex_sine = np.exp(1j*2*np.pi* (freq_shift/fs_real) * time_vector)
signal_shifted = signal_iq_interp * complex_sine
# step 3: take real part
signal_real = signal_shifted.real
Further Reading
Some good background information on I/Q signals and the concept of negative frequencies, as well as the the analytic signal and the Hilbert transform can be found here:
- Good introduction to I/Q signals: Rick Lyon’s Quadrature Signals: Complex, but not complicated
- Another good introduction to I/Q signals: I/Q data for Dummies
- Detailed information on the analytic signal: Analytic signal – Wikipedia
- Python documentation on the Hilbert function: scipy.signal.hilbert