OFDM Channel Estimation: LS vs LMMSE


Before reaching for a neural estimator, I wanted a clean baseline: two classical channel estimators, LS and LMMSE, run on the exact same simulated OFDM data, so any gap between them is attributable to the estimator and nothing else. Code’s on GitHub.

The setup

One OFDM symbol, 64 subcarriers, an 8-tap Rayleigh channel with an exponentially decaying power delay profile normalized to unit power:

H[k] = sum_l  a_l * exp(-j*2*pi*k*l/N),      a_l ~ CN(0, p_l),  sum_l p_l = 1

Pilots sit on every 4th subcarrier (16 of 64) and are all-ones, so the received pilot value is the LS estimate at that subcarrier:

y[k] = H[k] + n[k],     n ~ CN(0, sigma^2),   sigma^2 = 10^(-SNR/10)

SNR sweeps 0–30 dB in 5 dB steps, same channel realizations and same pilots at every point, so the sweep isolates the effect of noise and nothing else. Because the taps are Gaussian, the channel covariance is known exactly — it’s just the Fourier transform of the power delay profile:

R[a,b] = E[ H[a] H*[b] ] = sum_l p_l * exp(-j*2*pi*(a-b)*l/N)

Two estimators, one dataset

LS takes the pilot observations as-is and linearly interpolates real and imaginary parts onto all 64 subcarriers. No channel knowledge, no noise knowledge — cheap and dumb by design.

LMMSE is one linear filter applied to the same pilot observations:

W     = R[:, P] @ inv(R[P, P] + sigma^2 * I)
H_hat = W @ y_pilots

R[P,P] relates the pilots to each other, R[:,P] bridges from pilots to every subcarrier, and sigma^2 * I sets how much the estimator leans on statistics versus the raw measurement.

Worth being upfront about: LMMSE here gets the exact R and exact sigma^2. That’s a performance upper bound, not a deployable receiver — a real one has to estimate both, and the gap between true-R LMMSE and estimated-R LMMSE is exactly the room a learned estimator has to compete for.

Results

NMSE in dB, lower is better:

SNR (dB) LS at pilots LS + interp LMMSE gain
0 0.05 −1.21 −5.30 4.1
5 −4.94 −5.69 −8.93 3.2
10 −9.92 −9.40 −13.29 3.9
15 −14.95 −11.86 −18.08 6.2
20 −19.94 −13.00 −22.91 9.9
25 −24.91 −13.46 −27.92 14.5
30 −29.91 −13.62 −32.96 19.3

Three things worth reading off that table:

LS hits a hard error floor around −13.6 dB. Past 15 dB the limit stops being noise and becomes interpolation itself — a straight line can’t follow a curved frequency response, no matter how clean the pilots are. Only more pilots fix that.

LMMSE keeps a clean −1 dB/dB slope with no floor, because R tells it the actual shape of the channel between pilots instead of guessing with a straight line.

LMMSE also beats the raw noise floor (sigma^2) by about 3 dB. Each output combines all 16 correlated pilots, so the noise averages down. LS treats every pilot as an isolated point and gets none of that averaging for free.

The LS at pilots column is really a correctness check rather than a real estimator — it has to equal sigma^2 exactly. If it drifts, the noise or pilot scaling is wrong somewhere upstream.

That LMMSE-with-true-R ceiling is the number I’m chasing next: how close a learned estimator (MLP, CNN, transformer) can get to it when it only sees noisy pilots and has to learn the channel’s structure instead of being handed it.