gstlal 1.13.0
Loading...
Searching...
No Matches
kernels.py
1# Copyright (C) 2010--2013 Kipp Cannon, Chad Hanna, Leo Singer
2#
3# This program is free software; you can redistribute it and/or modify it
4# under the terms of the GNU General Public License as published by the
5# Free Software Foundation; either version 2 of the License, or (at your
6# option) any later version.
7#
8# This program is distributed in the hope that it will be useful, but
9# WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11# Public License for more details.
12#
13# You should have received a copy of the GNU General Public License along
14# with this program; if not, write to the Free Software Foundation, Inc.,
15# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16
17
18import math
19from typing import Optional, Tuple
20
21import numpy
22try:
23 from pyfftw.interfaces import scipy_fftpack as fftpack
24except ImportError:
25 from scipy import fftpack
26
27import lal
28from lal import LIGOTimeGPS
29
30
31class PSDFirKernel(object):
32 def __init__(self):
33 self.revplan = None
34 self.fwdplan = None
35 self.target_phase = None
36 self.target_phase_mask = None
37
39 self,
40 psd: lal.REAL8FrequencySeries,
41 f_low: float = 10.0,
42 m1: float = 1.4,
43 m2: float = 1.4
44 ) -> None:
45 """
46 Compute the phase response of zero-latency whitening filter
47 given a reference PSD.
48
49 """
50 kernel, latency, sample_rate = self.psd_to_linear_phase_whitening_fir_kernel(psd)
51 kernel, phase = self.linear_phase_fir_kernel_to_minimum_phase_whitening_fir_kernel(kernel, sample_rate)
52
53 # get merger model for SNR = 1.
54 f_psd = psd.f0 + numpy.arange(len(psd.data.data)) * psd.deltaF
55 horizon_distance = HorizonDistance(f_low, f_psd[-1], psd.deltaF, m1, m2)
56 f_model, model= horizon_distance(psd, 1.)[1]
57
58 # find the range of frequency bins covered by the merger
59 # model
60 kmin, kmax = f_psd.searchsorted(f_model[0]), f_psd.searchsorted(f_model[-1]) + 1
61
62 # compute SNR=1 model's (d SNR^2 / df) spectral density
63 unit_snr2_density = numpy.zeros_like(phase)
64 unit_snr2_density[kmin:kmax] = model / psd.data.data[kmin:kmax]
65
66 # integrate across each frequency bin, converting to
67 # snr^2/bin. NOTE: this step is here for the record, but
68 # is commented out because it has no effect on the result
69 # given the renormalization that occurs next.
70 #unit_snr2_density *= psd.deltaF
71
72 # take 16th root, then normalize so max=1. why? I don't
73 # know, just feels good, on the whole.
74 unit_snr2_density = unit_snr2_density**(1./16)
75 unit_snr2_density /= unit_snr2_density.max()
76
77 # record phase vector and SNR^2 density vector
78 self.target_phase = phase
79 self.target_phase_mask = unit_snr2_density
80
82 self,
83 psd: lal.REAL8FrequencySeries,
84 invert: Optional[bool] = True,
85 nyquist: Optional[float] = None
86 ) -> Tuple[numpy.ndarray, int, int]:
87 """
88 Compute an acausal finite impulse-response filter kernel
89 from a power spectral density conforming to the LAL
90 normalization convention, such that if colored Gaussian
91 random noise with the given PSD is fed into an FIR filter
92 using the kernel the filter's output will be zero-mean
93 unit-variance Gaussian random noise. The PSD must be
94 provided as a lal.REAL8FrequencySeries object.
95
96 The phase response of this filter is 0, just like whitening
97 done in the frequency domain.
98
99 Args:
100 psd:
101 lal.REAL8FrequencySeries, the reference PSD
102 invert:
103 bool, default true, whether to invert the kernel
104 nyquist:
105 float, disabled by default, whether to change
106 the Nyquist frequency.
107
108 Returns:
109 Tuple[numpy.ndarray, int, int], the kernel, latency,
110 sample rate pair. The kernel is a numpy array containing
111 the filter kernel, the latency is the filter latency in
112 samples and the sample rate is in Hz. The kernel and
113 latency can be used, for example, with gstreamer's stock
114 audiofirfilter element.
115
116 """
117 #
118 # this could be relaxed with some work
119 #
120
121 assert psd.f0 == 0.0
122
123 #
124 # extract the PSD bins and determine sample rate for kernel
125 #
126
127 data = psd.data.data / 2
128 sample_rate = 2 * int(round(psd.f0 + (len(data) - 1) * psd.deltaF))
129
130 #
131 # remove LAL normalization
132 #
133
134 data *= sample_rate
135
136 #
137 # change Nyquist frequency if requested. round to nearest
138 # available bin
139 #
140
141 if nyquist is not None:
142 i = int(round((nyquist - psd.f0) / psd.deltaF))
143 assert i < len(data)
144 data = data[:i + 1]
145 sample_rate = 2 * int(round(psd.f0 + (len(data) - 1) * psd.deltaF))
146
147 #
148 # compute the FIR kernel. it always has an odd number of
149 # samples and no DC offset.
150 #
151
152 data[0] = data[-1] = 0.0
153 if invert:
154 data_nonzeros = (data != 0.)
155 data[data_nonzeros] = 1./data[data_nonzeros]
156 # repack data: data[0], data[1], 0, data[2], 0, ....
157 tmp = numpy.zeros((2 * len(data) - 1,), dtype = data.dtype)
158 tmp[len(data)-1:] = data
159 #tmp[:len(data)] = data
160 data = tmp
161
162 kernel_fseries = lal.CreateCOMPLEX16FrequencySeries(
163 name = "double sided psd",
164 epoch = LIGOTimeGPS(0),
165 f0 = 0.0,
166 deltaF = psd.deltaF,
167 length = len(data),
168 sampleUnits = lal.Unit("strain s")
169 )
170
171 kernel_tseries = lal.CreateCOMPLEX16TimeSeries(
172 name = "timeseries of whitening kernel",
173 epoch = LIGOTimeGPS(0.),
174 f0 = 0.,
175 deltaT = 1.0 / sample_rate,
176 length = len(data),
177 sampleUnits = lal.Unit("strain")
178 )
179
180 # FIXME check for change in length
181 if self.revplan is None:
182 self.revplan = lal.CreateReverseCOMPLEX16FFTPlan(len(data), 1)
183
184 kernel_fseries.data.data = numpy.sqrt(data) + 0.j
185 lal.COMPLEX16FreqTimeFFT(kernel_tseries, kernel_fseries, self.revplan)
186 kernel = kernel_tseries.data.data.real
187 kernel = numpy.roll(kernel, (len(data) - 1) // 2) / sample_rate * 2
188
189 #
190 # apply a Tukey window whose flat bit is 50% of the kernel.
191 # preserve the FIR kernel's square magnitude
192 #
193
194 norm_before = numpy.dot(kernel, kernel)
195 kernel *= lal.CreateTukeyREAL8Window(len(data), .5).data.data
196 kernel *= math.sqrt(norm_before / numpy.dot(kernel, kernel))
197
198 #
199 # the kernel's latency
200 #
201
202 latency = (len(data) - 1) // 2
203
204 #
205 # done
206 #
207
208 return kernel, latency, sample_rate
209
210
212 self,
213 linear_phase_kernel: numpy.ndarray,
214 sample_rate: int
215 ) -> Tuple[numpy.ndarray, numpy.ndarray]:
216 """
217 Compute the minimum-phase response filter (zero latency)
218 associated with a linear-phase response filter (latency
219 equal to half the filter length).
220
221 From "Design of Optimal Minimum-Phase Digital FIR Filters
222 Using Discrete Hilbert Transforms", IEEE Trans. Signal
223 Processing, vol. 48, pp. 1491-1495, May 2000.
224
225 Args:
226 linear_phase_kernel:
227 numpy.ndarray, the kernel to compute the minimum-phase kernel with
228 sample_rate:
229 int, the sample rate
230
231 Returns:
232 Tuple[numpy.ndarray. numpy.ndarray], the kernel and the phase response.
233 The kernel is a numpy array containing the filter kernel. The kernel
234 can be used, for example, with gstreamer's stock audiofirfilter element.
235
236 """
237 #
238 # compute abs of FFT of kernel
239 #
240
241 # FIXME check for change in length
242 if self.fwdplan is None:
243 self.fwdplan = lal.CreateForwardCOMPLEX16FFTPlan(len(linear_phase_kernel), 1)
244 if self.revplan is None:
245 self.revplan = lal.CreateReverseCOMPLEX16FFTPlan(len(linear_phase_kernel), 1)
246
247 deltaT = 1. / sample_rate
248 deltaF = 1. / (len(linear_phase_kernel) * deltaT)
249 working_length = len(linear_phase_kernel)
250
251 kernel_tseries = lal.CreateCOMPLEX16TimeSeries(
252 name = "timeseries of whitening kernel",
253 epoch = LIGOTimeGPS(0.),
254 f0 = 0.,
255 deltaT = deltaT,
256 length = working_length,
257 sampleUnits = lal.Unit("strain")
258 )
259 kernel_tseries.data.data = linear_phase_kernel
260
261 absX = lal.CreateCOMPLEX16FrequencySeries(
262 name = "absX",
263 epoch = LIGOTimeGPS(0),
264 f0 = 0.0,
265 deltaF = deltaF,
266 length = working_length,
267 sampleUnits = lal.Unit("strain s")
268 )
269
270 logabsX = lal.CreateCOMPLEX16FrequencySeries(
271 name = "absX",
272 epoch = LIGOTimeGPS(0),
273 f0 = 0.0,
274 deltaF = deltaF,
275 length = working_length,
276 sampleUnits = lal.Unit("strain s")
277 )
278
279 cepstrum = lal.CreateCOMPLEX16TimeSeries(
280 name = "cepstrum",
281 epoch = LIGOTimeGPS(0.),
282 f0 = 0.,
283 deltaT = deltaT,
284 length = working_length,
285 sampleUnits = lal.Unit("strain")
286 )
287
288 theta = lal.CreateCOMPLEX16FrequencySeries(
289 name = "theta",
290 epoch = LIGOTimeGPS(0),
291 f0 = 0.0,
292 deltaF = deltaF,
293 length = working_length,
294 sampleUnits = lal.Unit("strain s")
295 )
296
297 min_phase_kernel = lal.CreateCOMPLEX16TimeSeries(
298 name = "min phase kernel",
299 epoch = LIGOTimeGPS(0.),
300 f0 = 0.,
301 deltaT = deltaT,
302 length = working_length,
303 sampleUnits = lal.Unit("strain")
304 )
305
306 lal.COMPLEX16TimeFreqFFT(absX, kernel_tseries, self.fwdplan)
307 absX.data.data[:] = abs(absX.data.data)
308
309 #
310 # compute the cepstrum of the kernel (i.e., the iFFT of the
311 # log of the abs of the FFT of the kernel)
312 #
313
314 logabsX.data.data[:] = numpy.log(absX.data.data)
315 lal.COMPLEX16FreqTimeFFT(cepstrum, logabsX, self.revplan)
316
317 #
318 # multiply cepstrum by sgn
319 #
320
321 cepstrum.data.data[0] = 0.
322 cepstrum.data.data[working_length // 2] = 0.
323 cepstrum.data.data[working_length // 2 + 1:] = -cepstrum.data.data[working_length // 2 + 1:]
324
325 #
326 # compute theta
327 #
328
329 lal.COMPLEX16TimeFreqFFT(theta, cepstrum, self.fwdplan)
330
331 #
332 # compute the gain and phase of the zero-phase
333 # approximation relative to the original linear-phase
334 # filter
335 #
336
337 theta_data = theta.data.data[working_length // 2:]
338 #gain = numpy.exp(theta_data.real)
339 phase = -theta_data.imag
340
341 #
342 # apply optional masked phase adjustment
343 #
344
345 if self.target_phase is not None:
346 # compute phase adjustment for +ve frequencies
347 phase_adjustment = (self.target_phase - phase) * self.target_phase_mask
348
349 # combine with phase adjustment for -ve frequencies
350 phase_adjustment = numpy.concatenate((phase_adjustment[1:][-1::-1].conj(), phase_adjustment))
351
352 # apply adjustment. phase adjustment is what we
353 # wish to add to the phase. theta's imaginary
354 # component contains the negative of the phase, so
355 # we need to add -phase to theta's imaginary
356 # component
357 theta.data.data += -1.j * phase_adjustment
358
359 # report adjusted phase
360 #phase = -theta.data.data[working_length // 2:].imag
361
362 #
363 # compute minimum phase kernel
364 #
365
366 absX.data.data *= numpy.exp(theta.data.data)
367 lal.COMPLEX16FreqTimeFFT(min_phase_kernel, absX, self.revplan)
368
369 kernel = min_phase_kernel.data.data.real
370
371 #
372 # this kernel needs to be reversed to follow conventions
373 # used with the audiofirfilter and lal_firbank elements
374 #
375
376 kernel = kernel[-1::-1]
377
378 #
379 # done
380 #
381
382 return kernel, phase
383
384
385def fir_whitener_kernel(
386 length: int,
387 duration: float,
388 sample_rate: int,
389 psd: lal.REAL8FrequencySeries
390) -> lal.COMPLEX16FrequencySeries:
391 """Create an FIR whitener kernel.
392
393 """
394 assert psd
395 #
396 # Add another COMPLEX16TimeSeries and COMPLEX16FrequencySeries for kernel's FFT (Leo)
397 #
398
399 # Add another FFT plan for kernel FFT (Leo)
400 fwdplan_kernel = lal.CreateForwardCOMPLEX16FFTPlan(length, 1)
401 kernel_tseries = lal.CreateCOMPLEX16TimeSeries(
402 name = "timeseries of whitening kernel",
403 epoch = LIGOTimeGPS(0.),
404 f0 = 0.,
405 deltaT = 1.0 / sample_rate,
406 length = length,
407 sampleUnits = lal.Unit("strain")
408 )
409 kernel_fseries = lal.CreateCOMPLEX16FrequencySeries(
410 name = "freqseries of whitening kernel",
411 epoch = LIGOTimeGPS(0),
412 f0 = 0.0,
413 deltaF = 1.0 / duration,
414 length = length,
415 sampleUnits = lal.Unit("strain s")
416 )
417
418 #
419 # Obtain a kernel of zero-latency whitening filter and
420 # adjust its length (Leo)
421 #
422
423 psd_fir_kernel = PSDFirKernel()
424 (kernel, latency, fir_rate) = psd_fir_kernel.psd_to_linear_phase_whitening_fir_kernel(psd, nyquist = sample_rate / 2.0)
425 (kernel, theta) = psd_fir_kernel.linear_phase_fir_kernel_to_minimum_phase_whitening_fir_kernel(kernel, fir_rate)
426 kernel = kernel[-1::-1]
427 # FIXME this is off by one sample, but shouldn't be. Look at the miminum phase function
428 # assert len(kernel) == length
429 if len(kernel) < length:
430 kernel = numpy.append(kernel, numpy.zeros(length - len(kernel)))
431 else:
432 kernel = kernel[:length]
433
434 kernel_tseries.data.data = kernel
435
436 #
437 # FFT of the kernel
438 #
439
440 lal.COMPLEX16TimeFreqFFT(kernel_fseries, kernel_tseries, fwdplan_kernel) #FIXME
441
442 return kernel_fseries
443
444
445def one_second_highpass_kernel(rate: int, cutoff: int = 12) -> numpy.ndarray:
446 """Create a one second high-pass kernel.
447
448 Args:
449 rate (Hz):
450 int, the sampling rate
451 cutoff (Hz):
452 int, the high-pass cutoff
453
454 Returns:
455 numpy.ndarray, the high-passed kernel
456
457 """
458 highpass_filter_fd = numpy.ones(rate, dtype=complex)
459 highpass_filter_fd[:int(cutoff)] = 0.
460 highpass_filter_fd[-int(cutoff):] = 0.
461 highpass_filter_fd[(rate // 2 - 1):(rate // 2 + 1)] = 0.
462 highpass_filter_td = fftpack.ifft(highpass_filter_fd)
463 highpass_filter_td = numpy.roll(highpass_filter_td.real, rate // 2)
464 highpass_filter_kernel = numpy.zeros(len(highpass_filter_td) + 1)
465 highpass_filter_kernel[:-1] = highpass_filter_td[:]
466 x = numpy.arange(len(highpass_filter_kernel))
467 mid = len(x) / 2.
468 highpass_filter_kernel *= 1. - (x - mid)**2 / mid**2
469 return highpass_filter_kernel
470
471
472def fixed_duration_bandpass_kernel(
473 rate: int,
474 flow: float = 0,
475 fhigh: float = numpy.inf,
476 duration: float = 1.0
477) -> numpy.ndarray:
478 """Create a fixed-duration band-pass kernel.
479
480 Args:
481 rate (Hz):
482 int, the sampling rate
483 flow (Hz):
484 float, default 0, the low frequency of the pass band
485 fhigh (Hz):
486 float, default +inf, the high frequency of the pass band
487 duration (s):
488 float, default 1.0, the duration of the kernel
489
490 Returns:
491 numpy.ndarray, the band-passed kernel
492
493 """
494 deltaF = 1. / duration
495 nsamps = int(rate * duration) + 1
496 f = numpy.arange(nsamps) * deltaF - rate / 2.
497 filt = numpy.ones(len(f))
498 ix1 = numpy.logical_and(f <= -flow, f >= -fhigh)
499 ix2 = numpy.logical_and(f >= flow, f <= fhigh)
500 filt[numpy.logical_not(numpy.logical_or(ix1, ix2))] = 0.
501 filt = numpy.real(fftpack.ifft(fftpack.ifftshift(filt))) / nsamps
502 window = numpy.sinc(2 * f / rate)
503 out = numpy.roll(filt, nsamps // 2) * window
504 out /= (out**2).sum()**.5
505 return out
Tuple[numpy.ndarray, numpy.ndarray] linear_phase_fir_kernel_to_minimum_phase_whitening_fir_kernel(self, numpy.ndarray linear_phase_kernel, int sample_rate)
Definition kernels.py:215
None set_phase(self, lal.REAL8FrequencySeries psd, float f_low=10.0, float m1=1.4, float m2=1.4)
Definition kernels.py:44
Tuple[numpy.ndarray, int, int] psd_to_linear_phase_whitening_fir_kernel(self, lal.REAL8FrequencySeries psd, Optional[bool] invert=True, Optional[float] nyquist=None)
Definition kernels.py:86