Note

This page was generated from a Jupyter Notebook. Download the notebook (.ipynb)

[1]:
# Skipped in CI: Colab/bootstrap dependency install cell.

Lock-in Detection: Recovering Weak AM/FM Structure๏ƒ

Open In Colab

Lock-in detection is the right tool when the physically interesting information lives as a slow modulation on a much faster carrier. The goal is not to measure the carrier itself, but to move the modulation into baseband where low-frequency trend tools can recover it.

This public version is distilled from the legacy heterodyne notebook and focuses on the essential AM/FM recovery workflow.

[2]:
import matplotlib.pyplot as plt
import numpy as np
from scipy import signal

from gwexpy import TimeSeries

fs = 1024.0
duration = 40.0
t = np.arange(0, duration, 1 / fs)
np.random.seed(42)

# The carrier is the strong narrowband tone that transports the information we actually care about.
f_carrier = 100.0

# Slow AM and PM terms encode the physical drift to be recovered after demodulation.
amp_mod = 1.0 + 0.5 * np.sin(2 * np.pi * 0.2 * t)
phase_mod = 2.0 * np.sin(2 * np.pi * 0.05 * t)

clean_signal = amp_mod * np.cos(2 * np.pi * f_carrier * t + phase_mod)
noise = np.random.normal(0, 0.8, len(t))
ts = TimeSeries(clean_signal + noise, sample_rate=fs, name="Modulated Signal", unit="V")

1. Recover amplitude and phase with lock_in๏ƒ

The reference frequency tells the algorithm which coherent part of the waveform to keep. Broadband noise averages away, while the slow AM/FM structure survives.

[3]:
# stride sets the time resolution of the recovered baseband trend: shorter stride follows faster modulation but averages less noise.
amp, phase = ts.lock_in(f0=f_carrier, stride=0.1, output="amp_phase", deg=False)

fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8), sharex=True)
ax1.plot(amp, label="Extracted Amplitude", color="red")
ax1.plot(t, amp_mod, label="True Amplitude", color="black", ls="--", alpha=0.7)
ax1.set_ylabel("Amplitude [V]")
ax1.legend()

ax2.plot(phase, label="Extracted Phase", color="blue")
ax2.plot(t, phase_mod, label="True Phase", color="black", ls="--", alpha=0.7)
ax2.set_ylabel("Phase [rad]")
ax2.legend()
plt.xlabel("Time [s]")
plt.show()

../../../../_images/web_en_user_guide_tutorials_case_lockin_detection_5_0.png

2. Manual demodulation shows the same physics๏ƒ

Mixing with cosine/sine creates a DC term plus an image at 2*f_c. The low-pass filter rejects that high-frequency image so only the slow envelope and phase drift remain.

[4]:
ref_cos = np.cos(2 * np.pi * f_carrier * t)
ref_sin = np.sin(2 * np.pi * f_carrier * t)

# Multiply by the reference to shift the carrier to baseband and create a mirrored image at 2*f_c.
mixed_i = (ts * ref_cos) * 2.0
mixed_q = (ts * (-ref_sin)) * 2.0

# Low-pass after mixing removes the 2*f_c image term so the slow modulation remains.
b, a = signal.butter(4, 2.0, fs=fs, btype="low")
i_filtered = TimeSeries(signal.filtfilt(b, a, mixed_i.value), sample_rate=fs)
q_filtered = TimeSeries(signal.filtfilt(b, a, mixed_q.value), sample_rate=fs)

amp_manual = np.sqrt(i_filtered**2 + q_filtered**2)
phase_manual = np.arctan2(q_filtered.value, i_filtered.value)

plt.figure(figsize=(12, 4))
plt.plot(amp_manual, label="Manual Amplitude")
plt.plot(amp, label="lock_in()", alpha=0.7)
plt.legend()
plt.show()

../../../../_images/web_en_user_guide_tutorials_case_lockin_detection_7_0.png

3. Demodulation moves the information into baseband๏ƒ

After demodulation, the interesting structure is centered near zero frequency. That is why lock-in detection turns a high-frequency carrier problem into a low-frequency trend-analysis problem.

[5]:
asd_raw = ts.asd(fftlength=4)
complex_signal = ts.lock_in(f0=f_carrier, stride=1 / fs, output="complex")
asd_demod = complex_signal.asd(fftlength=4)

plt.figure(figsize=(10, 6))
plt.loglog(asd_raw.frequencies, asd_raw.value, label="Original Signal")
plt.loglog(asd_demod.frequencies, asd_demod.value, label="Demodulated Signal")
plt.axvline(f_carrier, color="red", ls="--", label="Carrier")
plt.legend()
plt.grid(True, which="both")
plt.show()

../../../../_images/web_en_user_guide_tutorials_case_lockin_detection_9_0.png