gstlal-inspiral  0.4.2
 All Classes Namespaces Files Functions Variables Pages
gstlal_inspiral_plot_sensitivity
1 #!/usr/bin/python
2 #
3 # Copyright (C) 2012-2014 Stephen Privitera, Kipp Cannon, Chad Hanna, Drew Keppel
4 #
5 # This program is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by the
7 # Free Software Foundation; either version 2 of the License, or (at your
8 # option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
13 # Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License along
16 # with this program; if not, write to the Free Software Foundation, Inc.,
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 
19 ## @file
20 # A program to plot the sensitivity of a gstlal inspiral search, e.g., VT
21 
22 import numpy
23 
24 try:
25  import sqlite3
26 except ImportError:
27  # pre 2.5.x
28  from pysqlite2 import dbapi2 as sqlite3
29 
30 import matplotlib
31 matplotlib.use("agg")
32 from matplotlib import pyplot
33 
34 import os
35 import sys
36 import copy
37 from optparse import OptionParser
38 
39 from gstlal import inspiral_pipe
40 
41 from glue import segments
42 from glue.ligolw import ligolw
43 from glue.ligolw import lsctables
44 from glue.ligolw import dbtables
45 from glue.ligolw import utils as ligolw_utils
46 from glue.ligolw import array as ligolw_array
47 from glue import segmentsUtils
48 from glue.lal import CacheEntry
49 from glue import iterutils
50 
51 import lal
52 
53 from pylal import rate
54 from pylal import imr_utils
55 from pylal import git_version
56 
57 __author__ = "Stephen Privitera <sprivite@caltech.edu>, Chad Hanna <channa@perimeterinstitute.ca>, Kipp Cannon <kipp.cannon@ligo.org>"
58 __version__ = "git id %s" % git_version.id
59 __date__ = git_version.date
60 
61 pyplot.rc('font',**{'family':'serif','serif':['Computer Modern Roman']})
62 matplotlib.rcParams.update({"text.usetex": True})
63 
64 
65 def chirp_mass(m1,m2):
66  m1 = numpy.array(m1)
67  m2 = numpy.array(m2)
68  mu = (m1*m2)/(m1+m2)
69  mtotal = m1+m2
70  return mu**(3./5) *mtotal**(2./5)
71 
72 class upper_limit(object):
73  """
74  The upper_limit class organizes the calculation of the sensitive search volume
75  for a search described by the input database.
76  """
77  def __init__(self, opts):
78  ## Instance variables ######################################
79  self.opts = opts
80  self.instruments = []
81  self.segments = segments.segmentlistdict()
82  self.zero_lag_segments = segments.segmentlistdict()
83  self.start_time = None
84  self.end_time = None
85  ############################################################
86 
87  # read the zero lag databases first
88  for f in self.opts.zero_lag_database:
89  if opts.verbose:
90  print >> sys.stdout, "\nGathering search data from zero-lag database: %s...." % (f,)
91  working_filename = dbtables.get_connection_filename(f, tmp_path=opts.tmp_space, verbose = opts.verbose)
92  connection = sqlite3.connect(working_filename)
93 
94  # find out which instruments were on and when
95  self.get_on_instruments(connection)
96  self.get_segments(connection) #get single ifo segments with vetoes applied
97 
98  # done with zl database
99  connection.commit()
100  dbtables.discard_connection_filename(f, working_filename, verbose = self.opts.verbose)
101 
102  # derived quantities
103  self.get_zero_lag_segments() #make coincident ifo segments from single ifo segments
104  self.get_livetime() #get the livetime for the chosen set of instruments
105 
106  # open up injection databases
107  self.found_injections_fars = {}
108  self.found_injections_snrs = {}
109  self.total_injections = {}
110  for f in self.opts.injection_database:
111  if opts.verbose:
112  print "Reading results of injection analysis from %s ..."%f
113  working_filename = dbtables.get_connection_filename(f, tmp_path=opts.tmp_space, verbose = opts.verbose)
114  connection = sqlite3.connect(working_filename)
115  for inst in self.instruments:
116  self.found_injections_fars.setdefault(inst,[])
117  self.found_injections_snrs.setdefault(inst,[])
118  self.total_injections.setdefault(inst,[])
119 
120  # get sim-far relationships
121  found, total, missed = imr_utils.get_min_far_inspiral_injections(connection, segments = self.zero_lag_segments[inst], table_name="coinc_inspiral:table")
122  self.found_injections_fars[inst] += found
123  self.total_injections[inst] += total
124 
125  # get sim-snr relationships
126  found, total, missed = imr_utils.get_max_snr_inspiral_injections(connection, segments = self.zero_lag_segments[inst], table_name="coinc_inspiral:table")
127  self.found_injections_snrs[inst] += found
128 
129  dbtables.discard_connection_filename(f, working_filename, verbose = self.opts.verbose)
130 
131  # remove injections that violate user-imposed constraints and
132  # symmetrize between m1-m2
133  for instr in self.instruments:
134  self.total_injections[instr] = imr_utils.symmetrize_sims(self.total_injections[instr], "mass1", "mass2")
135  self.found_injections_fars[instr] = self.filter_injections(self.found_injections_fars[instr])
136  self.found_injections_snrs[instr] = self.filter_injections(self.found_injections_snrs[instr])
137 
138 
139  def get_on_instruments(self,connection):
140  '''
141  Retrieve the sets of instruments which were on during the search.
142  '''
143  for inst in connection.cursor().execute("""SELECT DISTINCT(ifos) FROM search_summary"""):
144  inst = frozenset(lsctables.instrument_set_from_ifos(inst[0]))
145  if not inst in self.instruments:
146  self.instruments.append(inst)
147 
148  return self.instruments
149 
150 
151  def get_segments(self,connection):
152  '''
153  Retrieve raw single IFO segments from the database and
154  subtract vetoes.
155  '''
156  #
157  # get start and end times of the analysis, used for output
158  # naming. these may extend beyond the actual analyzed
159  # segments because of vetoes.
160  #
161  start_time = int( connection.cursor().execute('SELECT MIN(start_time) FROM segment').fetchone()[0] )
162  end_time = int( connection.cursor().execute('SELECT MAX(end_time) FROM segment').fetchone()[0] )
163  if not self.start_time or start_time < self.start_time:
164  self.start_time = start_time
165  if not self.end_time or end_time < self.end_time:
166  self.end_time = end_time
167 
168 
169  # get single ifos from ifo sets
170  ifos = set()
171  for inst in self.instruments:
172  ifos |= inst
173 
174  # retrieve single-ifo segments
175  segs = imr_utils.get_segments( connection, dbtables.get_xml(connection), "%s:table" % opts.coinc_table_name, opts.live_time_program, opts.veto_segments_name, opts.data_segments_name)
176 
177  for ifo in ifos:
178  self.segments.setdefault(ifo,segments.segmentlist())
179  self.segments[ifo] |= segs[ifo]
180 
181  return self.segments
182 
183 
184  def get_zero_lag_segments(self):
185  '''
186  Compute multi-ifo (coincident) segment list from single ifo segments.
187  '''
188  if self.opts.verbose:
189  print >>sys.stdout,"\nForming coincident segments from single IFO segments..."
190 
191  for inst in self.instruments[:]:
192  # intersect single ifo segments
193  self.zero_lag_segments.setdefault(inst,segments.segmentlist())
194  self.segments.union(set(self.segments.keys()) - inst)
195  self.zero_lag_segments[inst] = self.segments.intersection(inst) - self.segments.union(set(self.segments.keys()) - inst)
196  if self.opts.verbose:
197  print >>sys.stdout,"\t%s were on for %d seconds (after vetoes, including playground)" % (','.join(sorted(list(inst))),abs(self.zero_lag_segments[inst]))
198 
199  # subtract playground segments
200  if not self.opts.include_play:
201  self.zero_lag_segments[inst] -= segmentsUtils.S2playground(self.zero_lag_segments[inst].extent())
202  if self.opts.verbose:
203  print >>sys.stdout,"\t%s were on for %d seconds (after vetoes, excluding playground)" % (','.join(sorted(list(inst))),abs(self.zero_lag_segments[inst]))
204 
205  # remove instrument sets that were never on
206  if abs(self.zero_lag_segments[inst]) == 0:
207  print >>sys.stderr, "No livetime for in %s observation time. Skipping..."%("".join(sorted(list(inst))))
208  self.instruments.remove(inst)
209 
210  return self.zero_lag_segments
211 
212 
213  def get_livetime(self):
214  '''Compute the instrument livetimes from the search segments.'''
215  self.livetime = {}
216  for inst in self.zero_lag_segments.keys():
217  self.livetime[inst] = float(abs(self.zero_lag_segments[inst]))/lal.YRJUL_SI
218 
219  return True
220 
221 
222  def set_bins(self, bin_type, inst):
223  if bin_type == "Total_Mass":
224  self.bins = imr_utils.guess_distance_total_mass_bins_from_sims(self.total_injections[inst], nbins = opts.mass_bins, distbins = opts.dist_bins)
225  self.sim_to_bins = imr_utils.sim_to_distance_total_mass_bins_function
226  if bin_type == "Chirp_Mass":
227  self.bins = imr_utils.guess_distance_chirp_mass_bins_from_sims(self.total_injections[inst], mbins = opts.mass_bins, distbins = opts.dist_bins)
228  self.sim_to_bins = imr_utils.sim_to_distance_chirp_mass_bins_function
229  if bin_type == "Mass1_Mass2":
230  self.bins = imr_utils.guess_distance_mass1_mass2_bins_from_sims(self.total_injections[inst], mass1bins = opts.mass_bins, mass2bins = opts.mass_bins, distbins = opts.dist_bins)
231  self.sim_to_bins = imr_utils.sim_to_distance_mass1_mass2_bins_function
232  if bin_type == "Aligned_Spin":
233  self.bins = imr_utils.guess_distance_effective_spin_parameter_bins_from_sims(self.total_injections[inst], chibins = opts.spin_bins, distbins = opts.dist_bins)
234  self.sim_to_bins = imr_utils.sim_to_distance_effective_spin_parameter_bins_function
235  if bin_type == "Mass_Ratio":
236  self.bins = imr_utils.guess_distance_mass_ratio_bins_from_sims(self.total_injections[inst], qbins = opts.mass_bins, distbins = opts.dist_bins)
237  self.sim_to_bins = imr_utils.sim_to_distance_mass_ratio_bins_function
238 
239  return self.bins, self.sim_to_bins
240 
241 
242  def filter_injections(self, sims):
243  '''
244  Remove injections from those found in the database based on
245  user-imposed restrictions.
246  '''
247  for inst in self.instruments:
248  newinjs = []
249  for far, sim in sims:
250  # exclude simulations from certain waveform families
251  if sim.waveform in self.opts.exclude_sim_type:
252  continue
253 
254  if not self.opts.min_mtotal < sim.mass1 + sim.mass2 < self.opts.max_mtotal:
255  continue
256 
257  if self.opts.max_mass_ratio and not (1.0/self.opts.max_mass_ratio <= sim.mass1/sim.mass2 <= self.opts.max_mass_ratio):
258  continue
259 
260  # symmetrize in m1-m2
261  if sim.mass1 > sim.mass2:
262  m1 = sim.mass1
263  m2 = sim.mass2
264  sim.mass1 = m2
265  sim.mass2 = m1
266 
267  # passed all constraint tests, we can use it
268  newinjs.append((far,sim))
269 
270  return newinjs
271 
272 
273 def parse_command_line():
274 
275  description = '''
276  description:
277 
278 The program gstlal_inspiral_plot_sensitivity computes the sensitive volume of a CBC search from input databases containing triggers from simulation experiments. These triggers need to be ranked by false alarm rate, the detection statistic used in S6 searches. Then injections which register a trigger louder than the loudest event, by false alarm rate, are considered found. All others are considered missed. The efficiency of detecting an event depends on the source parameters, such as its component masses, distance, spin, inclination, sky position, etc. However, lalapps_cbc_svim only considers the dependency of the efficiency on distance and mass, marginalizing over the other parameters. Injections are binned in distance and mass and the estimated efficiency is integrated over distance to convert the efficiency into a physical volume.
279 '''
280 
281  parser = OptionParser(version = git_version.verbose_msg, usage = description)
282 
283  # FAR range and resolution
284  parser.add_option("--xaxis-points", default = 50, type = "int", help = "Specify the number of FARs/SNRs for which to compute the search volume.")
285  parser.add_option("--min-far", default = 1.0e-6/lal.YRJUL_SI, type = "float", help = "Specify the minimum FAR (Hz)") # one per million years is probably detection worthy
286  parser.add_option("--max-far", default = 12.0/lal.YRJUL_SI, type = "float", help = "Specify the maximum FAR (Hz)") # one per month is possibly EM-followup worthy
287  parser.add_option("--min-snr", default = 7, type = "float", help = "Specify the minimum SNR for volume calculation.")
288  parser.add_option("--max-snr", default = 15, type = "float", help = "Specify the maximum SNR for volume calculation.")
289 
290  # Input data options
291  parser.add_option("--include-play", default = False, action = "store_true", help = "Include playground data in computing the livetime and volume.")
292  parser.add_option("--zero-lag-database", default = [], action = "append", help = "Name of database containing the zero lag segments and triggers.")
293  parser.add_option("--injection-database", default = [], action = "append", help = "Name of database containing injection parameters and triggers.")
294  parser.add_option("--live-time-program", default = "gstlal_inspiral", metavar = "name", help = "Set the name of the program whose rings will be extracted from the search_summary table. Default = \"gstlal_inspiral\".")
295  parser.add_option("--veto-segments-name", default = "vetoes", help = "Set the name of the veto segments to use from the XML document.")
296  parser.add_option("--data-segments-name", default = "whitehtsegments", help = "Set the name of the data segments. Default = \"whitehtsegments\".")
297  parser.add_option("--coinc-table-name", default = "coinc_inspiral", metavar = "name", help = "Set the name of the table containing coincident triggers. Default = \"coinc_inspiral\".")
298 
299  # Output data options
300  parser.add_option("--user-tag", default = "SEARCH", metavar = "name", help = "Add a descriptive tag to the names of output files.")
301  parser.add_option("--output-dir", default = "./", metavar = "name", help = "Select a directory to place output files.")
302  parser.add_option("-t", "--tmp-space", metavar = "path", help = "Path to a directory suitable for use as a work area while manipulating the database file. The database file will be worked on in this directory, and then moved to the final location when complete. This option is intended to improve performance when running in a networked environment, where there might be a local disk with higher bandwidth than is available to the filesystem on which the final output will reside.")
303  parser.add_option("--output-path", default = "./", action = "store", help="Choose directory to save output files.")
304  parser.add_option("--output-cache", default = None, help = "Name of output cache file. If not specified, then no cache file will be written.")
305  parser.add_option("--verbose", action = "store_true", help = "Be verbose.")
306 
307  #
308  # Binning options
309  #
310  parser.add_option("--mass-bins", default = 4, metavar = "integer", type = "int", help = "Number of mass bins (per dimension), if mass bin boundaries are not explicitly set.")
311  parser.add_option("--spin-bins", default = 4, metavar = "integer", type = "int", help = "Number of spin bins (per dimension), if spin bin boundaries are not explicitly set.")
312  parser.add_option("--dist-bins", default = 15, metavar = "integer", type = "int", help = "Space distance bins evenly and specify the number of distance bins to use.")
313 
314  # injection filters
315  parser.add_option("--min-mtotal", metavar = "m", type = 'float', default = 2.0, help = "Specify the minimum total mass to consider among the injections found in the DB. This filters all injections outside this total mass range.")
316  parser.add_option("--max-mtotal", metavar = "m", type = 'float', default = 100.0, help = "Specify the maximum total mass to consider among the injections found in the DB. This filters all injections outside this total mass range.")
317  parser.add_option("--min-mass", metavar = "MM", type = 'float', default = 1.0, help = "Specify the minimum component mass to consider among the injections found in the DB. This filters all injectionswith any component lighter than MM.")
318  parser.add_option("--max-mass", metavar = "MM", type = 'float', default = 99.0, help = "Specify the maximum mass to consider among the injections found in the DB. This filters all injections with any component heavier than MM.")
319  parser.add_option("--max-mass-ratio", metavar = "q", type = 'float', default = None, help = "Specify the maximum allowed mass ratio. Should be >= 1.")
320  parser.add_option("--exclude-sim-type", default = [], action = "append", metavar = "SIM", help = "When computing the search volume, exclude injections made using the SIM waveform family. Example: SpinTaylorthreePointFivePN. Use this option multiple times to exclude more than one injection type.")
321 
322  # more options
323  # Bin injections in mass by m1-m2
324  parser.add_option("--bin-by-mass1-mass2", default = False, action = "store_true", help = "Bin injections by component mass in two dimensions when estimating the search efficiency.")
325 
326  # Bin injections by total mass
327  parser.add_option("--bin-by-total-mass", default = False, action = "store_true", help = "Bin injections by total mass when estimating the search efficiency.")
328 
329  # Bin injections by chirp mass
330  parser.add_option("--bin-by-chirp-mass", default = False, action = "store_true", help = "Bin injections by chirp mass when estimating the search efficiency.")
331 
332  # aligned spin
333  parser.add_option("--bin-by-aligned-spin", default = False, action = "store_true", help = "Bin injections by aligned spin parameter when estimating the search efficiency.")
334 
335  # aligned spin
336  parser.add_option("--bin-by-mass-ratio", default = False, action = "store_true", help = "Bin injections by mass ratio when estimating the search efficiency.")
337 
338  opts, filenames = parser.parse_args()
339  opts.injection_database.extend(filenames)
340 
341  opts.bin_types = []
342  if opts.bin_by_total_mass:
343  opts.bin_types.append("Total_Mass")
344  if opts.bin_by_chirp_mass:
345  opts.bin_types.append("Chirp_Mass")
346  if opts.bin_by_mass1_mass2:
347  opts.bin_types.append("Mass1_Mass2")
348  if opts.bin_by_aligned_spin:
349  opts.bin_types.append("Aligned_Spin")
350  if opts.bin_by_mass_ratio:
351  opts.bin_types.append("Mass_Ratio")
352 
353  if opts.max_mass_ratio and (opts.max_mass_ratio < 1):
354  raise ValueError, "The maximum mass ratio must be >=1!"
355 
356  return opts, filenames
357 
358 
359 ############################ MAIN PROGRAM #####################################
360 ###############################################################################
361 ###############################################################################
362 
363 #create an empty cache which will store the output files/metadata
364 cache_list = []
365 
366 
367 def write_cache(cache_list, fileout):
368  # write cache file
369  if opts.output_cache is not None:
370  fd = open( fileout, "w" )
371  for l in cache_list:
372  fd.write( str(l) + "\n")
373  fd.close()
374  return
375 
376 #
377 # MAIN
378 #
379 
380 
381 # read in command line opts
382 opts, database = parse_command_line()
383 
384 # read in data from input database, store in upper limit object
385 if opts.verbose: print "\n\nSetting up the search volume calculations...\n"
386 UL = upper_limit(opts)
387 
388 # get the range of xaxis values to use
389 fars = numpy.logspace(numpy.log10(opts.min_far), numpy.log10(opts.max_far), opts.xaxis_points)
390 snrs = numpy.logspace(numpy.log10(opts.min_snr), numpy.log10(opts.max_snr), opts.xaxis_points)
391 
392 
393 #
394 # loop over the requested instruments and mass bin types, compute the
395 # search volume as a function of FAR and SNR threshold
396 #
397 for bin_type in opts.bin_types:
398 
399  for instr in UL.instruments:
400 
401  # check for empty injection sets
402  if not UL.total_injections[instr]:
403  print >> sys.stderr, "No injections performed in %s time. Skipping..." % "".join(sorted(list(instr)))
404  continue
405 
406  if opts.verbose:
407  print "\n\nComputing sensitive volume for %s observation time binning by %s...\n" % ("".join(sorted(list(instr))),bin_type)
408 
409  bins, s2b = UL.set_bins(bin_type,instr)
410 
411  #
412  # prepare output XML file. record mass bins, fars, snrs and
413  # livetime
414  #
415  xmldoc = ligolw.Document()
416  child = xmldoc.appendChild(ligolw.LIGO_LW())
417 
418  # write out mass bins
419  for j in range(len(bins[1:])):
420  xml = ligolw.LIGO_LW({u"Name": u"mass%d_bins:%s" % (j+1,bin_type)})
421  edges = tuple( numpy.concatenate((l,u[-1:])) for l,u in zip(bins.lower(), bins.upper()) )
422  xml.appendChild(ligolw_array.from_array(u"array", edges[j+1]))
423  child.appendChild(xml)
424 
425  # write out fars/snrs/livetime
426  xml = ligolw.LIGO_LW({u"Name": u"far_bins:%s" % (bin_type,)})
427  xml.appendChild(ligolw_array.from_array(u"array", fars))
428  child.appendChild(xml)
429 
430  xml = ligolw.LIGO_LW({u"Name": u"snr_bins:%s" % (bin_type,)})
431  xml.appendChild(ligolw_array.from_array(u"array", snrs))
432  child.appendChild(xml)
433 
434  xml = ligolw.LIGO_LW({u"Name": u"livetime:%s" % (bin_type,)})
435  xml.appendChild(ligolw_array.from_array(u"array", numpy.array([UL.livetime[instr]])))
436  child.appendChild(xml)
437 
438  #
439  # compute volume by far and snr for all mass bins
440  #
441  vols_far, errs_far, vols_snr, errs_snr = [], [], [], []
442  for k, far, snr in zip(range(opts.xaxis_points), fars, snrs):
443 
444  #
445  # get found/missed injections
446  #
447  found_by_far = [s for f, s in UL.found_injections_fars[instr] if f < far]
448  found_by_snr = [s for rho, s in UL.found_injections_snrs[instr] if rho > snr]
449 
450  #
451  # compute volume vs far
452  #
453  vol, err = imr_utils.compute_search_volume_in_bins(found_by_far, UL.total_injections[instr], bins, s2b)
454  vol.array *= UL.livetime[instr] #preferred unit is Mpc^3*yr
455  err.array *= UL.livetime[instr] #preferred unit is Mpc^3*yr
456  vols_far.append(vol)
457  errs_far.append(err)
458 
459  #
460  # write volume and volume errors array to xml
461  #
462  xml = ligolw.LIGO_LW({u"Name": u"volume_by_far_%d:%s" % (k, bin_type,)})
463  xml.appendChild(ligolw_array.from_array(u"array", vol.array))
464  child.appendChild(xml)
465 
466  xml = ligolw.LIGO_LW({u"Name": u"volume_error_by_far_%d:%s" % (k, bin_type,)})
467  xml.appendChild(ligolw_array.from_array(u"array", err.array))
468  child.appendChild(xml)
469 
470  #
471  # compute volume vs snr
472  #
473  vol, err = imr_utils.compute_search_volume_in_bins(found_by_snr, UL.total_injections[instr], bins, s2b)
474  vol.array *= UL.livetime[instr] #preferred unit is Mpc^3*yr
475  err.array *= UL.livetime[instr] #preferred unit is Mpc^3*yr
476  vols_snr.append(vol)
477  errs_snr.append(err)
478 
479  #
480  # write volume and volume errors array to xml
481  #
482  xml = ligolw.LIGO_LW({u"Name": u"volume_by_snr_%d:%s" % (k, bin_type,)})
483  xml.appendChild(ligolw_array.from_array(u"array", vol.array))
484  child.appendChild(xml)
485 
486  xml = ligolw.LIGO_LW({u"Name": u"volume_error_by_snr_%d:%s" % (k, bin_type,)})
487  xml.appendChild(ligolw_array.from_array(u"array", err.array))
488  child.appendChild(xml)
489 
490  #
491  # finish xml document
492  #
493  output_filename = "%s-SEARCH_VOLUME_BINNED_BY_%s_%s-%d-%d.xml" % ("".join(sorted(list(instr))), bin_type.upper(), opts.user_tag, UL.start_time, UL.end_time-UL.start_time)
494  ligolw_utils.write_filename(xmldoc, output_filename)
495  cache_list.append( CacheEntry( "".join(sorted(list(instr))), bin_type,segments.segment(UL.start_time, UL.end_time), "file://localhost%s/%s" % (os.getcwd(), output_filename)) )
496 
497  #
498  # set up figures for plots FIXME: since we are now writing the
499  # data to disk, it may make more sense to carry out the
500  # plotting in a separate code
501  #
502  fig_far_vol, fig_far_range, fig_snr_vol, fig_snr_range = pyplot.figure(), pyplot.figure(), pyplot.figure(), pyplot.figure()
503  figs = [fig_far_vol, fig_far_range, fig_snr_vol, fig_snr_range]
504 
505  # plot the volume/range versus far/snr for each bin
506  mbins = rate.NDBins(bins[1:])
507  labels = []
508  for mlo, mmid, mhi in zip(iterutils.MultiIter(*mbins.lower()),
509  iterutils.MultiIter(*mbins.centres()),
510  iterutils.MultiIter(*mbins.upper())):
511 
512  # plot labels
513  if bin_type == "Aligned_Spin":
514  label = "$\chi \in [%.2f, %.2f]$" % (mlo[0], mhi[0])
515  if bin_type == "Mass_Ratio":
516  label = "$m_1/m_2 \in [%.2f, %.2f]$" % (mlo[0], mhi[0])
517  if bin_type == "Total_Mass":
518  label = "$M_{total} \in [%.2f, %.2f] M_\odot$" % (mlo[0], mhi[0])
519  if bin_type == "Chirp_Mass":
520  label = "$M_{chirp} \in [%.2f, %.2f] M_\odot$" % (mlo[0], mhi[0])
521  if bin_type == "Mass1_Mass2":
522  if mmid[0] > mmid[1]: # symmetrized sims have m1 < m2
523  continue
524  label = "$m_1 \in [%.2f, %.2f], m_2 \in [%.2f, %.2f] M_\odot$" % (mlo[0], mhi[0], mlo[1], mhi[1])
525  labels.append(label)
526 
527  # volume plots
528  fig_far_vol.gca().errorbar( fars, [v[mmid] for v in vols_far], yerr = [e[mmid] for e in errs_far], label=label )
529  fig_snr_vol.gca().errorbar( snrs, [v[mmid] for v in vols_snr], yerr = [e[mmid] for e in errs_snr], label=label )
530 
531  # range plots
532  dist = [ rate.BinnedArray(mbins,array = (v.array/UL.livetime[instr]/(4*numpy.pi/3))**(1./3) ) for v in vols_far ]
533  derr = [ rate.BinnedArray(mbins,array = (d.array/3)*(e.array/v.array) ) for e,v,d in zip(errs_far, vols_far, dist) ]
534  fig_far_range.gca().errorbar( fars, [d[mmid] for d in dist], yerr = [e[mmid] for e in derr], label=label )
535 
536  dist = [ rate.BinnedArray(mbins,array = (v.array/UL.livetime[instr]/(4*numpy.pi/3))**(1./3) ) for v in vols_snr ]
537  derr = [ rate.BinnedArray(mbins,array = (d.array/3)*(e.array/v.array) ) for e,v,d in zip(errs_snr, vols_snr, dist) ]
538  fig_snr_range.gca().errorbar( snrs, [d[mmid] for d in dist], yerr = [e[mmid] for e in derr], label=label )
539 
540  # customize axes
541  ax = fig_far_vol.gca()
542  ax.set_xlabel("Combined FAR (Hz)")
543  ax.set_ylabel("VT (Mpc$^3$ yr)")
544  ax.set_xscale("log")
545  ax.set_xlim([min(fars), max(fars)])
546  ax.invert_xaxis()
547  ax.set_ylim(ymin=0)
548 
549  ax = fig_snr_vol.gca()
550  ax.set_xlabel("Network SNR")
551  ax.set_ylabel("VT (Mpc$^3$ yr)")
552  ax.set_xlim([min(snrs), max(snrs)])
553  ax.set_ylim(ymin=0)
554 
555  ax = fig_far_range.gca()
556  ax.set_xlabel("Combined FAR (Hz)")
557  ax.set_ylabel("Range (Mpc)")
558  ax.set_xscale("log")
559  ax.set_xlim([min(fars), max(fars)])
560  ax.invert_xaxis()
561  ax.set_ylim(ymin=0)
562 
563  ax = fig_snr_range.gca()
564  ax.set_xlabel("Network SNR")
565  ax.set_ylabel("Range (Mpc)")
566  ax.set_xlim([min(snrs), max(snrs)])
567  ax.set_ylim(ymin=0)
568 
569  # common to all figures
570  for fig in figs:
571  ax = fig.gca()
572  ax.grid()
573  ax.legend(loc="upper right")
574  ax.set_title("Search Sensitivity in %s Time" % "".join(sorted(list(instr))))
575 
576  # save and close figures
577  tag = inspiral_pipe.T050017_filename(instr, "GSTLAL_INSPIRAL_PLOT_SENSITIVITY_%s_VOLUME_VS_FAR_BINNED_BY_%s" % (opts.user_tag, bin_type.upper()), UL.start_time, UL.end_time, ".png", path = opts.output_dir)
578  fig_far_vol.savefig(tag)
579 
580  tag = inspiral_pipe.T050017_filename(instr, "GSTLAL_INSPIRAL_PLOT_SENSITIVITY_%s_VOLUME_VS_SNR_BINNED_BY_%s" % (opts.user_tag, bin_type.upper()), UL.start_time, UL.end_time, ".png", path = opts.output_dir)
581  fig_snr_vol.savefig(tag)
582 
583  tag = inspiral_pipe.T050017_filename(instr, "GSTLAL_INSPIRAL_PLOT_SENSITIVITY_%s_RANGE_VS_FAR_BINNED_BY_%s" % (opts.user_tag, bin_type.upper()), UL.start_time, UL.end_time, ".png", path = opts.output_dir)
584  fig_far_range.savefig(tag)
585 
586  tag = inspiral_pipe.T050017_filename(instr, "GSTLAL_INSPIRAL_PLOT_SENSITIVITY_%s_RANGE_VS_SNR_BINNED_BY_%s" % (opts.user_tag, bin_type.upper()), UL.start_time, UL.end_time, ".png", path = opts.output_dir)
587  fig_snr_range.savefig(tag)
588 
589  for fig in figs:
590  pyplot.close(fig)
591 
592 # write a cache file describing the files generated during by this program
593 if opts.output_cache:
594  write_cache(cache_list, opts.output_cache)