gstlal  1.4.1
reference_psd.py
1 #
2 # Copyright (C) 2010--2013 Kipp Cannon, Chad Hanna, Leo Singer
3 #
4 # This program is free software; you can redistribute it and/or modify it
5 # under the terms of the GNU General Public License as published by the
6 # Free Software Foundation; either version 2 of the License, or (at your
7 # option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
12 # Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License along
15 # with this program; if not, write to the Free Software Foundation, Inc.,
16 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 
18 
19 #
20 # =============================================================================
21 #
22 # Preamble
23 #
24 # =============================================================================
25 #
26 
27 
28 import math
29 import numpy
30 import scipy
31 import os
32 try:
33  from pyfftw.interfaces import scipy_fftpack as fftpack
34 except ImportError:
35  from scipy import fftpack
36 from scipy import interpolate
37 import sys
38 import signal
39 import warnings
40 
41 
42 import gi
43 gi.require_version('Gst', '1.0')
44 from gi.repository import GObject
45 from gi.repository import Gst
46 GObject.threads_init()
47 Gst.init(None)
48 
49 
50 from glue.ligolw import utils as ligolw_utils
51 import lal
52 import lal.series
53 from lal import LIGOTimeGPS
54 import lalsimulation
55 
56 
57 from gstlal import datasource
58 from gstlal import pipeparts
59 from gstlal import pipeio
60 from gstlal import simplehandler
61 
62 
63 __doc__ = """
64 **Review Status**
65 
66 +-------------------------------------------------+------------------------------------------+------------+
67 | Names | Hash | Date |
68 +=================================================+==========================================+============+
69 | Florent, Sathya, Duncan Me., Jolien, Kipp, Chad | b3ef077fe87b597578000f140e4aa780f3a227aa | 2014-05-01 |
70 +-------------------------------------------------+------------------------------------------+------------+
71 
72 """
73 
74 
75 #
76 # =============================================================================
77 #
78 # PSD Measurement
79 #
80 # =============================================================================
81 #
82 
83 
84 #
85 # pipeline handler for PSD measurement
86 #
87 
88 
90  def __init__(self, *args, **kwargs):
91  self.psd = None
92  simplehandler.Handler.__init__(self, *args, **kwargs)
93 
94  def do_on_message(self, bus, message):
95  if message.type == Gst.MessageType.ELEMENT and message.get_structure().get_name() == "spectrum":
96  self.psd = pipeio.parse_spectrum_message(message)
97  return True
98  return False
99 
100 
101 #
102 # measure_psd()
103 #
104 
105 
106 def measure_psd(gw_data_source_info, instrument, rate, psd_fft_length = 8, verbose = False):
107  """
108 **Gstreamer graph**
109 
110 .. graphviz::
111 
112  digraph G {
113  // graph properties
114 
115  rankdir=LR;
116  compound=true;
117  node [shape=record fontsize=10 fontname="Verdana"];
118  edge [fontsize=8 fontname="Verdana"];
119 
120  // nodes
121 
122  "mkbasicsrc()" ;
123  capsfilter1 ;
124  resample ;
125  capsfilter2 ;
126  queue ;
127  whiten ;
128  fakesink ;
129 
130  "mkbasicsrc()" -> capsfilter1 -> resample -> capsfilter2 -> queue -> whiten -> fakesink;
131  }
132  """
133 
134  #
135  # 8 FFT-lengths is just a ball-parky estimate of how much data is
136  # needed for a good PSD, this isn't a requirement of the code (the
137  # code requires a minimum of 1)
138  #
139 
140  if gw_data_source_info.seg is not None and float(abs(gw_data_source_info.seg)) < 8 * psd_fft_length:
141  raise ValueError("segment %s too short" % str(gw_data_source_info.seg))
142 
143  #
144  # build pipeline
145  #
146 
147  if verbose:
148  print >>sys.stderr, "measuring PSD in segment %s" % str(gw_data_source_info.seg)
149  print >>sys.stderr, "building pipeline ..."
150  mainloop = GObject.MainLoop()
151  pipeline = Gst.Pipeline(name="psd")
152  handler = PSDHandler(mainloop, pipeline)
153 
154  head, _, _ = datasource.mkbasicsrc(pipeline, gw_data_source_info, instrument, verbose = verbose)
155  head = pipeparts.mkcapsfilter(pipeline, head, "audio/x-raw, rate=[%d,MAX]" % rate) # disallow upsampling
156  head = pipeparts.mkresample(pipeline, head, quality = 9)
157  head = pipeparts.mkcapsfilter(pipeline, head, "audio/x-raw, rate=%d" % rate)
158  head = pipeparts.mkqueue(pipeline, head, max_size_buffers = 8)
159  if gw_data_source_info.seg is not None:
160  average_samples = int(round(float(abs(gw_data_source_info.seg)) / (psd_fft_length / 2.) - 1.))
161  else:
162  #FIXME maybe let the user specify this
163  average_samples = 64
164  head = pipeparts.mkwhiten(pipeline, head, psd_mode = 0, zero_pad = 0, fft_length = psd_fft_length, average_samples = average_samples, median_samples = 7)
165  pipeparts.mkfakesink(pipeline, head)
166 
167  #
168  # setup signal handler to shutdown pipeline for live data
169  #
170 
171  if gw_data_source_info.data_source in ("lvshm", "framexmit"):# FIXME what about nds online?
173 
174  #
175  # process segment
176  #
177 
178  if verbose:
179  print >>sys.stderr, "putting pipeline into READY state ..."
180  if pipeline.set_state(Gst.State.READY) == Gst.StateChangeReturn.FAILURE:
181  raise RuntimeError("pipeline failed to enter READY state")
182  if gw_data_source_info.data_source not in ("lvshm", "framexmit"):# FIXME what about nds online?
183  datasource.pipeline_seek_for_gps(pipeline, *gw_data_source_info.seg)
184  if verbose:
185  print >>sys.stderr, "putting pipeline into PLAYING state ..."
186  if pipeline.set_state(Gst.State.PLAYING) == Gst.StateChangeReturn.FAILURE:
187  raise RuntimeError("pipeline failed to enter PLAYING state")
188  if verbose:
189  print >>sys.stderr, "running pipeline ..."
190  mainloop.run()
191 
192  #
193  # done
194  #
195 
196  if verbose:
197  print >>sys.stderr, "PSD measurement complete"
198  return handler.psd
199 
200 
201 def write_psd(filename, psddict, verbose = False, trap_signals = None):
202  """
203  Wrapper around make_psd_xmldoc() to write the XML document directly
204  to a named file.
205  """
206  ligolw_utils.write_filename(lal.series.make_psd_xmldoc(psddict), filename, gz = (filename or "stdout").endswith(".gz"), verbose = verbose, trap_signals = trap_signals)
207 
208 
209 #
210 # =============================================================================
211 #
212 # PSD Utilities
213 #
214 # =============================================================================
215 #
216 
217 
218 class HorizonDistance(object):
219  def __init__(self, f_min, f_max, delta_f, m1, m2, spin1 = (0., 0., 0.), spin2 = (0., 0., 0.), eccentricity = 0., inclination = 0.):
220  """
221  Configures the horizon distance calculation for a specific
222  waveform model. The waveform is pre-computed and stored,
223  so this initialization step can be time-consuming but
224  computing horizon distances from measured PSDs will be
225  fast.
226 
227  The waveform model's spectrum parameters, for example its
228  Nyquist and frequency resolution, need not match the
229  parameters for the PSDs that will ultimately be supplied
230  but there are some advantages to be had in getting them to
231  match. For example, computing the waveform with a smaller
232  delta_f than will be needed will require additional storage
233  and consume additional CPU time for the initialization,
234  while computing it with too low an f_max or too large a
235  delta_f might lead to inaccurate horizon distance
236  estimates.
237 
238  f_min (Hertz) sets the frequency at which the waveform
239  model is to begin.
240 
241  f_max (Hertz) sets the frequency upto which the waveform's
242  model is desired.
243 
244  delta_f (Hertz) sets the frequency resolution of the
245  desired waveform model.
246 
247  m1, m2 (solar masses) set the component masses of the
248  system to model.
249 
250  spin1, spin2 (3-component vectors, geometric units) set the
251  spins of the component masses.
252 
253  eccentricity [0, 1) sets the eccentricity of the system.
254 
255  inclination (radians) sets the orbital inclination of the
256  system.
257 
258  Example:
259 
260  >>> # configure for non-spinning, circular, 1.4+1.4 BNS
261  >>> horizon_distance = HorizonDistance(10., 1024., 1./32., 1.4, 1.4)
262  >>> # populate a PSD for testing
263  >>> import lal, lalsimulation
264  >>> psd = lal.CreateREAL8FrequencySeries("psd", lal.LIGOTimeGPS(0), 0., 1./32., lal.Unit("strain^2 s"), horizon_distance.model.data.length)
265  >>> lalsimulation.SimNoisePSDaLIGODesignSensitivityP1200087(psd, 0.)
266  0
267  >>> # compute horizon distance
268  >>> D, (f, model) = horizon_distance(psd)
269  >>> print "%.4g Mpc" % D
270  434.5 Mpc
271  >>> # compute distance and spectrum for SNR = 25
272  >>> D, (f, model) = horizon_distance(psd, 25.)
273  >>> "%.4g Mpc" % D
274  '139 Mpc'
275  >>> f
276  array([ 10. , 10.03125, 10.0625 , ..., 1023.9375 ,
277  1023.96875, 1024. ])
278  >>> model
279  array([ 8.00942750e-45, 7.95221458e-45, 7.89520620e-45, ...,
280  1.11473675e-49, 1.11465176e-49, 1.11456678e-49])
281 
282  NOTE:
283 
284  - Currently the SEOBNRv4_ROM waveform model is used, so its
285  limitations with respect to masses, spins, etc., apply.
286  The choice of waveform model is subject to change.
287  """
288  self.f_min = f_min
289  self.f_max = f_max
290  self.m1 = m1
291  self.m2 = m2
292  self.spin1 = numpy.array(spin1)
293  self.spin2 = numpy.array(spin2)
294  self.inclination = inclination
295  self.eccentricity = eccentricity
296  # NOTE: the waveform models are computed up-to but not
297  # including the supplied f_max parameter so we need to pass
298  # (f_max + delta_f) if we want the waveform model defined
299  # in the f_max bin
300  hp, hc = lalsimulation.SimInspiralFD(
301  m1 * lal.MSUN_SI, m2 * lal.MSUN_SI,
302  spin1[0], spin1[1], spin1[2],
303  spin2[0], spin2[1], spin2[2],
304  1.0, # distance (m)
305  inclination,
306  0.0, # reference orbital phase (rad)
307  0.0, # longitude of ascending nodes (rad)
308  eccentricity,
309  0.0, # mean anomaly of periastron
310  delta_f,
311  f_min,
312  f_max + delta_f,
313  100., # reference frequency (Hz)
314  None, # LAL dictionary containing accessory parameters
315  lalsimulation.GetApproximantFromString("SEOBNRv4_ROM")
316  )
317  assert hp.data.length > 0, "huh!? h+ has zero length!"
318 
319  #
320  # store |h(f)|^2 for source at D = 1 m. see (5) in
321  # arXiv:1003.2481
322  #
323 
324  self.model = lal.CreateREAL8FrequencySeries(
325  name = "signal spectrum",
326  epoch = LIGOTimeGPS(0),
327  f0 = hp.f0,
328  deltaF = hp.deltaF,
329  sampleUnits = hp.sampleUnits * hp.sampleUnits,
330  length = hp.data.length
331  )
332  self.model.data.data[:] = numpy.abs(hp.data.data)**2.
333 
334 
335  def __call__(self, psd, snr = 8.):
336  """
337  Compute the horizon distance for the configured waveform
338  model given the PSD and the SNR at which the horizon is
339  defined (default = 8). Equivalently, from a PSD and an
340  observed SNR compute and return the amplitude of the
341  configured waveform's spectrum required to achieve that
342  SNR.
343 
344  The return value is a two-element tuple. The first element
345  is the horizon distance in Mpc. The second element is,
346  itself, a two-element tuple containing two vectors giving
347  the frequencies and amplitudes of the waveform model's
348  spectrum scaled so as to have the given SNR. The vectors
349  are clipped to the range of frequencies that were used for
350  the SNR integral.
351 
352  The parameters of the PSD, for example its Nyquist and
353  frequency resolution, need not match the parameters of the
354  configured waveform model. In the event of a mismatch, the
355  waveform model is resampled to the frequencies at which the
356  PSD has been measured.
357 
358  The inspiral spectrum returned has the same units as the
359  PSD and is normalized so that the SNR is
360 
361  SNR^2 = \int (inspiral_spectrum / psd) df
362 
363  That is, the ratio of the inspiral spectrum to the PSD
364  gives the spectral density of SNR^2.
365  """
366  #
367  # frequencies at which PSD has been measured
368  #
369 
370  f = psd.f0 + numpy.arange(psd.data.length) * psd.deltaF
371 
372  #
373  # nearest-neighbour interpolation of waveform model
374  # evaluated at PSD's frequency bins
375  #
376 
377  indexes = ((f - self.model.f0) / self.model.deltaF).round().astype("int").clip(0, self.model.data.length - 1)
378  model = self.model.data.data[indexes]
379 
380  #
381  # range of indexes for integration
382  #
383 
384  kmin = (max(psd.f0, self.model.f0, self.f_min) - psd.f0) / psd.deltaF
385  kmin = int(round(kmin))
386  kmax = (min(psd.f0 + psd.data.length * psd.deltaF, self.model.f0 + self.model.data.length * self.model.deltaF, self.f_max) - psd.f0) / psd.deltaF
387  kmax = int(round(kmax)) + 1
388  assert kmin < kmax, "PSD and waveform model do not intersect"
389 
390  #
391  # SNR for source at D = 1 m <--> D in m for source w/ SNR =
392  # 1. see (3) in arXiv:1003.2481
393  #
394 
395  f = f[kmin:kmax]
396  model = model[kmin:kmax]
397  D = math.sqrt(4. * (model / psd.data.data[kmin:kmax]).sum() * psd.deltaF)
398 
399  #
400  # distance at desired SNR
401  #
402 
403  D /= snr
404 
405  #
406  # scale inspiral spectrum by distance to achieve desired SNR
407  #
408 
409  model *= 4. / D**2.
410 
411  #
412  # D in Mpc for source with specified SNR, and waveform
413  # model
414  #
415 
416  return D / (1e6 * lal.PC_SI), (f, model)
417 
418 
419 def effective_distance_factor(inclination, fp, fc):
420  """
421  Returns the ratio of effective distance to physical distance for
422  compact binary mergers. Inclination is the orbital inclination of
423  the system in radians, fp and fc are the F+ and Fx antenna factors.
424  See lal.ComputeDetAMResponse() for a function to compute antenna
425  factors. The effective distance is given by
426 
427  Deff = effective_distance_factor * D
428 
429  See Equation (4.3) of arXiv:0705.1514.
430  """
431  cos2i = math.cos(inclination)**2
432  return 1.0 / math.sqrt(fp**2 * (1+cos2i)**2 / 4 + fc**2 * cos2i)
433 
434 
435 class PSDFirKernel(object):
436  def __init__(self):
437  self.revplan = None
438  self.fwdplan = None
439  self.target_phase = None
440  self.target_phase_mask = None
441 
442  def set_phase(self, psd, f_low = 10.0, m1 = 1.4, m2 = 1.4):
443  # compute phase response of zero-latency whitening filter
444  # corresponding to psd
445  kernel, latency, sample_rate = self.psd_to_linear_phase_whitening_fir_kernel(psd)
446  kernel, phase = self.linear_phase_fir_kernel_to_minimum_phase_whitening_fir_kernel(kernel, sample_rate)
447 
448  # get merger model for SNR = 1.
449  f_psd = psd.f0 + numpy.arange(len(psd.data.data)) * psd.deltaF
450  horizon_distance = HorizonDistance(f_low, f_psd[-1], psd.deltaF, m1, m2)
451  f_model, model= horizon_distance(psd, 1.)[1]
452 
453  # find the range of frequency bins covered by the merger
454  # model
455  kmin, kmax = f_psd.searchsorted(f_model[0]), f_psd.searchsorted(f_model[-1]) + 1
456 
457  # compute SNR=1 model's (d SNR^2 / df) spectral density
458  unit_snr2_density = numpy.zeros_like(phase)
459  unit_snr2_density[kmin:kmax] = model / psd.data.data[kmin:kmax]
460 
461  # integrate across each frequency bin, converting to
462  # snr^2/bin. NOTE: this step is here for the record, but
463  # is commented out because it has no effect on the result
464  # given the renormalization that occurs next.
465  #unit_snr2_density *= psd.deltaF
466 
467  # take 16th root, then normalize so max=1. why? I don't
468  # know, just feels good, on the whole.
469  unit_snr2_density = unit_snr2_density**(1./16)
470  unit_snr2_density /= unit_snr2_density.max()
471 
472  # record phase vector and SNR^2 density vector
473  self.target_phase = phase
474  self.target_phase_mask = unit_snr2_density
475 
476  def psd_to_linear_phase_whitening_fir_kernel(self, psd, invert = True, nyquist = None):
477  """
478  Compute an acausal finite impulse-response filter kernel
479  from a power spectral density conforming to the LAL
480  normalization convention, such that if colored Gaussian
481  random noise with the given PSD is fed into an FIR filter
482  using the kernel the filter's output will be zero-mean
483  unit-variance Gaussian random noise. The PSD must be
484  provided as a lal.REAL8FrequencySeries object.
485 
486  The phase response of this filter is 0, just like whitening
487  done in the frequency domain.
488 
489  The return value is the tuple (kernel, latency, sample
490  rate). The kernel is a numpy array containing the filter
491  kernel, the latency is the filter latency in samples and
492  the sample rate is in Hz. The kernel and latency can be
493  used, for example, with gstreamer's stock audiofirfilter
494  element.
495  """
496  #
497  # this could be relaxed with some work
498  #
499 
500  assert psd.f0 == 0.0
501 
502  #
503  # extract the PSD bins and determine sample rate for kernel
504  #
505 
506  data = psd.data.data / 2
507  sample_rate = 2 * int(round(psd.f0 + (len(data) - 1) * psd.deltaF))
508 
509  #
510  # remove LAL normalization
511  #
512 
513  data *= sample_rate
514 
515  #
516  # change Nyquist frequency if requested. round to nearest
517  # available bin
518  #
519 
520  if nyquist is not None:
521  i = int(round((nyquist * 2. - psd.f0) / psd.deltaF))
522  assert i < len(data)
523  data = data[:i + 1]
524  sample_rate = 2 * int(round(psd.f0 + (len(data) - 1) * psd.deltaF))
525 
526  #
527  # compute the FIR kernel. it always has an odd number of
528  # samples and no DC offset.
529  #
530 
531  data[0] = data[-1] = 0.0
532  if invert:
533  data_nonzeros = (data != 0.)
534  data[data_nonzeros] = 1./data[data_nonzeros]
535  # repack data: data[0], data[1], 0, data[2], 0, ....
536  tmp = numpy.zeros((2 * len(data) - 1,), dtype = data.dtype)
537  tmp[len(data)-1:] = data
538  #tmp[:len(data)] = data
539  data = tmp
540 
541  kernel_fseries = lal.CreateCOMPLEX16FrequencySeries(
542  name = "double sided psd",
543  epoch = LIGOTimeGPS(0),
544  f0 = 0.0,
545  deltaF = psd.deltaF,
546  length = len(data),
547  sampleUnits = lal.Unit("strain s")
548  )
549 
550  kernel_tseries = lal.CreateCOMPLEX16TimeSeries(
551  name = "timeseries of whitening kernel",
552  epoch = LIGOTimeGPS(0.),
553  f0 = 0.,
554  deltaT = 1.0 / sample_rate,
555  length = len(data),
556  sampleUnits = lal.Unit("strain")
557  )
558 
559  # FIXME check for change in length
560  if self.revplan is None:
561  self.revplan = lal.CreateReverseCOMPLEX16FFTPlan(len(data), 1)
562 
563  kernel_fseries.data.data = numpy.sqrt(data) + 0.j
564  lal.COMPLEX16FreqTimeFFT(kernel_tseries, kernel_fseries, self.revplan)
565  kernel = kernel_tseries.data.data.real
566  kernel = numpy.roll(kernel, (len(data) - 1) / 2) / sample_rate * 2
567 
568  #
569  # apply a Tukey window whose flat bit is 50% of the kernel.
570  # preserve the FIR kernel's square magnitude
571  #
572 
573  norm_before = numpy.dot(kernel, kernel)
574  kernel *= lal.CreateTukeyREAL8Window(len(data), .5).data.data
575  kernel *= math.sqrt(norm_before / numpy.dot(kernel, kernel))
576 
577  #
578  # the kernel's latency
579  #
580 
581  latency = (len(data) - 1) / 2
582 
583  #
584  # done
585  #
586 
587  return kernel, latency, sample_rate
588 
589 
590  def linear_phase_fir_kernel_to_minimum_phase_whitening_fir_kernel(self, linear_phase_kernel, sample_rate):
591  """
592  Compute the minimum-phase response filter (zero latency)
593  associated with a linear-phase response filter (latency
594  equal to half the filter length).
595 
596  From "Design of Optimal Minimum-Phase Digital FIR Filters
597  Using Discrete Hilbert Transforms", IEEE Trans. Signal
598  Processing, vol. 48, pp. 1491-1495, May 2000.
599 
600  The return value is the tuple (kernel, phase response).
601  The kernel is a numpy array containing the filter kernel.
602  The kernel can be used, for example, with gstreamer's stock
603  audiofirfilter element.
604  """
605  #
606  # compute abs of FFT of kernel
607  #
608 
609  # FIXME check for change in length
610  if self.fwdplan is None:
611  self.fwdplan = lal.CreateForwardCOMPLEX16FFTPlan(len(linear_phase_kernel), 1)
612  if self.revplan is None:
613  self.revplan = lal.CreateReverseCOMPLEX16FFTPlan(len(linear_phase_kernel), 1)
614 
615  deltaT = 1. / sample_rate
616  deltaF = 1. / (len(linear_phase_kernel) * deltaT)
617  working_length = len(linear_phase_kernel)
618 
619  kernel_tseries = lal.CreateCOMPLEX16TimeSeries(
620  name = "timeseries of whitening kernel",
621  epoch = LIGOTimeGPS(0.),
622  f0 = 0.,
623  deltaT = deltaT,
624  length = working_length,
625  sampleUnits = lal.Unit("strain")
626  )
627  kernel_tseries.data.data = linear_phase_kernel
628 
629  absX = lal.CreateCOMPLEX16FrequencySeries(
630  name = "absX",
631  epoch = LIGOTimeGPS(0),
632  f0 = 0.0,
633  deltaF = deltaF,
634  length = working_length,
635  sampleUnits = lal.Unit("strain s")
636  )
637 
638  logabsX = lal.CreateCOMPLEX16FrequencySeries(
639  name = "absX",
640  epoch = LIGOTimeGPS(0),
641  f0 = 0.0,
642  deltaF = deltaF,
643  length = working_length,
644  sampleUnits = lal.Unit("strain s")
645  )
646 
647  cepstrum = lal.CreateCOMPLEX16TimeSeries(
648  name = "cepstrum",
649  epoch = LIGOTimeGPS(0.),
650  f0 = 0.,
651  deltaT = deltaT,
652  length = working_length,
653  sampleUnits = lal.Unit("strain")
654  )
655 
656  theta = lal.CreateCOMPLEX16FrequencySeries(
657  name = "theta",
658  epoch = LIGOTimeGPS(0),
659  f0 = 0.0,
660  deltaF = deltaF,
661  length = working_length,
662  sampleUnits = lal.Unit("strain s")
663  )
664 
665  min_phase_kernel = lal.CreateCOMPLEX16TimeSeries(
666  name = "min phase kernel",
667  epoch = LIGOTimeGPS(0.),
668  f0 = 0.,
669  deltaT = deltaT,
670  length = working_length,
671  sampleUnits = lal.Unit("strain")
672  )
673 
674  lal.COMPLEX16TimeFreqFFT(absX, kernel_tseries, self.fwdplan)
675  absX.data.data[:] = abs(absX.data.data)
676 
677  #
678  # compute the cepstrum of the kernel (i.e., the iFFT of the
679  # log of the abs of the FFT of the kernel)
680  #
681 
682  logabsX.data.data[:] = numpy.log(absX.data.data)
683  lal.COMPLEX16FreqTimeFFT(cepstrum, logabsX, self.revplan)
684 
685  #
686  # multiply cepstrum by sgn
687  #
688 
689  cepstrum.data.data[0] = 0.
690  cepstrum.data.data[working_length // 2] = 0.
691  cepstrum.data.data[working_length // 2 + 1:] = -cepstrum.data.data[working_length // 2 + 1:]
692 
693  #
694  # compute theta
695  #
696 
697  lal.COMPLEX16TimeFreqFFT(theta, cepstrum, self.fwdplan)
698 
699  #
700  # compute the gain and phase of the zero-phase
701  # approximation relative to the original linear-phase
702  # filter
703  #
704 
705  theta_data = theta.data.data[working_length // 2:]
706  #gain = numpy.exp(theta_data.real)
707  phase = -theta_data.imag
708 
709  #
710  # apply optional masked phase adjustment
711  #
712 
713  if self.target_phase is not None:
714  # compute phase adjustment for +ve frequencies
715  phase_adjustment = (self.target_phase - phase) * self.target_phase_mask
716 
717  # combine with phase adjustment for -ve frequencies
718  phase_adjustment = numpy.concatenate((phase_adjustment[1:][-1::-1].conj(), phase_adjustment))
719 
720  # apply adjustment. phase adjustment is what we
721  # wish to add to the phase. theta's imaginary
722  # component contains the negative of the phase, so
723  # we need to add -phase to theta's imaginary
724  # component
725  theta.data.data += -1.j * phase_adjustment
726 
727  # report adjusted phase
728  #phase = -theta.data.data[working_length // 2:].imag
729 
730  #
731  # compute minimum phase kernel
732  #
733 
734  absX.data.data *= numpy.exp(theta.data.data)
735  lal.COMPLEX16FreqTimeFFT(min_phase_kernel, absX, self.revplan)
736 
737  kernel = min_phase_kernel.data.data.real
738 
739  #
740  # this kernel needs to be reversed to follow conventions
741  # used with the audiofirfilter and lal_firbank elements
742  #
743 
744  kernel = kernel[-1::-1]
745 
746  #
747  # done
748  #
749 
750  return kernel, phase
751 
752 
753 def interpolate_psd(psd, deltaF):
754  #
755  # no-op?
756  #
757 
758  if deltaF == psd.deltaF:
759  return psd
760 
761  #
762  # interpolate PSD by clipping/zero-padding time-domain impulse
763  # response of equivalent whitening filter
764  #
765 
766  #from scipy import fftpack
767  #psd_data = psd.data.data
768  #x = numpy.zeros((len(psd_data) * 2 - 2,), dtype = "double")
769  #psd_data = numpy.where(psd_data, psd_data, float("inf"))
770  #x[0] = 1 / psd_data[0]**.5
771  #x[1::2] = 1 / psd_data[1:]**.5
772  #x = fftpack.irfft(x)
773  #if deltaF < psd.deltaF:
774  # x *= numpy.cos(numpy.arange(len(x)) * math.pi / (len(x) + 1))**2
775  # x = numpy.concatenate((x[:(len(x) / 2)], numpy.zeros((int(round(len(x) * psd.deltaF / deltaF)) - len(x),), dtype = "double"), x[(len(x) / 2):]))
776  #else:
777  # x = numpy.concatenate((x[:(int(round(len(x) * psd.deltaF / deltaF)) / 2)], x[-(int(round(len(x) * psd.deltaF / deltaF)) / 2):]))
778  # x *= numpy.cos(numpy.arange(len(x)) * math.pi / (len(x) + 1))**2
779  #x = 1 / fftpack.rfft(x)**2
780  #psd_data = numpy.concatenate(([x[0]], x[1::2]))
781 
782  #
783  # interpolate log(PSD) with cubic spline. note that the PSD is
784  # clipped at 1e-300 to prevent nan's in the interpolator (which
785  # doesn't seem to like the occasional sample being -inf)
786  #
787 
788  psd_data = psd.data.data
789  psd_data = numpy.where(psd_data, psd_data, 1e-300)
790  f = psd.f0 + numpy.arange(len(psd_data)) * psd.deltaF
791  interp = interpolate.splrep(f, numpy.log(psd_data), s = 0)
792  f = psd.f0 + numpy.arange(round((len(psd_data) - 1) * psd.deltaF / deltaF) + 1) * deltaF
793  psd_data = numpy.exp(interpolate.splev(f, interp, der = 0))
794 
795  #
796  # return result
797  #
798 
799  psd = lal.CreateREAL8FrequencySeries(
800  name = psd.name,
801  epoch = psd.epoch,
802  f0 = psd.f0,
803  deltaF = deltaF,
804  sampleUnits = psd.sampleUnits,
805  length = len(psd_data)
806  )
807  psd.data.data = psd_data
808 
809  return psd
810 
811 
812 def movingmedian(psd, window_size):
813  """
814  Assumes that the underlying PSD doesn't have variance, i.e., that there
815  is no median / mean correction factor required
816  """
817  data = psd.data.data
818  datacopy = numpy.copy(data)
819  for i in range(window_size, len(data)-window_size):
820  datacopy[i] = numpy.median(data[i-window_size:i+window_size])
821  psd = lal.CreateREAL8FrequencySeries(
822  name = psd.name,
823  epoch = psd.epoch,
824  f0 = psd.f0,
825  deltaF = psd.deltaF,
826  sampleUnits = psd.sampleUnits,
827  length = len(datacopy)
828  )
829  psd.data.data = datacopy
830  return psd
831 
832 
833 def polyfit(psd, minsample, maxsample, order, verbose = False):
834  # f / f_min between f_min and f_max, i.e. f[0] here is 1
835  f = numpy.arange(maxsample - minsample) * psd.deltaF + 1
836  data = psd.data.data[minsample:maxsample]
837 
838  logf = numpy.linspace(numpy.log(f[0]), numpy.log(f[-1]), 100000)
839  interp = interpolate.interp1d(numpy.log(f), numpy.log(data))
840  data = interp(logf)
841  p = numpy.poly1d(numpy.polyfit(logf, data, order))
842  if verbose:
843  print >> sys.stderr, "\nFit polynomial is: \n\nlog(PSD) = \n", p, "\n\nwhere x = f / f_min\n"
844  data = numpy.exp(p(numpy.log(f)))
845  olddata = psd.data.data
846  olddata[minsample:maxsample] = data
847  psd = lal.CreateREAL8FrequencySeries(
848  name = psd.name,
849  epoch = psd.epoch,
850  f0 = psd.f0,
851  deltaF = psd.deltaF,
852  sampleUnits = psd.sampleUnits,
853  length = len(olddata)
854  )
855  psd.data.data = olddata
856  return psd
857 
858 def one_second_highpass_kernel(rate, cutoff = 12):
859  highpass_filter_fd = numpy.ones(rate, dtype=complex)
860  highpass_filter_fd[:int(cutoff)] = 0.
861  highpass_filter_fd[-int(cutoff):] = 0.
862  highpass_filter_fd[rate/2-1:rate/2+1] = 0.
863  highpass_filter_td = fftpack.ifft(highpass_filter_fd)
864  highpass_filter_td = numpy.roll(highpass_filter_td.real, rate/2)
865  highpass_filter_kernel = numpy.zeros(len(highpass_filter_td)+1)
866  highpass_filter_kernel[:-1] = highpass_filter_td[:]
867  x = numpy.arange(len(highpass_filter_kernel))
868  mid = len(x) / 2.
869  highpass_filter_kernel *= 1. - (x-mid)**2 / mid**2
870  return highpass_filter_kernel
871 
872 def fixed_duration_bandpass_kernel(rate, flow = 0, fhigh = float("inf"), duration = 1.0):
873  deltaF = 1. / duration
874  nsamps = int(rate * duration) + 1
875  f = numpy.arange(nsamps) * deltaF - rate / 2.
876  filt = numpy.ones(len(f))
877  ix1 = numpy.logical_and(f <= -flow, f >= -fhigh)
878  ix2 = numpy.logical_and(f >= flow, f <= fhigh)
879  filt[numpy.logical_not(numpy.logical_or(ix1, ix2))] = 0.
880  filt = numpy.real(scipy.ifft(scipy.fftpack.ifftshift(filt))) / nsamps
881  window = numpy.sinc(2 * f / rate)
882  out = numpy.roll(filt, nsamps / 2) * window
883  out /= (out**2).sum()**.5
884  return out
def __init__(self, f_min, f_max, delta_f, m1, m2, spin1=(0., 0., 0.), spin2=(0., 0., 0.), eccentricity=0., inclination=0.)
def linear_phase_fir_kernel_to_minimum_phase_whitening_fir_kernel(self, linear_phase_kernel, sample_rate)
def psd_to_linear_phase_whitening_fir_kernel(self, psd, invert=True, nyquist=None)