Note
Go to the end to download the full example code or to run this example in your browser via JupyterLite.
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()

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)