Chapter 4

I/Q Fundamentals and Complex Baseband

If you understand I/Q, you can read any RF system in the world. If you don't, you'll always be one layer above the math, taking someone else's word for it.

4.1 Why I/Q: The Analytic Signal and One-Sided Spectra

Open any RTSA file format and you will find a stream of pairs. Each pair is two numbers: I (in-phase) and Q (quadrature). Two real-valued samples, treated as a single complex number. This is the language of modern radio.

It is also the source of more confusion in introductory RF education than any other topic. So let's clear it up.

A real-valued radio signal $s(t)$ has a spectrum that is symmetric across DC. Every component at frequency $+f$ has a mirror at $-f$, and the two halves carry redundant information. If your spectrum analyzer only knew how to handle real signals, half its dynamic range would be wasted on mirror images of every signal you tried to look at.

Worse, you cannot tell from a real-valued sample stream whether a signal is moving up in frequency or down. A 1 kHz tone and a -1 kHz tone produce indistinguishable real samples. The phase information that tells you "this signal is rotating clockwise versus counterclockwise" is lost.

The analytic signal solves both problems. It is a complex-valued signal whose spectrum exists only on the positive frequency axis:

$$z(t) = s(t) + j \hat{s}(t)$$

where $\hat{s}(t)$ is the Hilbert transform of $s(t)$, a 90-degree phase shifter applied to every frequency component. The analytic signal has the same information content as the original, but the negative frequencies have been deleted along with their redundancy.

Run an analytic signal through an FFT and you see a one-sided spectrum: positive frequencies are real signals, negative frequencies are signals below the carrier. Direction of rotation is preserved. Bandwidth is fully utilized. Your spectrum analyzer's screen real estate doubles in usefulness.

In a hardware receiver, the analytic signal is constructed by quadrature downconversion rather than the Hilbert transform. The I path multiplies by $\cos$, the Q path multiplies by $\sin$, and the result is treated as the real and imaginary parts of $z(t)$. Same outcome, different machinery.

This is why every modern RTSA stores I/Q samples instead of real samples. Half the storage cost, no information loss, and easier downstream math.

Figure 4-1
Figure 4-1. A real-valued signal has a symmetric two-sided spectrum that wastes half its bandwidth on a redundant mirror. The analytic signal removes the negative-frequency mirror and is implemented in hardware by quadrature downconversion. The I/Q pair is the analytic signal in disguise.

4.2 Euler, Complex Exponentials, and Rotating Phasors

Euler's identity is the single most useful equation in radio:

$$e^{j\theta} = \cos\theta + j\sin\theta$$

Plug in $\theta = 2\pi f t$ and you get a complex sinusoid:

$$e^{j 2\pi f t} = \cos(2\pi f t) + j \sin(2\pi f t)$$

This is a unit-magnitude phasor rotating in the complex plane at frequency $f$ revolutions per second. Project it onto the real axis and you get the cosine. Project onto the imaginary axis and you get the sine. The phasor encodes both at once.

Now suppose you have a real cosine at frequency $f$:

$$\cos(2\pi f t) = \frac{e^{j 2\pi f t} + e^{-j 2\pi f t}}{2}$$

A real cosine is the sum of a positive-frequency phasor and a negative-frequency phasor of equal magnitude. That is the mathematical reason real signals have symmetric spectra. The two phasors are the mirror image of each other around DC.

The analytic signal of a real cosine keeps only the positive-frequency phasor:

$$z(t) = e^{j 2\pi f t}$$

This is a single rotating phasor. Its spectrum is a single point at $+f$. No mirror, no ambiguity.

Modulation as Phasor Multiplication

A baseband signal $b(t)$ modulated onto a carrier becomes:

$$s(t) = \text{Re}\{b(t) \cdot e^{j 2\pi f_c t}\}$$

where $b(t)$ is itself complex (containing both amplitude and phase modulation). On a real-valued physical antenna, only the real part propagates. But mathematically, the full signal lives in the complex plane.

When this signal arrives at a quadrature receiver, multiplying by $e^{-j 2\pi f_c t}$ in the I/Q mixer recovers $b(t)$. The complex multiplication is the rotation that brings the carrier back to DC, leaving the baseband modulation untouched.

This is the entire engine of digital communication. Every QAM constellation, every OFDM symbol, every chirp pulse is a complex baseband signal pulled into the receiver, rotated to DC, and decoded as a sequence of complex numbers.

4.3 Sampling I/Q vs Sampling RF

There are two ways to digitize an RF signal. Real sampling at twice the highest frequency, or quadrature sampling at the bandwidth.

Real Sampling

Suppose you want to digitize a signal centered at 2 GHz with 100 MHz of bandwidth. Real sampling requires:

$$f_s > 2 \cdot 2.05\,\text{GHz} = 4.1\,\text{GS/s}$$

at minimum, more in practice. That is a punishing rate. The ADC and storage are expensive, and most of the samples are wasted on encoding the carrier rather than the modulation.

Quadrature Sampling

The same signal sampled in I/Q after downconversion to baseband requires:

$$f_s > 100\,\text{MHz}$$

complex (which is 100 MS/s on each of the I and Q channels). The carrier is removed analytically before digitization. The ADC handles the modulation only.

The reduction is enormous. A 41x improvement in sampling rate for the same information content. This is why every wideband RF instrument built since 2000 uses quadrature sampling.

When Real Sampling Still Appears

Some architectures sample at the IF (a low intermediate frequency, say 100 to 300 MHz) and then digitally convert to baseband. The math is exactly equivalent to analog quadrature sampling, but the implementation is different: a single ADC handles a real-valued IF signal at maybe 500 MS/s, and a digital downconverter inside the FPGA produces complex baseband. This trades analog mixer complexity for digital signal processing complexity.

In modern RTSAs, both approaches show up. The Aaronia SPECTRAN V6 PLUS uses analog quadrature downconversion to baseband, sampling I and Q on parallel ADCs. Other instrument families use IF sampling followed by digital quadrature conversion. Both end up with the same complex baseband stream.

4.4 I/Q Imbalance and Correction

Because real-world I and Q paths involve separate mixers, separate filters, and separate ADCs, they are never perfectly matched. Two systematic errors result.

Amplitude Imbalance

If the I path has a gain $G_I$ and the Q path has $G_Q$, with $G_I \neq G_Q$, the rotating phasor becomes elliptical instead of circular. A pure tone produces a constellation that traces an ellipse rather than a circle.

Mathematically, the imbalance produces a small image of every signal mirrored across DC. Suppose the true signal is at $+1$ MHz. With a 1 percent amplitude imbalance, you also see a residual signal at $-1$ MHz, about 40 dB below the original.

Phase Imbalance

If the I and Q local oscillators are not perfectly 90 degrees apart (suppose $\phi$ degrees off), a similar image appears at the mirror frequency. A 1 degree phase error produces a -41 dB image. A 0.1 degree error produces -61 dB.

Combined Imbalance

The general expression for the image rejection ratio (IRR) given amplitude imbalance $\epsilon$ and phase imbalance $\phi$ in radians is:

$$\text{IRR} \approx \frac{4}{\epsilon^2 + \phi^2}\,\text{(linear)}$$

A 1 percent amplitude imbalance and a 1 degree phase imbalance combine to give about 35 dB of image rejection. That is not enough for most measurements. Modern RTSAs digitally correct down to 60, 70, even 80 dB IRR.

Digital Correction

The correction is a 2x2 matrix applied to the (I, Q) pair after digitization, with calibration parameters set at the factory across temperature and frequency, then applied in the FPGA as a continuous correction.

Aaronia RTSA Suite PRO offers user-accessible image rejection calibration as part of its commissioning workflow. The calibration uses a known reference signal, measures the residual image, and tunes the correction matrix until the image drops below a target threshold.

4.5 Reading an I/Q File

Real engineering work happens when you take an I/Q capture out of the RTSA and feed it into Python, MATLAB, or GNU Radio for custom analysis. To do that, you need to know what's in the file.

File Formats in the Wild

FormatSample typeMetadataCommon producers
Raw complex int16I, Q interleaved 16-bit signedNone (filename only)RTL-SDR, BladeRF, custom
Raw complex float32I, Q interleaved 32-bit floatNoneGNU Radio, MATLAB
SigMFFloat or int with sidecar JSONYes (.sigmf-meta)RTSA Suite PRO, GNU Radio
WAV (2 channel)I left, Q rightRIFF headerMany SDR tools
HDF5ConfigurableRich metadataResearch tools

The two most important to recognize are raw interleaved complex (no metadata, just a stream of I, Q, I, Q, ...) and SigMF (the modern standard with proper metadata).

SigMF in 60 Seconds

SigMF (Signal Metadata Format) is a JSON sidecar that pairs with a raw I/Q file. The metadata file describes the data file:

{
  "global": {
    "core:datatype": "ci16_le",
    "core:sample_rate": 245000000,
    "core:hw": "Aaronia SPECTRAN V6 PLUS 2000XA-6",
    "core:author": "RTSA Suite PRO"
  },
  "captures": [{
    "core:sample_start": 0,
    "core:frequency": 3500000000,
    "core:datetime": "2026-04-25T14:32:11.000Z"
  }]
}

This tells any consuming tool: data is complex int16 little-endian, sample rate 245 MHz, captured at 3.5 GHz center on a SPECTRAN V6 PLUS, on April 25, 2026. The data file is just a stream of int16 pairs, byte-for-byte identical to a raw capture, but now interpretable.

RTSA Suite PRO writes SigMF natively. GNU Radio reads it. scikit-rf reads it. Any modern analysis tool reads it. This format is becoming standard, and it deserves to.

Reading Raw Complex int16 in Python

A 1-second capture at 245 MS/s of complex int16 is about 980 megabytes. Reading it:

import numpy as np

# Each sample is 2 bytes I + 2 bytes Q = 4 bytes total
raw = np.fromfile('capture.sigmf-data', dtype=np.int16)
iq = raw[::2].astype(np.float32) + 1j * raw[1::2].astype(np.float32)
iq /= 32768.0  # normalize to [-1, +1]

print(f"Loaded {len(iq)} complex samples")
print(f"Duration: {len(iq) / 245e6:.3f} seconds")

That is the entire decoder. After this, iq is a NumPy complex array ready for FFT, filtering, demodulation, or anything else.

For SigMF files, use the sigmf Python package:

from sigmf import sigmffile

handle = sigmffile.fromfile('capture.sigmf-meta')
samples = handle.read_samples()  # returns complex64 directly

That's it. Two lines and you have your samples.

Reading Bigger Captures

A 24-hour capture at 245 MHz is about 84 terabytes. You don't load that into memory. You memory-map it:

raw = np.memmap('huge_capture.sigmf-data', dtype=np.int16, mode='r')
# raw is a virtual array; the OS pages in chunks as you index

Combined with chunk-wise processing (read 1 second of samples, FFT, save spectra, drop the buffer, repeat), this scales to arbitrarily large recordings on commodity hardware.

4.6 Worked Example: Decoding a Burst from Raw I/Q

Here is a minimum-viable end-to-end analysis. We have a SigMF capture from a SPECTRAN V6 PLUS that contains a Bluetooth Low Energy advertising burst at 2.402 GHz. We want to find the burst, isolate it, and decode the modulation type.

Step 1: Load the Capture

from sigmf import sigmffile
import numpy as np

handle = sigmffile.fromfile('ble_capture.sigmf-meta')
fs = handle.get_global_field('core:sample_rate')      # 245 MHz
fc = handle.get_capture_info(0)['core:frequency']     # 2.402 GHz
iq = handle.read_samples()
print(f"{len(iq)} samples at {fs/1e6} MHz, centered at {fc/1e9} GHz")

Step 2: Find the Burst

A BLE advertising packet lasts roughly 376 microseconds. We slide an energy detector across the capture:

window = int(fs * 50e-6)  # 50 microsecond window
power = np.abs(iq) ** 2
energy = np.convolve(power, np.ones(window) / window, mode='valid')
threshold = np.percentile(energy, 99)
bursts = np.where(energy > threshold)[0]
print(f"Detected burst start at sample {bursts[0]}")

Step 3: Isolate the Burst

start = bursts[0]
duration_samples = int(fs * 400e-6)  # slightly longer than expected
burst = iq[start : start + duration_samples]

Step 4: Detect Modulation

BLE uses Gaussian Frequency Shift Keying (GFSK). Frequency shifts encode the bits. We compute the instantaneous frequency by taking the derivative of the unwrapped phase:

phase = np.unwrap(np.angle(burst))
freq = np.diff(phase) * fs / (2 * np.pi)
print(f"Frequency excursion: {freq.min()/1e3:.1f} kHz to {freq.max()/1e3:.1f} kHz")

A symmetric excursion of about plus and minus 250 kHz around the carrier confirms BLE 1M PHY GFSK. An excursion of plus and minus 500 kHz confirms BLE 2M PHY. Other excursions might suggest BLE coded PHYs or non-BLE devices entirely.

Step 5: Visualize

from scipy import signal
import matplotlib.pyplot as plt

f, t, Sxx = signal.spectrogram(burst, fs=fs, nperseg=1024, noverlap=896)
plt.pcolormesh(t * 1e6, f / 1e6, 10 * np.log10(Sxx))
plt.xlabel('time [us]')
plt.ylabel('frequency [MHz from carrier]')
plt.colorbar(label='dB')
plt.show()

That is one hundred lines of code, end-to-end, from raw I/Q to confirmed modulation type. The same workflow generalizes to any burst-mode signal: Wi-Fi management frames, LoRa chirps, drone control packets, radar pulses, automotive key fob transmissions.

This is the productivity multiplier of working in I/Q. You are not limited to the analyses your instrument vendor wrote. You can write your own.

Aaronia in Practice: RTSA Suite PRO I/Q Pipeline

RTSA Suite PRO surfaces the I/Q pipeline as an editable graph, with each block representing a signal-processing stage and edges representing complex baseband streams. A typical I/Q processing graph might be: SPECTRAN source brings live I/Q from the hardware, frequency translator shifts the band of interest to baseband, decimator drops sample rate from 245 MHz to 5 MHz, filter block applies a low-pass, modulation classifier identifies the modulation type, demodulator produces decoded symbols, file writer archives both raw I/Q and decoded output to disk.

Each block exposes parameters editable in real time. The graph runs continuously, so changing a parameter updates the analysis immediately. A user with no programming experience can build sophisticated pipelines by dragging and connecting blocks. A user with programming experience can write custom blocks in Python or C++ and load them into the same framework. The same pipeline runs on live data and on recorded files. Drop a SigMF file onto the SPECTRAN source block and Suite PRO replays it through the entire chain as if the signal were live.

Chapter Summary

End-of-Chapter Quiz

Check your understanding

The Chapter 4 questions are now an interactive quiz. Pick an answer for each, get instant scoring, and see why each answer is right. Your progress is saved on this device.

Take the interactive quiz →