gstlal  1.4.1
plotpsd.py
Go to the documentation of this file.
1 # Copyright (C) 2013 Kipp Cannon
2 # Copyright (C) 2015 Chad Hanna
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 
31 
32 
33 import logging
34 import math
35 import matplotlib
36 from matplotlib import figure
37 from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
38 from matplotlib import ticker
39 import numpy
40 from gstlal import plotutil
41 from glue.ligolw import lsctables
42 from gstlal import reference_psd
43 
44 
45 def summarize_coinc_xmldoc(coinc_xmldoc):
46  coinc_event, = lsctables.CoincTable.get_table(coinc_xmldoc)
47  coinc_inspiral, = lsctables.CoincInspiralTable.get_table(coinc_xmldoc)
48  offset_vector = lsctables.TimeSlideTable.get_table(coinc_xmldoc).as_dict()[coinc_event.time_slide_id] if coinc_event.time_slide_id is not None else None
49  # FIXME: MBTA uploads are missing process table
50  #process, = lsctables.ProcessTable.get_table(coinc_xmldoc)
51  sngl_inspirals = dict((row.ifo, row) for row in lsctables.SnglInspiralTable.get_table(coinc_xmldoc))
52 
53  mass1 = sngl_inspirals.values()[0].mass1
54  mass2 = sngl_inspirals.values()[0].mass2
55  if mass1 < mass2:
56  mass1, mass2 = mass2, mass1
57  end_time = coinc_inspiral.end
58  on_instruments = coinc_inspiral.ifos
59  logging.info("%g Msun -- %g Msun event in %s at %.2f GPS" % (mass1, mass2, ", ".join(sorted(sngl_inspirals)), float(end_time)))
60 
61  return sngl_inspirals, mass1, mass2, end_time, on_instruments
62 
63 
64 def axes_plot_cummulative_snr(axes, psds, coinc_xmldoc):
65  sngl_inspirals, mass1, mass2, end_time, on_instruments = summarize_coinc_xmldoc(coinc_xmldoc)
66 
67  axes.grid(which = "both", linestyle = "-", linewidth = 0.2)
68  axes.minorticks_on()
69 
70  for instrument, sngl_inspiral in sngl_inspirals.items():
71  logging.info("found %s event with SNR %g" % (instrument, sngl_inspirals[instrument].snr))
72 
73  if instrument not in psds:
74  logging.info("no PSD for %s" % instrument)
75  continue
76  psd = psds[instrument]
77  if psd is None:
78  logging.info("no PSD for %s" % instrument)
79  continue
80  psd_data = psd.data.data
81  f = psd.f0 + numpy.arange(len(psd_data)) * psd.deltaF
82  logging.info("found PSD for %s spanning [%g Hz, %g Hz]" % (instrument, f[0], f[-1]))
83 
84  # FIXME: horizon distance stopped at 0.9 max frequency due
85  # to low pass filter messing up the end of the PSD. if we
86  # could figure out the frequency bounds and delta F we
87  # could move this out of the loop for some speed
88  horizon_distance = reference_psd.HorizonDistance(10., 0.9 * f[-1], psd.deltaF, mass1, mass2)
89 
90  # generate inspiral spectrum and clip PSD to its frequency
91  # range
92  inspiral_spectrum_x, inspiral_spectrum_y = horizon_distance(psd, sngl_inspiral.snr)[1]
93  lo = int(round((inspiral_spectrum_x[0] - psd.f0) / psd.deltaF))
94  hi = int(round((inspiral_spectrum_x[-1] - psd.f0) / psd.deltaF)) + 1
95  f = f[lo:hi]
96  psd_data = psd_data[lo:hi]
97 
98  # plot
99  snr2 = (inspiral_spectrum_y / psd_data).cumsum() * psd.deltaF
100  axes.semilogx(f, snr2**.5, color = plotutil.colour_from_instruments([instrument]), alpha = 0.8, linestyle = "-", label = "%s SNR = %.3g" % (instrument, sngl_inspiral.snr))
101 
102  axes.set_ylim([0., axes.get_ylim()[1]])
103 
104  axes.set_title(r"Cumulative SNRs for $%.3g\,\mathrm{M}_{\odot}$--$%.3g\,\mathrm{M}_{\odot}$ Merger Candidate at %.2f GPS" % (mass1, mass2, float(end_time)))
105  axes.set_xlabel(r"Frequency (Hz)")
106  axes.set_ylabel(r"Cumulative SNR")
107  axes.legend(loc = "upper left")
108 
109 
110 def latex_horizon_distance(Mpc):
111  if Mpc >= 256.:
112  # :-O
113  return "%s Gpc" % plotutil.latexnumber("%.4g" % (Mpc * 1e-3))
114  elif Mpc >= 0.25:
115  # :-)
116  return "%s Mpc" % plotutil.latexnumber("%.4g" % Mpc)
117  elif Mpc >= 2**-12:
118  # :-(
119  return "%s kpc" % plotutil.latexnumber("%.4g" % (Mpc * 1e3))
120  else:
121  # X-P
122  return "%s pc" % plotutil.latexnumber("%.4g" % (Mpc * 1e6))
123 
124 
125 def axes_plot_psds(axes, psds, coinc_xmldoc = None):
126  """
127  Places a PSD plot into a matplotlib Axes object.
128 
129  @param axes An Axes object into which the plot will be placed.
130 
131  @param psds A dictionary of PSDs as REAL8FrequencySeries keyed by
132  instrument
133 
134  @param coinc_xmldoc An XML document containing a single event with all
135  of the metadata as would be uploaded to gracedb. This is optional.
136  """
137 
138  if coinc_xmldoc is not None:
139  sngl_inspirals, mass1, mass2, end_time, on_instruments = summarize_coinc_xmldoc(coinc_xmldoc)
140  else:
141  # Use the cannonical BNS binary for horizon distance if an
142  # event wasn't given
143  sngl_inspirals = {}
144  mass1, mass2, end_time = 1.4, 1.4, None
145  on_instruments = set(psds)
146 
147  axes.grid(which = "both", linestyle = "-", linewidth = 0.2)
148  axes.minorticks_on()
149 
150  min_psds, max_psds = [], []
151  min_fs, max_fs = [], []
152  for instrument, psd in sorted(psds.items()):
153  if psd is None:
154  continue
155  psd_data = psd.data.data
156  f = psd.f0 + numpy.arange(len(psd_data)) * psd.deltaF
157  logging.info("found PSD for %s spanning [%g Hz, %g Hz]" % (instrument, f[0], f[-1]))
158  min_fs.append(f[0])
159  max_fs.append(f[-1])
160  # FIXME: horizon distance stopped at 0.9 max frequency due
161  # to low pass filter messing up the end of the PSD. if we
162  # could figure out the frequency bounds and delta F we
163  # could move this out of the loop for some speed
164  horizon_distance = reference_psd.HorizonDistance(10., 0.9 * f[-1], psd.deltaF, mass1, mass2)
165  if instrument in on_instruments:
166  alpha = 0.8
167  linestyle = "-"
168  label = "%s (%s Horizon)" % (instrument, latex_horizon_distance(horizon_distance(psd, 8.)[0]))
169  else:
170  alpha = 0.6
171  linestyle = ":"
172  label = "%s (Off, Last Seen With %s Horizon)" % (instrument, latex_horizon_distance(horizon_distance(psd, 8.)[0]))
173  axes.loglog(f, psd_data, color = plotutil.colour_from_instruments([instrument]), alpha = alpha, linestyle = linestyle, label = label)
174  if instrument in sngl_inspirals:
175  logging.info("found %s event with SNR %g" % (instrument, sngl_inspirals[instrument].snr))
176  inspiral_spectrum = horizon_distance(psd, sngl_inspirals[instrument].snr)[1]
177  axes.loglog(inspiral_spectrum[0], inspiral_spectrum[1], color = plotutil.colour_from_instruments([instrument]), dashes = (5, 2), alpha = 0.8, label = "SNR = %.3g" % sngl_inspirals[instrument].snr)
178  # record the minimum from within the rage 10 Hz -- 900 Hz
179  min_psds.append(psd_data[int((10.0 - psd.f0) / psd.deltaF) : int((900 - psd.f0) / psd.deltaF)].min())
180  # record the maximum from within the rage 1 Hz -- 900 Hz
181  max_psds.append(psd_data[int((1.0 - psd.f0) / psd.deltaF) : int((900 - psd.f0) / psd.deltaF)].max())
182 
183  if min_fs:
184  axes.set_xlim((6.0, max(max_fs)))
185  else:
186  axes.set_xlim((6.0, 3000.0))
187  if min_psds:
188  axes.set_ylim((10.**math.floor(math.log10(min(min_psds) / 3.)), 10.**math.ceil(math.log10(max(max_psds)))))
189 
190  # FIXME: I don't understand how these work
191  axes.yaxis.set_major_locator(ticker.LogLocator(10., subs = (1.0,)))
192  axes.yaxis.set_minor_locator(ticker.LogLocator(10., subs = (0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9)))
193 
194  title = r"Strain Noise Spectral Density for $%.3g\,\mathrm{M}_{\odot}$--$%.3g\,\mathrm{M}_{\odot}$ Merger Candidate" % (mass1, mass2)
195  if end_time is not None:
196  title += r" at %.2f GPS" % float(end_time)
197  axes.set_title(title)
198  axes.set_xlabel(r"Frequency (Hz)")
199  axes.set_ylabel(r"Spectral Density ($\mathrm{strain}^2 / \mathrm{Hz}$)")
200  axes.legend(loc = "upper right")
201 
202 
203 def plot_psds(psds, coinc_xmldoc = None, plot_width = 640):
204  """
205  Produces a matplotlib figure of PSDs.
206 
207  @param psds A dictionary of PSDs as REAL8FrequencySeries keyed by
208  instrument
209 
210  @param coinc_xmldoc An XML document containing a single event with all
211  of the metadata as would be uploaded to gracedb. This is optional.
212 
213  @param plot_width How wide to make the figure object in pixels
214  (ignored if axes is provided).
215  """
216  fig = figure.Figure()
217  FigureCanvas(fig)
218  fig.set_size_inches(plot_width / float(fig.get_dpi()), int(round(plot_width / plotutil.golden_ratio)) / float(fig.get_dpi()))
219  axes_plot_psds(fig.gca(), psds, coinc_xmldoc = coinc_xmldoc)
220  fig.tight_layout(pad = .8)
221  return fig
222 
223 
224 def plot_cumulative_snrs(psds, coinc_xmldoc, plot_width = 640):
225  """
226  Produces a matplotlib figure of cumulative SNRs.
227 
228  @param psds A dictionary of PSDs as REAL8FrequencySeries keyed by
229  instrument
230 
231  @param coinc_xmldoc An XML document containing a single event with all
232  of the metadata as would be uploaded to gracedb.
233 
234  @param plot_width How wide to make the figure object in pixels
235  (ignored if axes is provided).
236  """
237  fig = figure.Figure()
238  FigureCanvas(fig)
239  fig.set_size_inches(plot_width / float(fig.get_dpi()), int(round(plot_width / plotutil.golden_ratio)) / float(fig.get_dpi()))
240  axes_plot_cummulative_snr(fig.gca(), psds, coinc_xmldoc)
241  fig.tight_layout(pad = .8)
242  return fig
def axes_plot_psds(axes, psds, coinc_xmldoc=None)
Definition: plotpsd.py:125
def plot_psds(psds, coinc_xmldoc=None, plot_width=640)
Definition: plotpsd.py:203
def plot_cumulative_snrs(psds, coinc_xmldoc, plot_width=640)
Definition: plotpsd.py:224