LIGO tutorial: find an inspiral#

https://gwosc.org/tutorial06/

Read in the data and template#

import numpy as np
import matplotlib.pyplot as plt
import scipy.signal as sig
import h5py
import matplotlib.mlab as mlab
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[1], line 4
      2 import matplotlib.pyplot as plt
      3 import scipy.signal as sig
----> 4 import h5py
      5 import matplotlib.mlab as mlab

ModuleNotFoundError: No module named 'h5py'
fs = 4096
dataFile = h5py.File('data_w_signal.hdf5', 'r')
data = dataFile['strain/Strain'][...]
dataFile.close()
time = np.arange(0, 16, 1./fs)

# -- Read the template file (1 second, sampled at 4096 Hz)
templateFile = h5py.File('template.hdf5', 'r')
template = templateFile['strain/Strain'][...]
temp_time = np.arange(0, template.size / (1.0*fs), 1./fs)
templateFile.close()
plt.figure()
plt.plot(time,data)
plt.xlabel('Time (s)')
plt.ylabel('Strain')

plt.figure()
plt.plot(temp_time, template)
plt.xlabel('Time (s)')
plt.ylabel('Strain')
plt.title('Template')
Text(0.5, 1.0, 'Template')
_images/2a6667fb2e9eb6c09230b9e39a66c53bcd71adcb6268180a9f2ff033027b2f3e.png _images/cc72cd4326bf0e4d17404cea31211b8cafcb5794e3a77cf18ed089922e4ef354.png
data1 = np.copy(data[len(data)//2:])

N = len(data1)

# Try to understand the psd calculations they are doing
fft_d = np.fft.fft(data1)
psd_d = 2 * np.conjugate(fft_d) * fft_d  / (N*fs)
f_d = np.fft.fftfreq(len(data1), d = 1./fs)
plt.loglog(f_d[f_d>0], psd_d[f_d>0])

N1 = N//10
for i in range(10):
    fft_d = np.fft.fft(data1[i*N1:(i+1)*N1])
    if i == 0:
        psd_d = 2 * np.conjugate(fft_d) * fft_d  / (N1*fs) / 10
    else:
        psd_d += 2 * np.conjugate(fft_d) * fft_d  / (N1*fs) / 10

f_d = np.fft.fftfreq(N//10, d = 1./fs)
plt.loglog(f_d[f_d>0], psd_d[f_d>0])

Pxx, freqs = mlab.psd(data, Fs=fs, NFFT=len(data)//10)
plt.loglog(freqs, Pxx, label='matplotlib psd', alpha=0.5)

Pxx, freqs = mlab.psd(data, Fs=fs, NFFT=2*fs)
plt.loglog(freqs, Pxx, label='matplotlib psd', alpha=0.5)

plt.legend()
/opt/homebrew/lib/python3.11/site-packages/matplotlib/cbook.py:1699: ComplexWarning: Casting complex values to real discards the imaginary part
  return math.isfinite(val)
/opt/homebrew/lib/python3.11/site-packages/matplotlib/cbook.py:1345: ComplexWarning: Casting complex values to real discards the imaginary part
  return np.asarray(x, float)
<matplotlib.legend.Legend at 0x136eb35d0>
_images/df8a7191203c2eeb80df431368e08117e3455fc1ca49195992e4c6b3c63670bf.png

Bandpass filter#

def asd(fft):
    return np.sqrt(2* np.conjugate(fft) * fft / (fs*len(fft)))

# Template ASD
fft_t = np.fft.fft(template)
asd_t = asd(fft_t)
f_t = np.fft.fftfreq(len(template), d = 1./fs)
plt.plot(f_t[f_t>0], asd_t[f_t>0])
plt.xscale('log')
plt.yscale('log')

# Plot the -7/6 slope for the inspiral and 210 Hz location of the bump
f = np.linspace(50,300,100)
plt.plot(f, 1e-18*f**(-7/6),":")
plt.plot((210,210),(1e-22,3e-21), "--")
plt.show()

# Data ASD
fft_d = np.fft.fft(data)
asd_d = asd(fft_d)
f_d = np.fft.fftfreq(len(data), d = 1./fs)
plt.clf()
plt.plot(f_d[f_d>0], asd_d[f_d>0])

# with Blackman window
window = np.blackman(len(data))
data1 = data * window
fft_d1 = np.fft.fft(data1)
asd_d1 = asd(fft_d1)
f_d1 = np.fft.fftfreq(len(data1), d = 1./fs)
plt.plot(f_d1[f_d1>0], asd_d1[f_d1>0])

# ASD after applying a low pass filter between 80 and 250 Hz
(B,A) = sig.butter(4, [80/(fs/2.0), 250/(fs/2.0)], btype='pass')
data_pass = sig.lfilter(B, A, data1)
fft_d2 = np.fft.fft(data_pass)
asd_d2 = asd(fft_d2)
f_d2 = np.fft.fftfreq(len(data_pass), d = 1./fs)
plt.plot(f_d2[f_d2>0], asd_d2[f_d2>0])

plt.xscale('log')
plt.yscale('log')
plt.ylim((1e-27, 1e-18))
plt.show()

# time-series after filter
plt.plot(time, data_pass)
plt.show()
_images/f2ee48d4af37ea952050c8354ea7b013285ca8ca3253d7248b93ffda1e089336.png _images/94adc7dc97d5b15ff37a8eb5559a0c38e60ec322880962738babdc0395961c14.png _images/c904523620be02928e646c6f839527d8d52726af454a858f99fe2101681819c2.png

Time domain cross-correlation#

correlated_raw = np.correlate(data, template, 'valid')
correlated_passed = np.correlate(data_pass, template, 'valid')
plt.figure()
plt.plot(np.arange(0, (correlated_raw.size*1.)/fs, 1.0/fs),correlated_raw)
plt.plot(np.arange(0, (correlated_passed.size*1.)/fs, 1.0/fs),correlated_passed)
#plt.xlim((4,6))
plt.show()
_images/6b2256415c533ed0f5741107800e881ad9e9c66341b20a53f2da95e529a589bc.png

Matched filter#

data_fft=np.fft.fft(data)

zero_pad = np.zeros(data.size - template.size)
template_padded = np.append(template, zero_pad)
template_fft = np.fft.fft(template_padded)

# -- Calculate the PSD of the data
power_data_all, freq_psd_all = mlab.psd(data, Fs=fs, NFFT=fs)
power_data, freq_psd = mlab.psd(data[12*fs:], Fs=fs, NFFT=fs)

plt.clf()
plt.plot(freq_psd_all, power_data_all)
plt.plot(freq_psd, power_data)
plt.xscale('log')
plt.yscale('log')
plt.show()

# -- Interpolate to get the PSD values at the needed frequencies
datafreq = np.fft.fftfreq(data.size)*fs
power_vec = np.interp(datafreq, freq_psd, power_data)

print(np.fft.fftshift(datafreq))
print(freq_psd)
print(power_vec/max(power_vec))

# -- Calculate the matched filter output
optimal = data_fft * template_fft.conjugate() / power_vec
optimal_time = 2*np.fft.ifft(optimal)

# -- Normalize the matched filter output
df = np.abs(datafreq[1] - datafreq[0])
sigmasq = 2*(template_fft * template_fft.conjugate() / power_vec).sum() * df
sigma = np.sqrt(np.abs(sigmasq))
SNR = abs(optimal_time) / (sigma)

# -- Plot the result
plt.clf()
plt.plot(time, SNR)
plt.show()
_images/8d7be8a8c2bb8cfe517d203e0789745218fbbf03f124e4f1a523a3869182ed82.png
[-2048.     -2047.9375 -2047.875  ...  2047.8125  2047.875   2047.9375]
[0.000e+00 1.000e+00 2.000e+00 ... 2.046e+03 2.047e+03 2.048e+03]
[0.00023865 0.00035458 0.00047051 ... 0.00023865 0.00023865 0.00023865]
_images/f7856a43203af9b4f69382713dfd905c8122f5dbbb1b8030cde4540699c8a089.png
def psd(data, f_d):
    fft = np.fft.fft(data[12*fs:])
    psd = 2 * np.conjugate(fft) * fft / (fs * len(fft))
    freq = np.fft.fftfreq(len(data[12*fs:]), d = 1/fs)
    return np.interp(f_d, freq, psd)

fft_d = np.fft.fft(data)
fft_t = np.fft.fft(template, len(data))
f_d = np.fft.fftfreq(len(data), d = 1/fs)
psd_d = psd(data, f_d)

# -- Calculate the matched filter output
optimal = 2 * fft_d * fft_t.conjugate() / psd_d
optimal_time = np.fft.ifft(optimal)

# -- Normalize the matched filter output
df = np.abs(f_d[1] - f_d[0])
sigmasq = 2 * -(fft_t * fft_t.conjugate() / psd_d).sum() * df
sigma = np.sqrt(np.abs(sigmasq))
SNR = abs(optimal_time) / (sigma)

# -- Plot the result
plt.figure()
plt.plot(time, SNR)
plt.show()
_images/93aff8454aacb862bd6c2e1c9e008c4f7842702a7c4bf419a8fe71c26a9be0c1.png