30from typing
import Dict, Iterable, Optional, Tuple, Union
34from scipy
import interpolate
37gi.require_version(
'Gst',
'1.0')
38from gi.repository
import GObject
39from gi.repository
import Gst
44from ligo.lw
import utils
as ligolw_utils
47from lal
import LIGOTimeGPS
51from gstlal
import datasource
52from gstlal
import pipeparts
53from gstlal
import pipeio
54from gstlal
import simplehandler
60+-------------------------------------------------+------------------------------------------+------------+
61| Names | Hash | Date |
62+=================================================+==========================================+============+
63| Florent, Sathya, Duncan Me., Jolien, Kipp, Chad | b3ef077fe87b597578000f140e4aa780f3a227aa | 2014-05-01 |
64+-------------------------------------------------+------------------------------------------+------------+
86 simplehandler.Handler.__init__(self, *args, **kwargs)
89 if message.type == Gst.MessageType.ELEMENT
and message.get_structure().get_name() ==
"spectrum":
90 self.
psd = pipeio.parse_spectrum_message(message)
99 def on_spectrum_message(self, message):
100 self.
psd = pipeio.parse_spectrum_message(message)
109def measure_psd(gw_data_source_info, instrument, rate, psd_fft_length = 8, verbose = False):
120 node [shape=record fontsize=10 fontname="Verdana"];
121 edge [fontsize=8 fontname="Verdana"];
133 "mkbasicsrc()" -> capsfilter1 -> resample -> capsfilter2 -> queue -> whiten -> fakesink;
136 Stream Implementation: (future refactor)
137 stream = Stream.from_datasource(gw_data_source_info, instrument, verbose=verbose)
138 stream.add_callback(MessageType.ELEMENT, "spectrum", tracker.on_spectrum_message)
140 stream = stream.capsfilter(f"audio/x-raw, rate=[{rate:d},MAX]") # disallow upsampling
141 stream.resample(quality=9) \
142 .capsfilter(f"audio/x-raw, rate={rate:d}") \
143 .queue(max_size_buffers=8) \
147 fft_length=psd_fft_length,
148 average_samples=average_samples,
161 if gw_data_source_info.seg
is not None and float(abs(gw_data_source_info.seg)) < 8 * psd_fft_length:
162 raise ValueError(
"segment %s too short" % str(gw_data_source_info.seg))
169 print(
"measuring PSD in segment %s" % str(gw_data_source_info.seg), file=sys.stderr)
170 print(
"building pipeline ...", file=sys.stderr)
171 mainloop = GObject.MainLoop()
172 pipeline = Gst.Pipeline(name=
"psd")
175 head, _, _, _ = datasource.mkbasicsrc(pipeline, gw_data_source_info, instrument, verbose = verbose)
176 head = pipeparts.mkcapsfilter(pipeline, head,
"audio/x-raw, rate=[%d,MAX]" % rate)
177 head = pipeparts.mkresample(pipeline, head, quality = 9)
178 head = pipeparts.mkcapsfilter(pipeline, head,
"audio/x-raw, rate=%d" % rate)
179 head = pipeparts.mkqueue(pipeline, head, max_size_buffers = 8)
180 if gw_data_source_info.seg
is not None:
181 average_samples = int(round(float(abs(gw_data_source_info.seg)) / (psd_fft_length / 2.) - 1.))
185 head = pipeparts.mkwhiten(pipeline, head, psd_mode = 0, zero_pad = 0, fft_length = psd_fft_length, average_samples = average_samples, median_samples = 7)
186 pipeparts.mkfakesink(pipeline, head)
192 if gw_data_source_info.data_source
in (
"lvshm",
"framexmit"):
200 print(
"putting pipeline into READY state ...", file=sys.stderr)
201 if pipeline.set_state(Gst.State.READY) == Gst.StateChangeReturn.FAILURE:
202 raise RuntimeError(
"pipeline failed to enter READY state")
203 if gw_data_source_info.data_source
not in (
"lvshm",
"framexmit"):
204 datasource.pipeline_seek_for_gps(pipeline, *gw_data_source_info.seg)
206 print(
"putting pipeline into PLAYING state ...", file=sys.stderr)
207 if pipeline.set_state(Gst.State.PLAYING) == Gst.StateChangeReturn.FAILURE:
208 raise RuntimeError(
"pipeline failed to enter PLAYING state")
210 print(
"running pipeline ...", file=sys.stderr)
218 print(
"PSD measurement complete", file=sys.stderr)
222def read_psd(filename: str, verbose: Optional[bool] =
False) -> Dict[str, lal.REAL8FrequencySeries]:
223 """Reads in an XML-formatted PSD.
227 str, the file to read PSD(s) from
229 bool, default False, whether to display logging messages
232 a dictionary of lal FrequencySeries keyed by instrument
235 return lal.series.read_psd_xmldoc(
236 ligolw_utils.load_filename(
239 contenthandler=lal.series.PSDContentHandler
246 psddict: Dict[str, lal.REAL8FrequencySeries],
247 trap_signals: Optional[Iterable[signal.Signals]] =
None,
248 verbose: Optional[bool] =
False,
250 """Writes an XML-formatted PSD to disk.
252 Wrapper around make_psd_xmldoc() to write the XML document directly
257 str, the file to write PSD(s) to
259 Dict[str, lal.REAL8FrequencySeries], the PSD(s)
261 Iterable[signal.Signal], optional, whether to attach extra signal
264 bool, default False, whether to display logging messages
267 ligolw_utils.write_filename(lal.series.make_psd_xmldoc(psddict), filename, verbose = verbose, trap_signals = trap_signals)
273 zero_pad: bool =
False,
274 read_as_psd: bool =
False,
275) -> lal.REAL8FrequencySeries:
276 """Reads in a text-formatted ASD as a PSD.
280 str, the file to read ASD(s) from
282 float, default 0.25, the frequency resolution to interpolate to
284 bool, default False, whether to zero-pad PSD to 0 Hz if needed.
286 bool, default False, whether to treat input as PSD rather than ASD
289 lal.REAL8FrequencySeries, the PSD
292 data = numpy.loadtxt(filename, comments=
"#")
293 psd_data = data[:, 1]
295 psd_data = numpy.power(psd_data, 2)
299 f_pad = numpy.arange(0, f[0], df)
300 psd_data = numpy.concatenate((numpy.ones(len(f_pad)) * psd_data[0], psd_data))
301 f = numpy.concatenate((f_pad, f))
303 uniformf = numpy.arange(f[0], f.max(), df)
304 psdinterp = interpolate.interp1d(f, psd_data)
305 psd_data = psdinterp(uniformf)
307 psd = lal.CreateREAL8FrequencySeries(
312 sampleUnits = lal.Unit(
"s strain^2"),
313 length = len(psd_data),
315 psd.data.data = psd_data
322 psd: lal.REAL8FrequencySeries,
323 verbose: Optional[bool] =
False,
325 """Writes an text-formatted ASD to disk.
329 str, the file to write ASD to
331 lal.REAL8FrequencySeries, the PSD
333 bool, default False, whether to display logging messages
337 print(
"writing '%s' ..." % filename, file=sys.stderr)
338 with open(filename,
"w")
as f:
339 for i, x
in enumerate(psd.data.data):
340 print(
"%.16g %.16g" % (psd.f0 + i * psd.deltaF, x**.5), file=f)
343def interpolate_psd(psd: lal.REAL8FrequencySeries, deltaF: int) -> lal.REAL8FrequencySeries:
344 """Interpolates a PSD to a target frequency resolution.
348 lal.REAL8FrequencySeries, the PSD to interpolate
350 int, the target frequency resolution to interpolate to
353 lal.REAL8FrequencySeries, the interpolated PSD
360 if deltaF == psd.deltaF:
390 psd_data = psd.data.data
391 psd_data = numpy.where(psd_data, psd_data, 1e-300)
392 f = psd.f0 + numpy.arange(len(psd_data)) * psd.deltaF
393 interp = interpolate.splrep(f, numpy.log(psd_data), s = 0)
394 f = psd.f0 + numpy.arange(round((len(psd_data) - 1) * psd.deltaF / deltaF) + 1) * deltaF
395 psd_data = numpy.exp(interpolate.splev(f, interp, der = 0))
401 psd = lal.CreateREAL8FrequencySeries(
406 sampleUnits = psd.sampleUnits,
407 length = len(psd_data)
409 psd.data.data = psd_data
415 psd: Union[numpy.ndarray, lal.REAL8FrequencySeries],
417) -> Union[numpy.ndarray, lal.REAL8FrequencySeries]:
418 """Smoothen a PSD with a moving median.
420 Assumes that the underlying PSD doesn't have variance, i.e., that there
421 is no median / mean correction factor required.
425 Union[numpy.ndarray, lal.REAL8FrequencySeries], the PSD to smoothen
427 int, the size of the window used for the moving median
430 a smoothened PSD of same type as input PSD
433 if isinstance(psd, numpy.ndarray):
434 tmp = numpy.copy(psd)
436 tmp = numpy.copy(psd.data.data)
439 tmp[window_size:(len(tmp) - window_size)] = numpy.array(
440 pandas.Series(tmp).rolling(2 * window_size).median()[(2 * window_size - 1):-1]
443 if isinstance(psd, numpy.ndarray):
446 new_psd = lal.CreateREAL8FrequencySeries(
451 sampleUnits = psd.sampleUnits,
458def movingaverage(psd: numpy.ndarray, window_size: int) -> numpy.ndarray:
459 """Smoothen a PSD with a moving average.
463 numpy.ndarray, the PSD to smoothen
465 int, the size of the window used for the moving median
471 window = lal.CreateTukeyREAL8Window(window_size, 0.5).data.data
472 return numpy.convolve(psd, window,
'same')
475def taperzero_fseries(
476 fseries: lal.REAL8FrequencySeries,
477 minfs: Optional[Tuple[float, float]] = (35.0, 40.0),
478 maxfs: Optional[Tuple[float, float]] = (1800., 2048.)
479) -> lal.REAL8FrequencySeries:
480 """Taper the PSD to infinity for given min/max frequencies.
484 lal.REAL8FrequencySeries, the PSD to taper
486 Tuple[float, float], optional, the frequency boundaries over which to taper the
487 spectrum to infinity. i.e., frequencies below the first item in the tuple will
488 have an infinite spectrum, the second item in the tuple will not be changed.
489 A taper from 0 to infinity is applied in between.
491 Tuple[float, float], optional, the frequency boundaries over which to taper the
492 spectrum to infinity. i.e., frequencies above the second item in the tuple will
493 have an infinite spectrum, the first item in the tuple will not be changed.
494 A taper from 0 to infinity is applied in between.
497 lal.REAL8FrequencySeries, the tapered PSD
505 data = fseries.data.data
506 norm_before = numpy.dot(data.conj(), data).real
512 deltaF = fseries.deltaF
513 kmin = int(minfs[0] / deltaF)
514 kmax = int(minfs[1] / deltaF)
515 data[(len(data)//2 + 1) - kmin + 1:(len(data)//2 + 1) + kmin] = 0.
516 data[(len(data)//2 + 1) + kmin:(len(data)//2 + 1) + kmax] *= numpy.sin(numpy.arange(kmax-kmin) / (kmax-kmin-1.) * numpy.pi / 2.0)**4
517 data[(len(data)//2 + 1) - kmax:(len(data)//2 + 1) - kmin] *= numpy.cos(numpy.arange(kmax-kmin) / (kmax-kmin-1.) * numpy.pi / 2.0)**4
519 kmin = int(maxfs[0] / deltaF) - 1
520 kmax = int(maxfs[1] / deltaF) - 1
521 data[(len(data)//2 + 1) + kmax:] = data[:-(len(data)//2 + 1) - kmax] = 0.
522 data[(len(data)//2 + 1) + kmin:(len(data)//2 + 1) + kmax] *= numpy.cos(numpy.arange(kmax-kmin) / (kmax-kmin-1.) * numpy.pi / 2.0)**4
523 data[(len(data)//2 + 1) - kmax:(len(data)//2 + 1) - kmin] *= numpy.sin(numpy.arange(kmax-kmin) / (kmax-kmin-1.) * numpy.pi / 2.0)**4
529 fseries.data.data = data * math.sqrt(norm_before / numpy.dot(data.conj(), data).real)
539 psd: lal.REAL8FrequencySeries,
541 minfs: Optional[Tuple[int, int]] = (35.0, 40.0),
542 maxfs: Optional[Tuple[int, int]] = (1800., 2048.),
543 smoothing_frequency: Optional[float] = 4.,
544 fir_whiten: Optional[bool] =
False
545) -> lal.REAL8FrequencySeries:
546 """Condition a PSD suitable for whitening waveforms.
550 lal.REAL8FrequencySeries, the PSD to taper
552 int, the target delta F to interpolate to
554 Tuple[float, float], optional, the frequency boundaries over which to taper the
555 spectrum to infinity. i.e., frequencies below the first item in the tuple will
556 have an infinite spectrum, the second item in the tuple will not be changed.
557 A taper from 0 to infinity is applied in between.
559 Tuple[float, float], optional, the frequency boundaries over which to taper the
560 spectrum to infinity. i.e., frequencies above the second item in the tuple will
561 have an infinite spectrum, the first item in the tuple will not be changed.
562 A taper from 0 to infinity is applied in between.
563 smoothing_frequency (Hz):
564 float, default = 4 Hz, the target frequency resolution after smoothing. Lines with
565 bandwidths << smoothing_frequency are removed via a median calculation.
566 Remaining features will be blurred out to this resolution.
568 bool, default False, whether to enable causal whitening with a time-domain
569 whitening kernel vs. traditional acausal whitening
572 lal.REAL8FrequencySeries, the conditioned PSD
580 horizon_distance =
HorizonDistance(minfs[1], maxfs[0], psd.deltaF, 1.4, 1.4)
581 horizon_before = horizon_distance(psd, 8.0)[0]
587 psd = interpolate_psd(psd, newdeltaF)
593 psddata = psd.data.data
594 avgwindow = int(smoothing_frequency / newdeltaF)
595 psddata = movingmedian(psddata, avgwindow)
596 psddata = movingaverage(psddata, avgwindow)
597 psd.data.data = psddata
608 psddata = psd.data.data
609 kmin = int(minfs[0] / newdeltaF)
610 kmax = int(minfs[1] / newdeltaF)
611 psddata[:kmin+1] = numpy.inf
612 psddata[kmin:kmax] /= numpy.sin(numpy.arange(kmax-kmin) / (kmax-kmin-1.) * numpy.pi / 2.0)**4
614 kmin = int(maxfs[0] / newdeltaF)
615 kmax = int(maxfs[1] / newdeltaF)
616 psddata[kmax:] = numpy.inf
617 psddata[kmin:kmax] /= numpy.cos(numpy.arange(kmax-kmin) / (kmax-kmin-1.) * numpy.pi / 2.0)**4
619 psd.data.data = psddata
625 horizon_after = horizon_distance(psd, 8.0)[0]
627 psddata = psd.data.data
628 psd.data.data = psddata * (horizon_after / horizon_before)**2
638 psd: lal.REAL8FrequencySeries,
642 verbose: Optional[bool] =
False
643) -> lal.REAL8FrequencySeries:
644 """Fit a PSD to a polynomial.
648 lal.REAL8FrequencySeries, the PSD to fit
650 float, the low frequency to begin fitting with
652 float, the high frequency to stop fitting with
654 int, the order of the fitting polynomial
656 bool, default false, whether to display the fit
659 lal.REAL8FrequencySeries, the PSD fitted to a polynomial
662 minsample = int(f_low // psd.deltaF)
663 maxsample = int(f_high // psd.deltaF)
666 f = numpy.arange(maxsample - minsample) * psd.deltaF + 1
667 data = psd.data.data[minsample:maxsample]
669 logf = numpy.linspace(numpy.log(f[0]), numpy.log(f[-1]), 100000)
670 interp = interpolate.interp1d(numpy.log(f), numpy.log(data))
672 p = numpy.poly1d(numpy.polyfit(logf, data, order))
674 print(
"\nFit polynomial is: \n\nlog(PSD) = \n", p,
"\n\nwhere x = f / f_min\n", file=sys.stderr)
675 data = numpy.exp(p(numpy.log(f)))
676 olddata = psd.data.data
677 olddata[minsample:maxsample] = data
678 psd = lal.CreateREAL8FrequencySeries(
683 sampleUnits = psd.sampleUnits,
684 length = len(olddata)
686 psd.data.data = olddata
690def harmonic_mean(psddict: Dict[str, lal.REAL8FrequencySeries]) -> lal.REAL8FrequencySeries:
691 """Take the harmonic mean of a dictionary of PSDs.
694 refpsd = list(psddict.values())[0]
695 psd = lal.CreateREAL8FrequencySeries(
"psd", refpsd.epoch, 0., refpsd.deltaF, lal.Unit(
"strain^2 s"), refpsd.data.length)
696 psd.data.data[:] = 0.
698 psd.data.data[:] += 1. / psddict[ifo].data.data
699 psd.data.data[:] = len(psddict) / psd.data.data[:]
704 def __init__(self, f_min, f_max, delta_f, m1, m2, spin1 = (0., 0., 0.), spin2 = (0., 0., 0.), eccentricity = 0., inclination = 0., approximant =
"IMRPhenomD"):
706 Configures the horizon distance calculation for a specific
707 waveform model. The waveform is pre-computed and stored,
708 so this initialization step can be time-consuming but
709 computing horizon distances from measured PSDs will be
712 The waveform model's spectrum parameters, for example its
713 Nyquist and frequency resolution, need not match the
714 parameters for the PSDs that will ultimately be supplied
715 but there are some advantages to be had in getting them to
716 match. For example, computing the waveform with a smaller
717 delta_f than will be needed will require additional storage
718 and consume additional CPU time for the initialization,
719 while computing it with too low an f_max or too large a
720 delta_f might lead to inaccurate horizon distance
723 f_min (Hertz) sets the frequency at which the waveform
726 f_max (Hertz) sets the frequency upto which the waveform's
729 delta_f (Hertz) sets the frequency resolution of the
730 desired waveform model.
732 m1, m2 (solar masses) set the component masses of the
735 spin1, spin2 (3-component vectors, geometric units) set the
736 spins of the component masses.
738 eccentricity [0, 1) sets the eccentricity of the system.
740 inclination (radians) sets the orbital inclination of the
745 >>> # configure for non-spinning, circular, 1.4+1.4 BNS
746 >>> horizon_distance = HorizonDistance(10., 1024., 1./32., 1.4, 1.4)
747 >>> # populate a PSD for testing
748 >>> import lal, lalsimulation
749 >>> psd = lal.CreateREAL8FrequencySeries("psd", lal.LIGOTimeGPS(0), 0., 1./32., lal.Unit("strain^2 s"), horizon_distance.model.data.length)
750 >>> lalsimulation.SimNoisePSDaLIGODesignSensitivityP1200087(psd, 0.)
752 >>> # compute horizon distance
753 >>> D, (f, model) = horizon_distance(psd)
754 >>> print("%.4g Mpc" % D)
756 >>> # compute distance and spectrum for SNR = 25
757 >>> D, (f, model) = horizon_distance(psd, 25.)
761 array([ 10. , 10.03125, 10.0625 , ..., 1023.9375 ,
764 array([ 8.05622865e-45, 7.99763234e-45, 7.93964216e-45, ...,
765 1.11824864e-49, 1.11815656e-49, 1.11806450e-49])
769 - Currently the SEOBNRv4_ROM waveform model is used, so its
770 limitations with respect to masses, spins, etc., apply.
771 The choice of waveform model is subject to change.
777 self.
spin1 = numpy.array(spin1)
778 self.
spin2 = numpy.array(spin2)
786 hp, hc = lalsimulation.SimInspiralFD(
787 m1 * lal.MSUN_SI, m2 * lal.MSUN_SI,
788 spin1[0], spin1[1], spin1[2],
789 spin2[0], spin2[1], spin2[2],
801 lalsimulation.GetApproximantFromString(self.
approximant)
803 assert hp.data.length > 0,
"huh!? h+ has zero length!"
810 self.
model = lal.CreateREAL8FrequencySeries(
811 name =
"signal spectrum",
812 epoch = LIGOTimeGPS(0),
815 sampleUnits = hp.sampleUnits * hp.sampleUnits,
816 length = hp.data.length
818 self.
model.data.data[:] = numpy.abs(hp.data.data)**2.
823 Compute the horizon distance for the configured waveform
824 model given the PSD and the SNR at which the horizon is
825 defined (default = 8). Equivalently, from a PSD and an
826 observed SNR compute and return the amplitude of the
827 configured waveform's spectrum required to achieve that
830 The return value is a two-element tuple. The first element
831 is the horizon distance in Mpc. The second element is,
832 itself, a two-element tuple containing two vectors giving
833 the frequencies and amplitudes of the waveform model's
834 spectrum scaled so as to have the given SNR. The vectors
835 are clipped to the range of frequencies that were used for
838 The parameters of the PSD, for example its Nyquist and
839 frequency resolution, need not match the parameters of the
840 configured waveform model. In the event of a mismatch, the
841 waveform model is resampled to the frequencies at which the
842 PSD has been measured.
844 The inspiral spectrum returned has the same units as the
845 PSD and is normalized so that the SNR is
847 SNR^2 = \int (inspiral_spectrum / psd) df
849 That is, the ratio of the inspiral spectrum to the PSD
850 gives the spectral density of SNR^2.
856 f = psd.f0 + numpy.arange(psd.data.length) * psd.deltaF
863 indexes = ((f - self.
model.f0) / self.
model.deltaF).round().astype(
"int").clip(0, self.
model.data.length - 1)
864 model = self.
model.data.data[indexes]
870 kmin = (max(psd.f0, self.
model.f0, self.
f_min) - psd.f0) / psd.deltaF
871 kmin = int(round(kmin))
872 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
873 kmax = int(round(kmax)) + 1
874 assert kmin < kmax,
"PSD and waveform model do not intersect"
882 model = model[kmin:kmax]
883 D = math.sqrt(4. * (model / psd.data.data[kmin:kmax]).sum() * psd.deltaF)
902 return D / (1e6 * lal.PC_SI), (f, model)
905def effective_distance_factor(inclination, fp, fc):
907 Returns the ratio of effective distance to physical distance for
908 compact binary mergers. Inclination is the orbital inclination of
909 the system in radians, fp and fc are the F+ and Fx antenna factors.
910 See lal.ComputeDetAMResponse() for a function to compute antenna
911 factors. The effective distance is given by
913 Deff = effective_distance_factor * D
915 See Equation (4.3) of arXiv:0705.1514.
917 cos2i = math.cos(inclination)**2
918 return 1.0 / math.sqrt(fp**2 * (1+cos2i)**2 / 4 + fc**2 * cos2i)
__init__(self, f_min, f_max, delta_f, m1, m2, spin1=(0., 0., 0.), spin2=(0., 0., 0.), eccentricity=0., inclination=0., approximant="IMRPhenomD")
__call__(self, psd, snr=8.)
do_on_message(self, bus, message)