Crosby’s noise analysis of FM against AM (1937)#

Armstrong demonstrated wideband FM’s quiet reception in 1935. Murray Crosby of RCA explained it. Measured against the reference SNR \(\gamma = P_R/N_0W\) (baseband_snr()), an envelope-detected AM receiver gives at best \(\gamma/3\), because two thirds of the power sits in the carrier (am_output_snr()). An FM discriminator gives \(3\beta^2 P_m\gamma\) (fm_output_snr()): every doubling of the deviation buys 6 dB. Crosby also found the catch. Below a carrier-to-noise ratio of about 10 dB in the FM bandwidth, the improvement collapses (fm_threshold_snr()).

import matplotlib.pyplot as plt
import numpy as np
from scipy.signal import butter, sosfiltfilt

from engineeringkit.communications import am_output_snr, am_signal, carson_bandwidth, fm_output_snr, fm_threshold_snr

Simulated receivers#

A 1 kHz tone in a message band W = 5 kHz, at complex baseband: the AM receiver takes the envelope, the FM receiver differentiates the phase. Each first filters the received signal to its channel (\(\pm W\) for AM, twice the Carson band for FM, since Carson’s band holds only 98% of the power and clipping the rest distorts), as an IF filter would, and low-pass filters the detector output to W. The FM peak deviation is \(\beta W\).

rng = np.random.default_rng(1937)
fs, W, fm_tone = 400e3, 5e3, 1e3
t = np.arange(int(0.2 * fs)) / fs
message = np.cos(2 * np.pi * fm_tone * t)
lowpass = butter(8, W, fs=fs, output="sos")
beta = 5.0
if_am = lowpass
if_fm = butter(8, carson_bandwidth(beta * W, W), fs=fs, output="sos")  # twice Carson: no sideband clipping


def output_snr(recovered, reference):
    recovered, reference = recovered[2000:-2000], reference[2000:-2000]
    recovered = recovered - recovered.mean()  # the AM detector also passes the carrier's DC
    gain = np.dot(recovered, reference) / np.dot(reference, reference)
    return np.mean((gain * reference) ** 2) / np.mean((recovered - gain * reference) ** 2)


def receive(signal, gamma):
    power = np.mean(np.abs(signal) ** 2)
    N0 = power / (gamma * W)
    noise = (rng.standard_normal(t.size) + 1j * rng.standard_normal(t.size)) * np.sqrt(N0 * fs / 2)
    return signal + noise


gammas_db = np.arange(10, 46, 5.0)
print(f"FM bandwidth by Carson's rule: {carson_bandwidth(beta * W, W) / 1e3:.0f} kHz for a {2 * W / 1e3:.0f} kHz AM channel")
am_measured, fm_measured = [], []
for g in 10 ** (gammas_db / 10):
    am_rx = receive(am_signal(message, t, 0.0) + 0j, g)
    am_measured.append(output_snr(sosfiltfilt(lowpass, np.abs(sosfiltfilt(if_am, am_rx))), message))
    phase = 2 * np.pi * beta * W * np.cumsum(message) / fs  # peak deviation beta * W
    fm_rx = receive(np.exp(1j * phase), g)
    frequency = np.diff(np.unwrap(np.angle(sosfiltfilt(if_fm, fm_rx))), prepend=0.0) * fs / (2 * np.pi)
    fm_measured.append(output_snr(sosfiltfilt(lowpass, frequency), message))
am_measured, fm_measured = 10 * np.log10(am_measured), 10 * np.log10(fm_measured)
am_theory = 10 * np.log10(am_output_snr(10 ** (gammas_db / 10)))
fm_theory = 10 * np.log10(fm_output_snr(10 ** (gammas_db / 10), beta))
threshold_db = 10 * np.log10(fm_threshold_snr(beta))
for g, a, f, ft in zip(gammas_db, am_measured, fm_measured, fm_theory):
    print(f"gamma {g:4.0f} dB: AM {a:5.1f} dB, FM {f:5.1f} dB (theory {ft:5.1f})")
above = gammas_db > threshold_db + 3
assert np.allclose(am_measured, am_theory, atol=1.0)
assert np.allclose(fm_measured[above], fm_theory[above], atol=1.5)
assert fm_measured[0] < fm_theory[0] - 5  # below threshold the FM advantage collapses

fig, ax = plt.subplots(figsize=(6.5, 4.2))
ax.plot(gammas_db, gammas_db, "k:", label=r"baseband, DSB-SC, SSB: $\gamma$")
ax.plot(gammas_db, am_theory, "C0", label=r"AM, $\mu = 1$: $\gamma/3$")
ax.plot(gammas_db, am_measured, "C0o")
ax.plot(gammas_db, fm_theory, "C1", label=rf"FM, $\beta$ = {beta:.0f}: $3\beta^2\gamma/2$")
ax.plot(gammas_db, fm_measured, "C1s")
ax.axvline(threshold_db, color="C3", ls="--", label="FM threshold (CNR 10 dB)")
ax.set(xlabel=r"reference SNR $\gamma$ [dB]", ylabel="output SNR [dB]")
ax.legend(fontsize=8)
fig.tight_layout()

plt.show()
plot 04 crosby fm noise
FM bandwidth by Carson's rule: 60 kHz for a 10 kHz AM channel
gamma   10 dB: AM   5.2 dB, FM  -4.3 dB (theory  25.7)
gamma   15 dB: AM  10.8 dB, FM   6.5 dB (theory  30.7)
gamma   20 dB: AM  15.8 dB, FM  23.0 dB (theory  35.7)
gamma   25 dB: AM  20.9 dB, FM  41.3 dB (theory  40.7)
gamma   30 dB: AM  26.0 dB, FM  46.3 dB (theory  45.7)
gamma   35 dB: AM  30.9 dB, FM  51.3 dB (theory  50.7)
gamma   40 dB: AM  36.0 dB, FM  56.5 dB (theory  55.7)
gamma   45 dB: AM  41.1 dB, FM  61.6 dB (theory  60.7)

Total running time of the script: (0 minutes 0.287 seconds)

Gallery generated by Sphinx-Gallery