Example detection of Venus echo¶

Thomas Telkamp, 30-03-2025, CC BY-SA 4.0

The data is licensed under a Creative Commons Attribution 4.0 International License (CC BY 4.0), author of the Dwingeloo files is Stichting Radiotelescoop Dwingeloo, and author of the Stockert files is Astropeiler Stockert e.V.

InĀ [1]:
import matplotlib as mpl
import matplotlib.dates as mdates
import matplotlib.pyplot as plt
from matplotlib.dates import num2date
from matplotlib.ticker import FuncFormatter

import numpy as np
import pandas as pd

import sigmf

import astropy.units as u
from astropy.time import Time
import astropy.constants
InĀ [2]:
def doppler(v, f):
    return (1 - v / astropy.constants.c.value) * f
InĀ [3]:
# IQ files

# Note: the current released of the sigmf module (1.2.8) fails to open these SigMF files, use 
# an older version as workaround (e.g. with "pip install sigmf==1.2.6") until this is fixed

path = "/home/www/venus/"

# Dwingeloo, cw1
# pol1 = path+"/5ksps/dwingeloo_eve_extract_2025_03_22_12_05_39_1299.500MHz_5ksps_ci16_le.sigmf-meta"

# Stockert, cw2
pol1 = (
    path
    + "/5ksps/stockert_eve_extract_2025_03_22_12_15_39_1299.500MHz_5ksps_ci16_le.chan0.sigmf-meta"
)
pol2 = (
    path
    + "/5ksps/stockert_eve_extract_2025_03_22_12_15_39_1299.500MHz_5ksps_ci16_le.chan1.sigmf-meta"
)

if "dwingeloo" in pol1:
    site = "Dwingeloo"
elif "stockert" in pol1:
    site = "Stockert"
else:
    site = ""

handle = sigmf.sigmffile.fromfile(pol1)
data = handle.read_samples()

if site == "Stockert":

    handle2 = sigmf.sigmffile.fromfile(pol2)

    data2 = handle2.read_samples()

    pow_1 = np.mean(np.abs(data))
    pow_2 = np.mean(np.abs(data2))

    data2 = data2 * (pow_1 / pow_2)

    # Combine to LHCP
    data = data + np.exp(1j * 260 * np.pi / 180) * data2

sample_rate = int(handle.get_global_info()[handle.SAMPLE_RATE_KEY])
center_freq = handle.get_capture_info(0)["core:frequency"]
start_time = handle.get_capture_info(0)["core:datetime"]

fs = sample_rate

print("Site:", site)
print("Sample rate:", sample_rate, "sps")
print("Center frequency:", center_freq, "Hz")
print("Start time:", start_time)
print("Duration:", len(data) / fs, "sec")
Site: Stockert
Sample rate: 5000 sps
Center frequency: 1299500000 Hz
Start time: 2025-03-22T12:15:39.000000
Duration: 280.0 sec
InĀ [4]:
if site == "Dwingeloo":
    doppler_file = path + "dwingeloo_venus_doppler.csv"
else:
    doppler_file = path + "stockert_venus_doppler.csv"

doppler_data = pd.read_csv(doppler_file, sep=",")

doppler_timestamps = Time(list(doppler_data["rx_time_utc"]))
freq_offset = np.array(list(doppler_data["freq_offset_hz"]))
doppler_rate = np.array(list(doppler_data["doppler_rate_hz_s"]))
InĀ [5]:
time_astropy = Time(start_time)
time_astropy
Out[5]:
<Time object: scale='utc' format='isot' value=2025-03-22T12:15:39.000>
InĀ [6]:
# find start_time in Doppler data
duration = int(len(data) / sample_rate)
start_index = np.argmin(np.abs(doppler_timestamps - time_astropy))
freq_offset = freq_offset[start_index : start_index + duration]
doppler_rate = doppler_rate[start_index : start_index + duration]
InĀ [7]:
time_range = time_astropy + np.arange(len(freq_offset)) * u.s

fig, ax = plt.subplots()
ax.plot(time_range.datetime, (freq_offset), "-")
ax.set_xlabel("Time (UTC)")
ax.set_ylabel("Doppler (Hz)")
ax.xaxis_date()
ax.xaxis.set_major_formatter(FuncFormatter(lambda x, pos: f"{num2date(x):%H:%M:%S}"))
ax.set_title("Venus Doppler");
No description has been provided for this image
InĀ [8]:
time_range = time_astropy + np.arange(len(doppler_rate)) * u.s

fig, ax = plt.subplots()
ax.plot(time_range.datetime, doppler_rate, "-")
ax.set_xlabel("Time (UTC)")
ax.set_ylabel("Doppler rate (Hz)")
ax.xaxis_date()
ax.xaxis.set_major_formatter(FuncFormatter(lambda x, pos: f"{num2date(x):%H:%M:%S}"))
ax.set_title("Venus Doppler rate");
No description has been provided for this image
InĀ [9]:
expected_freq_offset = freq_offset[0]
print(f"Expected Doppler offset: {expected_freq_offset:.3f} Hz")
Expected Doppler offset: 180.436 Hz
InĀ [10]:
# Compensate for Doppler and Doppler rate

dr = np.repeat(doppler_rate, len(data) / len(doppler_rate))
t_s = np.arange(len(data)) / fs
fdrift = -dr # Doppler rate
f0_Hz = -expected_freq_offset # Doppler
phi_Hz = (fdrift * t_s**2) / 2 + (f0_Hz * t_s)  # Instantaneous phase.
phi_rad = 2 * np.pi * phi_Hz  # Convert to radians.
data = data * np.exp(1j * phi_rad)
InĀ [11]:
freq = np.fft.fftshift(np.fft.fftfreq(sample_rate, d=1 / sample_rate))
InĀ [12]:
spectrum = np.fft.fftshift(np.fft.fft(data.reshape(-1, int(sample_rate))))
spectrum.shape
Out[12]:
(280, 5000)
InĀ [13]:
spec_sum = np.mean(np.abs(spectrum), axis=0)
InĀ [14]:
subset = range(int(len(spec_sum) / 2 - 200), int(len(spec_sum) / 2 + 200))
InĀ [15]:
plot_cw = spec_sum[subset] / np.median(spec_sum[subset])
plot_cw = plot_cw**2

stddev = np.std(plot_cw)
s4 = 4 * stddev
s_med = np.median(plot_cw)
s_max = np.max(plot_cw)
InĀ [16]:
mid_samp = plot_cw[int(len(plot_cw) / 2)]
detect_sigma = np.round((mid_samp - 1) / stddev, 2)
print(detect_sigma)
6.02
InĀ [17]:
fig, ax0 = plt.subplots(figsize=(12, 6))
ax0.plot(freq[subset], plot_cw, "-", label="signal")
ax0.set_xlim(-205, 205)
ax0.axvline(0, linestyle="--", color="grey", alpha=0.3)
ax0.axhline(s_med, linestyle="--", color="green", alpha=0.3)
ax0.axhline(1 + s4, linestyle="--", color="grey", alpha=0.5, label="4.0 $\sigma$")
ax0.set_xlabel("Frequency offset from predicted Doppler (Hz)")
ax0.set_ylabel("Power")
ax0.set_title(
    "Dwingeloo-" + site + " (" + str(start_time) + ", " + str(len(data) / fs) + " sec.)"
)
ax0.legend();
No description has been provided for this image
InĀ [18]:
overlap = 8
fft_size = int(sample_rate)
pfb_size = fft_size * overlap
# pfb_buffer = np.zeros(pfb_size)+0*1j
blocks = len(data) // (fft_size)
blocks
Out[18]:
280
InĀ [19]:
# Blackman-Harris

a0 = 0.35875
a1 = 0.48829
a2 = 0.14128
a3 = 0.01168

taps = np.zeros(pfb_size)
blackman_window = np.zeros(pfb_size)

i = np.arange(pfb_size)
j = i - (pfb_size) / 2
taps = np.sinc((overlap) * (j / (pfb_size)))
blackman_window = (
    a0
    - a1 * np.cos(2 * np.pi * i / (pfb_size - 1))
    + a2 * np.cos(4 * np.pi * i / (pfb_size - 1))
    + a3 * np.cos(6 * np.pi * i / (pfb_size - 1))
)
blackman_window = blackman_window * taps
InĀ [20]:
spectrum = np.zeros((blocks, fft_size), dtype=np.complex128)

for i in range(blocks):
    start_index = i - (overlap - 1)
    if start_index < 0:
        start_index = 0
    l = ((i + 1) - start_index) * fft_size
    pfb_buffer = np.zeros(pfb_size, dtype=np.complex128)
    pfb_buffer[-l:] = data[start_index * fft_size : (i + 1) * fft_size]
    fft_input = np.mean(((pfb_buffer * blackman_window).reshape(overlap, -1)), axis=0)
    spectrum[i, :] = np.fft.fftshift(np.fft.fft(fft_input))
InĀ [21]:
spec_sum = np.mean(np.abs(spectrum), axis=0)
InĀ [22]:
subset = range(int(len(spec_sum) / 2 - 200), int(len(spec_sum) / 2 + 200))
freq = np.fft.fftshift(np.fft.fftfreq(fft_size, d=1 / fft_size))
InĀ [23]:
plot_cw = spec_sum[subset] / np.median(spec_sum[subset])

plot_cw = plot_cw**2

plot_noise = plot_cw.copy()
plot_noise[int(len(plot_cw) / 2)] = np.nan

stddev = np.nanstd(plot_noise)
s4 = 4 * stddev
s_med = np.nanmedian(plot_noise)
s_max = plot_cw[int(len(plot_cw) / 2)]
print("Peak:", np.round(10*np.log10(s_max),2), "dB")
Peak: 1.41 dB
InĀ [24]:
fig, ax0 = plt.subplots(figsize=(10, 4))
ax0.plot(freq[subset], plot_cw, "-", label="signal")
ax0.set_xlim(-205, 205)
ax0.axvline(0, linestyle="--", color="grey", alpha=0.3)
ax0.axhline(s_med, linestyle="--", color="green", alpha=0.3)
ax0.axhline(1 + s4, linestyle="--", color="grey", alpha=0.5, label="4.0 $\sigma$")
ax0.set_xlabel("Frequency offset from predicted Doppler (Hz)")
ax0.set_ylabel("Normalized Power")
ax0.set_title(
    "Dwingeloo-" + site + " (" + str(start_time) + ", " + str(len(data) / fs) + " sec.)"
)
ax0.legend();
No description has been provided for this image
InĀ [25]:
mid_samp = plot_cw[int(len(plot_cw) / 2)]
detect_sigma = np.round((mid_samp - 1) / stddev, 2)
print("Detection:", detect_sigma, "sigma")
Detection: 6.37 sigma