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