diff --git a/gstlal-ugly/bin/Makefile.am b/gstlal-ugly/bin/Makefile.am index 28cc95f2474dd3041609a9852924db18344b89f2..5f651c48c9e59f90c0f6eefbc95f5af70aaf2753 100644 --- a/gstlal-ugly/bin/Makefile.am +++ b/gstlal-ugly/bin/Makefile.am @@ -23,6 +23,7 @@ dist_bin_SCRIPTS = \ gstlal_ninja_smooth_reference_psd \ gstlal_median_of_psds \ gstlal_psd_pipe \ + gstlal_plot_channel_psd \ gstlal_segments_operations \ gstlal_recolor_frames_pipe \ gstlal_inj_frames \ diff --git a/gstlal-ugly/bin/gstlal_idq_trigger_gen b/gstlal-ugly/bin/gstlal_idq_trigger_gen index 44d8898eaad2e15a7cdadb41fc549dadcdd5d934..214f3808b5ba5894d8378b0ea4124ce52fb5f864 100755 --- a/gstlal-ugly/bin/gstlal_idq_trigger_gen +++ b/gstlal-ugly/bin/gstlal_idq_trigger_gen @@ -29,6 +29,7 @@ from optparse import OptionParser from collections import deque import os import sys +import socket import resource import StringIO import threading @@ -42,13 +43,20 @@ Gst.init(None) import lal import numpy +from gstlal import pipeio from gstlal import datasource from gstlal import idq_multirate_datasource from gstlal import multichannel_datasource from gstlal import sngltriggertable from gstlal import pipeparts from gstlal import simplehandler +from gstlal import httpinterface +from gstlal import bottle from glue.ligolw import utils as ligolw_utils +from glue import iterutils +from glue.ligolw import ligolw +from glue.ligolw import utils as ligolw_utils +from glue.ligolw.utils import process as ligolw_process from lal import gpstime # @@ -162,6 +170,13 @@ def parse_command_line(): class MultiChannelHandler(simplehandler.Handler): + """ + A subclass of simplehandler.Handler to be used with + multiple channels. + + Implements additional message handling for dealing with spectrum + messages and creates trigger files for use in iDQ. + """ def __init__(self, *args, **kwargs): self.lock = threading.Lock() self.basis_params = kwargs.pop("basis_params") @@ -175,9 +190,37 @@ class MultiChannelHandler(simplehandler.Handler): #self.header = "# %18s\t%20s\t%20s\t%6s\t%8s\t%8s\t%8s\t%10s\t%10s\t%9s\t%8s\t%s\n" % ("start_time", "stop_time", "trigger_time", "phase", "snr", "chisq", "sigmasq", "frequency", "Q", "latency", "rate", "channel") self.header = "# %18s\t%20s\t%20s\t%10s\t%8s\t%8s\t%8s\t%10s\t%s\n" % ("start_time", "stop_time", "trigger_time", "frequency", "phase", "sigmasq", "chisq", "snr", "channel") self.fdata = "" + + # set up bottle routes for spectra by each whitener + self.psds = {} + bottle.route("/psds.xml")(self.web_get_psd_xml) + super(MultiChannelHandler, self).__init__(*args, **kwargs) def do_on_message(self, bus, message): + """! + Handle application-specific message types, + e.g., spectrum messages. + + @param bus A reference to the pipeline's bus + @param message A reference to the incoming message + """ + # + # return value of True tells parent class that we have done + # all that is needed in response to the message, and that + # it should ignore it. a return value of False means the + # parent class should do what it thinks should be done + # + if message.type == Gst.MessageType.ELEMENT: + if message.get_structure().get_name() == "spectrum": + # get the channel name & psd. + instrument, info = message.src.get_name().split("_", 1) + channel, _ = info.rsplit("_", 1) + psd = pipeio.parse_spectrum_message(message) + + # save psd + self.psds[channel] = psd + return True return False def bufhandler(self, elem, sink_dict): @@ -249,6 +292,20 @@ class MultiChannelHandler(simplehandler.Handler): f.write(data) shutil.move(tmpfile, fpath) + def gen_psd_xmldoc(self): + xmldoc = lal.series.make_psd_xmldoc(self.psds) + process = ligolw_process.register_to_xmldoc(xmldoc, "gstlal_idq", {}) + ligolw_process.set_process_end_time(process) + return xmldoc + + def web_get_psd_xml(self): + with self.lock: + output = StringIO.StringIO() + ligolw_utils.write_fileobj(self.gen_psd_xmldoc(), output) + outstr = output.getvalue() + output.close() + return outstr + class LinkedAppSync(pipeparts.AppSync): def __init__(self, appsink_new_buffer, sink_dict = {}): @@ -352,8 +409,10 @@ class LinkedAppSync(pipeparts.AppSync): # #################### - +# # parsing and setting up some core structures +# + options, filenames = parse_command_line() data_source_info = multichannel_datasource.DataSourceInfo(options) @@ -363,7 +422,58 @@ channels = data_source_info.channel_dict.keys() # dictionary of basis parameters keyed by ifo, rate basis_params = {} +# +# create a new, empty, Bottle application and make it the current +# default, then start http server to serve it up +# + +bottle.default_app.push() +# uncomment the next line to show tracebacks when something fails +# in the web server +#bottle.app().catchall = False +import base64, uuid # FIXME: don't import when the uniquification scheme is fixed +httpservers = httpinterface.HTTPServers( + # FIXME: either switch to using avahi's native name + # uniquification system or adopt a naturally unique naming + # scheme (e.g., include a search identifier and job + # number). + service_name = "gstlal_idq (%s)" % base64.urlsafe_b64encode(uuid.uuid4().bytes), + service_properties = {}, + verbose = options.verbose +) + +# +# Set up a registry of the resources that this job provides +# + +@bottle.route("/") +@bottle.route("/index.html") +def index(channel_list = channels): + # get the host and port to report in the links from the + # request we've received so that the URLs contain the IP + # address by which client has contacted us + netloc = bottle.request.urlparts[1] + server_address = "http://%s" % netloc + yield "<html><body>\n<h3>%s %s</h3>\n<p>\n" % (netloc, " ".join(sorted(channel_list))) + for route in sorted(bottle.default_app().routes, key = lambda route: route.rule): + # don't create links back to this page + if route.rule in ("/", "/index.html"): + continue + # only create links for GET methods + if route.method != "GET": + continue + yield "<a href=\"%s%s\">%s</a><br>\n" % (server_address, route.rule, route.rule) + yield "</p>\n</body></html>" +# FIXME: get service-discovery working, then don't do this +open("registry.txt", "w").write("http://%s:%s/\n" % (socket.gethostname(), httpservers[0][0].port)) + +# # building the event loop and pipeline +# + +if options.verbose: + print >>sys.stderr, "assembling pipeline..." + mainloop = GObject.MainLoop() pipeline = Gst.Pipeline(sys.argv[0]) handler = MultiChannelHandler(mainloop, pipeline, basis_params = basis_params, description = options.description, out_path = options.out_path, instrument = instrument) @@ -399,6 +509,9 @@ for channel in channels: thishead = pipeparts.mktrigger(pipeline, thishead, rate, max_snr = True) src[(channel, rate)] = thishead +if options.verbose: + print >>sys.stderr, "done." + if options.verbose: print >>sys.stderr, "attaching appsinks to pipeline..." appsync = LinkedAppSync(appsink_new_buffer = handler.bufhandler) @@ -418,4 +531,37 @@ if data_source_info.data_source not in ("lvshm", "framexmit"):# what about nds o # run if pipeline.set_state(Gst.State.PLAYING) == Gst.StateChangeReturn.FAILURE: raise RuntimeError("pipeline failed to enter PLAYING state") -mainloop.run() +mainloop.run() + +if options.verbose: + print >>sys.stderr, "running pipeline..." + + +# +# Shutdown the web interface servers and garbage collect the Bottle +# app. This should release the references the Bottle app's routes +# hold to the pipeline's data (like template banks and so on). +# + + +del httpservers +bottle.default_app.pop() + + +# +# Set pipeline state to NULL and garbage collect the handler +# + + +if pipeline.set_state(Gst.State.NULL) != Gst.StateChangeReturn.SUCCESS: + raise RuntimeError("pipeline could not be set to NULL") +del handler.pipeline +del handler + +# +# close program manually if data source is live +# + + +if options.data_source in ("lvshm", "framexmit"): + sys.exit(0) diff --git a/gstlal-ugly/bin/gstlal_plot_channel_psd b/gstlal-ugly/bin/gstlal_plot_channel_psd new file mode 100755 index 0000000000000000000000000000000000000000..844ed853a70f3aa21a5eb91840521e1a6df2e9db --- /dev/null +++ b/gstlal-ugly/bin/gstlal_plot_channel_psd @@ -0,0 +1,159 @@ +#!/usr/bin/env python +# +# Copyright (C) 2012-2015 Chad Hanna +# Copyright (C) 2015 Kipp Cannon +# Copyright (C) 2017 Patrick Godwin +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; either version 2 of the License, or (at your +# option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + +from optparse import OptionParser +import os +import sys +import logging +import math +import numpy +import matplotlib +from matplotlib import figure +from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas + +from lal import series +from glue.ligolw import utils as ligolw_utils +from glue.ligolw import lsctables +from gstlal import plotutil + +matplotlib.rcParams.update({ + "font.size": 10.0, + "axes.titlesize": 10.0, + "axes.labelsize": 10.0, + "xtick.labelsize": 8.0, + "ytick.labelsize": 8.0, + "legend.fontsize": 8.0, + "figure.dpi": 300, + "savefig.dpi": 300, + "text.usetex": True, + "path.simplify": True +}) + + + +## @file +# A program to plot reference psds; see gstlal_plot_channel_psd for more info +# +# A program to plot a psd such as one generated by gstlal_reference_psd +# +# ### Usage: +# +# gstlal_plot_channel_psd OUTPUT-NAME PSD-FILE-1 PSD-FILE-2 +# +# e.g., +# +# gstlal_plot_channel_psd psd.png psd.xml.gz +# + +color_scheme = ['#332288', '#88CCEE', '#44AA99', '#117733', '#999933', '#DDCC77', '#CC6677', '#882255', '#AA4499'] + +def parse_command_line(): + parser = OptionParser() + parser.add_option("-o", "--output", help = "Set plot filename (default = replace input file's extension with .png).") + parser.add_option("-v", "--verbose", action = "store_true", help = "Be verbose.") + options, filenames = parser.parse_args() + + if not filenames: + raise ValueError("must supply at least one input filename") + if options.output and len(filenames) > 1: + raise ValueError("must supply only one input file when setting --output") + + return options, filenames + +def plot_psds(psds, plot_width = 640): + """! + Produces a matplotlib figure of PSDs. + + @param psds A dictionary of PSDs as REAL8FrequencySeries keyed by + channel + + @param plot_width How wide to make the plot in pixels + """ + on_channels = set(psds) + fig = figure.Figure() + FigureCanvas(fig) + fig.set_size_inches(plot_width / float(fig.get_dpi()), int(round(plot_width / plotutil.golden_ratio)) / float(fig.get_dpi())) + axes = fig.gca() + axes.grid(which = "both", linestyle = "-", linewidth = 0.2) + + min_psds, max_psds = [], [] + min_fs, max_fs = [], [] + color_index = 0 + for channel, psd in sorted(psds.items()): + if psd is None: + continue + psd_data = psd.data.data + f = psd.f0 + numpy.arange(len(psd_data)) * psd.deltaF + logging.info("found PSD for %s spanning [%g Hz, %g Hz]" % (channel, f[0], f[-1])) + min_fs.append(f[0]) + max_fs.append(f[-1]) + if channel in on_channels: + alpha = 0.8 + linestyle = "-" + else: + alpha = 0.6 + linestyle = ":" + label = r"%s" % channel.replace('_', '\_') + axes.loglog(f, psd_data, alpha = alpha, color = color_scheme[color_index], linestyle = linestyle, label = label) + # record the minimum from within the rage 10 Hz -- 1 kHz + min_psds.append(min([x for x in psd_data[int((10.0 - psd.f0) / psd.deltaF) : int((1000 - psd.f0) / psd.deltaF)] if x > 0])) + # record the maximum from within the rage 1 Hz -- 1 kHz + max_psds.append(psd_data[int((1.0 - psd.f0) / psd.deltaF) : int((1000 - psd.f0) / psd.deltaF)].max()) + color_index += 1 + if min_fs: + axes.set_xlim((6.0, max(max_fs))) + else: + axes.set_xlim((6.0, 3000.0)) + if min_psds: + axes.set_ylim((10**math.floor(math.log10(min(min_psds))), 10**math.ceil(math.log10(max(max_psds))))) + title = r"Noise Spectral Density" + axes.set_title(title) + axes.set_xlabel(r"Frequency (Hz)") + axes.set_ylabel(r"Spectral Density ($\mathrm{1} / \mathrm{Hz}$)") + axes.legend(loc = "upper right") + fig.tight_layout(pad = .8) + + return fig + + +options, filenames = parse_command_line() + +index = 0 +for fname in filenames: + index += 1 + if options.output: + outname = options.output + else: + outname = os.path.join(os.path.splitext(fname)[0], "%d" % index) + + fig = plot_psds( + series.read_psd_xmldoc( + ligolw_utils.load_filename( + fname, + verbose = options.verbose, + contenthandler = series.PSDContentHandler + ) + ), + plot_width = 2400 + ) + + fig.savefig(outname) + diff --git a/gstlal-ugly/python/idq_multirate_datasource.py b/gstlal-ugly/python/idq_multirate_datasource.py index 1daf46de22e12cfa34f998f29703fbc8d7d55b80..9872fb47171c75ae717f908114e45e7dbb119b5a 100644 --- a/gstlal-ugly/python/idq_multirate_datasource.py +++ b/gstlal-ugly/python/idq_multirate_datasource.py @@ -181,7 +181,7 @@ def mkwhitened_multirate_src(pipeline, src, rates, instrument, psd = None, psd_f # reduced to account for the loss of the zero padding to # keep the Hann window the same as implied by the # user-supplied parameters. - whiten = pipeparts.mkwhiten(pipeline, pipeparts.mkqueue(pipeline, head, max_size_time = 2 * psd_fft_length * Gst.SECOND), fft_length = psd_fft_length - 2 * zero_pad, zero_pad = 0, average_samples = 64, median_samples = 7, expand_gaps = True, name = "%s_%s_lal_whiten" % (instrument, channel_name)) + whiten = pipeparts.mkwhiten(pipeline, pipeparts.mkqueue(pipeline, head, max_size_time = 2 * psd_fft_length * Gst.SECOND), fft_length = psd_fft_length - 2 * zero_pad, zero_pad = 0, average_samples = 64, median_samples = 7, expand_gaps = True, name = "%s_%s_lalwhiten" % (instrument, channel_name)) pipeparts.mkfakesink(pipeline, whiten) # high pass filter @@ -222,11 +222,11 @@ def mkwhitened_multirate_src(pipeline, src, rates, instrument, psd = None, psd_f head = pipeparts.mkgate(pipeline, head, control = dqvector, default_state = False, threshold = 1, hold_length = -max(rates), attack_length = -max(rates) * (psd_fft_length + 1)) # Drop initial data to let the PSD settle head = pipeparts.mkdrop(pipeline, head, drop_samples = 16 * psd_fft_length * max(rates)) - head = pipeparts.mkchecktimestamps(pipeline, head, "%s_%s_timestamps_fir" % (instrument, channel_name)) + head = pipeparts.mkchecktimestamps(pipeline, head, "%s_%s_timestampsfir" % (instrument, channel_name)) #head = pipeparts.mknxydumpsinktee(pipeline, head, filename = "after_mkfirbank.txt") else: - head = whiten = pipeparts.mkwhiten(pipeline, head, fft_length = psd_fft_length, zero_pad = zero_pad, average_samples = 64, median_samples = 7, expand_gaps = True, name = "%s_%s_lal_whiten" % (instrument, channel_name)) + head = whiten = pipeparts.mkwhiten(pipeline, head, fft_length = psd_fft_length, zero_pad = zero_pad, average_samples = 64, median_samples = 7, expand_gaps = True, name = "%s_%s_lalwhiten" % (instrument, channel_name)) # make the buffers going downstream smaller, this can # really help with RAM head = pipeparts.mkreblock(pipeline, head, block_duration = block_duration) @@ -274,7 +274,7 @@ def mkwhitened_multirate_src(pipeline, src, rates, instrument, psd = None, psd_f head = pipeparts.mkcapsfilter(pipeline, head, "audio/x-raw, rate=%d, format=%s" % (max(rates), GstAudio.AudioFormat.to_string(GstAudio.AudioFormat.F64))) else: head = pipeparts.mkcapsfilter(pipeline, head, "audio/x-raw, rate=%d, format=%s" % (max(rates), GstAudio.AudioFormat.to_string(GstAudio.AudioFormat.F32))) - head = pipeparts.mkchecktimestamps(pipeline, head, "%s_%s_%d_timestamps__white" % (instrument, channel_name, max(rates))) + head = pipeparts.mkchecktimestamps(pipeline, head, "%s_%s_%d_timestampwhite" % (instrument, channel_name, max(rates))) # # optionally add vetoes @@ -336,7 +336,7 @@ def mkwhitened_multirate_src(pipeline, src, rates, instrument, psd = None, psd_f else: head[rate] = head[max(rates)] head[rate] = pipeparts.mkcapsfilter(pipeline, pipeparts.mkresample(pipeline, head[rate], quality = quality), caps = "audio/x-raw, rate=%d" % rate) - head[rate] = pipeparts.mkchecktimestamps(pipeline, head[rate], "%s_%s_%d_timestamps_white" % (instrument, channel_name, rate)) + head[rate] = pipeparts.mkchecktimestamps(pipeline, head[rate], "%s_%s_%d_timestampswhite" % (instrument, channel_name, rate)) head[rate] = pipeparts.mktee(pipeline, head[rate])