Cyclic Autocorrelation Function (CAF): How BPSK and QPSK Differ in Cyclostationary Analysis
The Cyclic Autocorrelation Function (CAF) reveals hidden periodic structure in modulated signals. This article explains what the CAF is, how to compute it, and why BPSK and QPSK produce fundamentally different CAF signatures — a difference that cannot be seen in a standard power spectrum.
In the previous article, we established that a cyclostationary signal has an autocorrelation that is periodic in time at a fixed lag . We also introduced the Spectral Correlation Function (SCF) as a way to visualise this periodic structure. The missing piece is the tool that sits between the raw time-varying autocorrelation and the SCF: the Cyclic Autocorrelation Function (CAF).
The CAF is the function that extracts the periodic component of with respect to , at each lag . It is the foundation that the SCF is built on, and it is the tool that makes modulation classification possible — as we will see, BPSK and QPSK produce fundamentally different CAF signatures at a specific cycle frequency that makes them directly distinguishable.
What is the Cyclic Autocorrelation Function?
Recall from the previous article that for a cyclostationary signal, the time-varying autocorrelation:
is periodic in with period . Since it is periodic in , it has a Fourier series expansion. The CAF is defined as the Fourier series coefficient of at cycle frequency :
where:
- is the lag — how far apart the two samples being compared are
- is the cycle frequency — the specific periodic component being extracted from
- is a complex tone at frequency . Multiplying by this tone and averaging over extracts only the component of the autocorrelation that oscillates at exactly . All other components average to zero
- The limit over means we average over a long observation window to suppress noise
The result is a single complex-valued function of lag , computed at one specific cycle frequency .
The key test for cyclostationarity: if for some , the signal is cyclostationary at that cycle frequency. Noise produces for all , because noise has no periodic structure to extract.
The discrete-time estimator
In practice the signal is sampled, so the continuous integral becomes a sum. The discrete-time estimator of the CAF is:
where:
- is the sample index
- is the total number of samples — a larger gives a more accurate estimate
- is the sample rate in Hz
- and are the signal and its conjugate, separated by lag
- is the complex tone at cycle frequency , evaluated at each sample
This estimator uses a one-sided lag convention, where one sample is shifted by and the other is not. The standard definition from Gardner uses a symmetric convention where one sample is shifted by and the other by . The two are equivalent up to a phase factor that does not affect the magnitude, which is what we plot.
BPSK and QPSK signals
Before computing the CAF, it helps to see what the two signals look like.
Figure 1 — BPSK switches between two phases (0° and 180°). QPSK switches between four phases (45°, 135°, 225°, 315°). Both use the same carrier frequency and symbol rate.
The signal parameters used throughout this article are: sample rate Hz, carrier frequency Hz, and symbol rate symbols per second, giving a symbol period seconds and 200 samples per symbol.
The two signals look visually similar in the time domain. A standard power spectrum would also show two similar peaks at . The CAF is the tool that reveals the difference between them.
CAF at : ordinary autocorrelation
When , the complex exponential , so the CAF reduces to the ordinary autocorrelation:
Figure 2 — CAF at for BPSK (left) and QPSK (right). Both show a peak at — this is just the ordinary autocorrelation and does not distinguish between the two modulations.
At , both BPSK and QPSK respond with a peak at . This tells us there is signal power, but nothing about the modulation type. To distinguish between them we need to evaluate the CAF at non-zero cycle frequencies.
CAF at : the baud-rate feature
The symbol rate is a cycle frequency present in both BPSK and QPSK. Both signals have symbols that repeat with period , so both exhibit cyclic structure at .
Figure 3 — CAF at Hz for BPSK (left) and QPSK (right). Both show a peak at , confirming that both signals have cyclic structure at the baud rate. This cycle frequency alone cannot distinguish between them.
The peak at and at (lag equal to one symbol period) confirms that both signals are cyclostationary at the baud rate. However since both respond, this cycle frequency by itself is not sufficient to classify the modulation type.
CAF at : the key discriminator
This is the result that the article has been building towards. BPSK has non-zero conjugate cyclostationarity — it produces a peak in the CAF at cycle frequency . QPSK, and all other complex-valued symmetric constellations such as 16-QAM and 8-PSK, have zero conjugate cyclostationarity — they produce no peak at .
Figure 4 — CAF at Hz for BPSK (left) and QPSK (right). BPSK shows a clear peak. QPSK is near zero. This single cycle frequency is the discriminator between the two modulations.
Why does BPSK produce a peak at ?
BPSK symbols are real-valued: . Squaring a real symbol gives — a constant. This means when you compute the product without taking the conjugate (the conjugate CAF), the random symbol component cancels and what remains is the carrier term . Expanding this product gives a term at frequency , which produces the peak at .
Why does QPSK produce no peak at ?
QPSK symbols are complex-valued, drawn from . For these symbols, is not constant — it takes values which are complex and average to zero over many symbols. The cancellation that happens for BPSK does not happen for QPSK. The conjugate spectral correlation function is zero for QPSK and all other higher-order PSK and QAM constellations.
The cyclic profile: a summary view
Rather than testing one at a time, we can sweep from 0 to and plot at each value. This gives the cyclic profile — a compact view of which cycle frequencies are active for each signal.
Figure 5 — Cyclic profile for BPSK (top) and QPSK (bottom). BPSK shows peaks at harmonics of and at . QPSK shows peaks at harmonics of only. The presence or absence of the peak is the modulation fingerprint.
The cyclic profile makes the difference between the two modulations immediately visible:
| Cycle frequency | BPSK | QPSK |
|---|---|---|
| (harmonics of baud rate) | Non-zero | Non-zero |
| (doubled carrier) | Non-zero | Zero |
A received signal showing a peak at is BPSK. A signal showing no peak there, only at baud-rate harmonics, is QPSK or a higher-order constellation. This is the basis of second-order modulation classification in SIGINT.
Python code
The complete code to reproduce all figures is provided in the Jupyter notebook below. Here is the CAF estimator function used throughout:
import numpy as np
import matplotlib.pyplot as plt
# ── Parameters ────────────────────────────────────────────────────────────────
fs = 1000 # sample rate (Hz)
fc = 50 # carrier frequency (Hz)
Rs = 5 # symbol rate (symbols/sec)
sps = int(fs/Rs) # samples per symbol = 200
n_symbols = 100 # number of symbols
N = n_symbols * sps
np.random.seed(42)
t = np.arange(N) / fs
# ── Generate BPSK ─────────────────────────────────────────────────────────────
bits_bpsk = np.random.choice([-1, 1], size=n_symbols)
bpsk = np.repeat(bits_bpsk, sps) * np.cos(2*np.pi*fc*t)
# ── Generate QPSK ─────────────────────────────────────────────────────────────
# Four phases: 45°, 135°, 225°, 315°
bits_qpsk = np.random.choice([0, 1, 2, 3], size=n_symbols)
phases = np.array([np.pi/4, 3*np.pi/4, 5*np.pi/4, 7*np.pi/4])
qpsk_syms = np.exp(1j * phases[bits_qpsk])
qpsk_bb = np.repeat(qpsk_syms, sps)
qpsk = np.real(qpsk_bb) * np.cos(2*np.pi*fc*t) \
- np.imag(qpsk_bb) * np.sin(2*np.pi*fc*t)
# ── CAF estimator ─────────────────────────────────────────────────────────────
def compute_caf(signal, alpha, fs, max_lag):
"""
Estimate the CAF magnitude at a given cycle frequency alpha.
signal : input signal (1D array)
alpha : cycle frequency to test (Hz)
fs : sample rate (Hz)
max_lag : maximum lag in samples — sweeps from -max_lag to +max_lag
"""
N = len(signal)
n = np.arange(N)
# Complex tone at cycle frequency alpha
# This extracts only the component of the autocorrelation
# that oscillates at exactly alpha — all other frequencies average to zero
tone = np.exp(-1j * 2*np.pi * alpha/fs * n)
lags = np.arange(-max_lag, max_lag + 1)
caf = np.zeros(len(lags), dtype=complex)
for i, tau in enumerate(lags):
if tau >= 0:
prod = signal[tau:] * np.conj(signal[:N-tau]) * tone[tau:]
else:
prod = signal[:N+tau] * np.conj(signal[-tau:]) * tone[:N+tau]
caf[i] = np.mean(prod)
return lags, np.abs(caf)
max_lag = 2 * sps
# Test at three key cycle frequencies
alpha_0 = 0 # ordinary autocorrelation
alpha_Rs = Rs # baud-rate feature — both BPSK and QPSK respond
alpha_2fc = 2*fc # conjugate feature — BPSK only
lags, caf_bpsk_0 = compute_caf(bpsk, alpha_0, fs, max_lag)
lags, caf_bpsk_Rs = compute_caf(bpsk, alpha_Rs, fs, max_lag)
lags, caf_bpsk_2fc = compute_caf(bpsk, alpha_2fc, fs, max_lag)
lags, caf_qpsk_0 = compute_caf(qpsk, alpha_0, fs, max_lag)
lags, caf_qpsk_Rs = compute_caf(qpsk, alpha_Rs, fs, max_lag)
lags, caf_qpsk_2fc = compute_caf(qpsk, alpha_2fc, fs, max_lag)
Summary
The CAF is the Fourier series coefficient of the time-varying autocorrelation with respect to time . At each cycle frequency , it gives one function of lag . A non-zero peak in at some confirms that the signal is cyclostationary at that frequency.
For BPSK and QPSK specifically:
- Both are cyclostationary at — the baud-rate harmonics
- Only BPSK is cyclostationary at — the conjugate feature
This single difference at is the second-order discriminator between BPSK and QPSK, and it is directly visible in the CAF without any demodulation or prior knowledge of the signal parameters.
Next in the SIGINT series: the Spectral Correlation Function (SCF) in depth — the full 2D surface, how to read the fingerprint across all pairs, and what changes when you add noise.
Jupyter notebook: sigint_02_cyclic_autocorrelation_function.ipynb