diff --git a/.travis.yml b/.travis.yml index 4e3348832..2782a764c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -51,6 +51,7 @@ script: - .travis/test-exe.sh gwdetchar-software-saturations --help - .travis/test-exe.sh gwdetchar-scattering --help - .travis/test-exe.sh gwdetchar-overflow --help + - .travis/test-exe.sh gwdetchar-omega --help after_success: - coveralls diff --git a/bin/gwdetchar-omega b/bin/gwdetchar-omega new file mode 100644 index 000000000..7ba51df9d --- /dev/null +++ b/bin/gwdetchar-omega @@ -0,0 +1,451 @@ +#!/usr/bin/env python +# coding=utf-8 +# Copyright (C) LIGO Scientific Collaboration (2015-) +# +# This file is part of the GW DetChar python package. +# +# GW DetChar 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 3 of the License, or +# (at your option) any later version. +# +# GW DetChar 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 GW DetChar. If not, see . + +"""Compute an omega scan for a list of channels around a given GPS time. +""" + +from __future__ import division + +import os +import re +import sys +import ast +import warnings +from StringIO import StringIO + +import numpy +from numpy import fft as npfft +from scipy.signal import butter + +from matplotlib import (use, rcParams) +use('agg') # nopep8 + +from gwpy.utils import gprint +from gwpy.time import tconvert +from gwpy.table import EventTable +from gwpy.timeseries import TimeSeriesDict +from gwpy.detector import (Channel, ChannelList) +from gwpy.signal.qtransform import QTiling + +from gwdetchar import (cli, __version__) +from gwdetchar.omega import (config, plot, html) + +__author__ = 'Alex Urban ' +__credits__ = 'Duncan Macleod ' + + +# -- parse command line ------------------------------------------------------- + +parser = cli.create_parser(description=__doc__) +parser.add_argument('ifo', type=str, help='IFO prefix for this analysis') +parser.add_argument('gpstime', type=str, help='GPS time of scan') +parser.add_argument('-o', '--output-directory', + help='output directory for the omega scan, ' + 'default: ~/public_html/wdq/{IFO}_{gpstime}') +parser.add_argument('-f', '--config-file', action='append', default=None, + help='path to configuration file to use, can be given ' + 'multiple times (files read in order), default: ' + 'None') +parser.add_argument('-t', '--far-threshold', type=float, default=1e-3, + help='White noise false alarm rate threshold for ' + 'processing channels; default: %(default)s') +parser.add_argument('--condor', action='store_true', default=False, + help='indicates this job is running under condor, ' + 'only use when running as part of a workflow') +parser.add_argument('--colormap', default='viridis', + help='name of colormap to use, default: %(default)s') +parser.add_argument('-v', '--verbose', action='store_true', default='False', + help='print verbose output, default: %(default)s') +cli.add_nproc_option(parser) + +args = parser.parse_args() + +print("----------------------------------------------\n" + "Creating %s omega scan at GPS second %s..." % (args.ifo, args.gpstime)) + +gpstime = args.gpstime +gps = float(gpstime) +ifo = args.ifo +obs = ifo[0] +far = args.far_threshold + +# set output directory +outdir = args.output_directory +if outdir is None: + outdir = os.path.expanduser('~/public_html/wdq/%s_%s' % (ifo, gps)) +if not os.path.isdir(outdir): + os.makedirs(outdir) +os.chdir(outdir) +print("Output directory created as %s" % outdir) + +# parse configuration file +cp = config.OmegaConfigParser(ifo=ifo) +cp.read(args.config_file) + +# prepare html variables +htmlv = { + 'title': '%s Qscan | %s' % (ifo, gps), + 'config': args.config_file, +} + + +# -- FIXME: Eventually move these classes to gwdetchar.omega ------------------ + +class OmegaChannel(Channel): + def __init__(self, channelname, section, **params): + self.name = channelname + frametype = params.get('frametype', None) + frange = tuple( + [float(s) for s in params.get('frequency-range', None).split(',')] + ) + qrange = tuple( + [float(s) for s in params.get('q-range', None).split(',')] + ) + mismatch = float(params.get('max-mismatch', 0.2)) + snrthresh = float(params.get('snr-threshold', 5.5)) + pranges = [int(t) for t in params.get('plot-time-durations', + None).split(',')] + always_plot = ast.literal_eval(params.get('always-plot', 'False')) + super(OmegaChannel, self).__init__(channelname, frametype=frametype, + frange=frange, qrange=qrange, + mismatch=mismatch, pranges=pranges, + snrthresh=snrthresh, + always_plot=always_plot) + self.plots = {} + for plottype in ['timeseries_raw', 'timeseries_highpassed', + 'timeseries_whitened', 'qscan_raw', + 'qscan_whitened', 'qscan_autoscaled', + 'eventgram_raw', 'eventgram_whitened', + 'eventgram_autoscaled']: + self.plots[plottype] = [get_fancyplots(self.name, plottype, t) + for t in pranges] + self.section = section + self.params = params.copy() + + +class OmegaChannelList(object): + def __init__(self, **params): + self.name = params.get('name', None) + self.key = self.name.lower().replace(' ', '-') + self.duration = int(params.get('duration', 32)) + self.fftlength = int(params.get('fftlength', 2)) + self.resample = int(params.get('resample', 0)) + self.frametype = params.get('frametype', None) + chans = params.get('channels', None).split('\n') + self.channels = [OmegaChannel(c, self.name, **params) for c in chans] + self.params = params.copy() + + +# -- Utilities ---------------------------------------------------------------- + +def get_fancyplots(channel, plottype, duration, caption=None): + """Construct FancyPlot objects for output HTML pages + + Parameters + ---------- + channel : `str` + the name of the channel + plottype : `str` + the type of plot, e.g. 'raw_timeseries' + duration : `str` + duration of the plot, in seconds + caption : `str`, optional + a caption to render in the fancybox + """ + plotdir = 'plots' + chan = channel.replace('-', '_').replace(':', '-') + filename = '%s/%s-%s-%s.png' % (plotdir, chan, plottype, duration) + if not caption: + caption = os.path.basename(filename) + return html.FancyPlot(filename, caption) + + +def get_widths(x0, xdata): + """Generator to get the width of 1-D rectangular tiles + + Parameters + ---------- + x0 : `float` + starting point of the first tile + xdata : `array` + center points of all tiles + """ + for x in xdata: + width = 2 * (x - x0) + x0 = x + width/2 + yield width + + +def eventgram(time, data, search=0.5, frange=(0, numpy.inf), + qrange=(4, 96), snrthresh=5.5, mismatch=0.2): + """Create an eventgram with the Q-plane that has the most significant + tile. + + Parameters + ---------- + time : `float` or `int` + central GPS time of the search, in seconds + data : `TimeSeries` + timeseries data to analyze + search : `float`, optional + search analysis window, will be centered at `time` + frange : `tuple` of `float`, optional + `(low, high)` range of frequencies to scan + qrange : `tuple` of `float`, optional + `(low, high)` range of Qs to scan + snrthresh : `float` + threshold on tile SNR, tiles quieter than this will not be included + mismatch : `float` + the maximum fractional mismatch between neighboring tiles + + Returns + ------- + table : `gwpy.table.EventTable` + an `EventTable` object containing all tiles louder than `snrthresh` on + the Q plane with the loudest tile + """ + # generate tilings + planes = QTiling(abs(data.span), data.sample_rate.value, qrange=qrange, + frange=frange, mismatch=mismatch) + + # get frequency domain data + fdata = data.fft().value + + # set up results + Z = 0 # max normalized tile energy + N = 0 # no. of independent tiles + numplanes = 0 + qmax, qmin = qrange[1], qrange[0] + pweight = (1 + numpy.log10(qmax/qmin)/numpy.sqrt(2)) + + # Q-transform data for each `(Q, frequency)` tile + for plane in planes: + n_ind = 0 + numplanes += 1 + freqs, normenergies = plane.transform(fdata, epoch=data.x0) + # find peak energy in this plane and record if loudest + for freq, ts in zip(freqs, normenergies): + n_ind += 1 + 2 * numpy.pi * abs(data.span) * freq / plane.q + peak = ts.crop(time-search/2, time+search/2).value.max() + if peak > Z: + Z = peak + snr = numpy.sqrt(2*Z) + fc = freq + ts_cropped = ts.crop(time-search/2, time+search/2) + tc = ts_cropped.times.value[ts_cropped.value.argmax()] + del ts_cropped + peakplane = plane + N += n_ind * pweight / numplanes + + # create an eventgram for the plane with the loudest tile + energies = [] + central_times, central_freqs, durations, bandwidths = [], [], [], [] + freqs, normenergies = peakplane.transform(fdata, epoch=data.x0) + bws = get_widths(peakplane.frange[0], freqs) + for f, b, ts in zip(freqs, bws, normenergies): + durs = get_widths(data.x0.value, ts.times.value) + for t, dur, E in zip(ts.times.value, durs, ts.value): + if E >= snrthresh**2/2: + central_freqs.append(f) + bandwidths.append(b) + central_times.append(t) + durations.append(dur) + energies.append(E) + table = EventTable([central_times, central_freqs, durations, + bandwidths, energies], + names=('central_time', 'central_freq', 'duration', + 'bandwidth', 'energy')) + + # get parameters and return + table.q = peakplane.q + table.Z = Z + table.snr = snr + table.tc = tc + table.fc = fc + table.frange = peakplane.frange + table.engthresh = -numpy.log(far * abs(data.span) / (1.5 * N)) + return table + + +# -- Compute Qscan ------------------------------------------------------------ + +# make subdirectories +plotdir = 'plots' +aboutdir = 'about' +for d in [plotdir, aboutdir]: + if not os.path.isdir(d): + os.makedirs(d) + +# determine channel blocks +blocks = [OmegaChannelList(**cp[s]) for s in cp.sections()] + +# set up html output +gprint('Setting up HTML at %s/index.html...' % outdir) +html.write_qscan_page(ifo, gps, blocks, **htmlv) + +# launch omega scans +gprint('Launching Omega scans...') + +# range over blocks +for block in blocks[:]: + gprint('Processing block %s' % block.name) + chans = [c.name for c in block.channels] + # read in fftlength seconds of data + # centered on gps + duration = block.duration + fftlength = block.fftlength + data = TimeSeriesDict.get(chans, gps-256-fftlength/4, gps+256+fftlength/4, + frametype=block.frametype, nproc=args.nproc, + verbose=args.verbose) + # compute qscans + for c in block.channels[:]: + if args.verbose: + gprint('Computing omega scans for channel %s...' % c.name) + + # get raw timeseries + series = data[c.name] + if block.resample: + series = series.resample(block.resample) + + # filter the timeseries + corner = c.frange[0] / 1.5 + hpseries = series.highpass(corner, gpass=.5, gstop=100, filtfilt=True) + asd = series.asd(fftlength, fftlength/2, method='lal_median_mean') + wseries = hpseries.whiten(fftlength, fftlength/2, window='hann', + asd=asd) + + # crop the timeseries + wseries = wseries.crop(gps-duration/2, gps+duration/2) + hpseries = hpseries.crop(gps-duration/2, gps+duration/2) + + # compute eventgrams + try: + table = eventgram(gps, wseries, frange=c.frange, qrange=c.qrange, + snrthresh=c.snrthresh, mismatch=c.mismatch) + except UnboundLocalError: + if args.verbose: + gprint('Channel is misbehaved, removing it from the analysis') + del series, hpseries, wseries, asd + block.channels.remove(c) + continue + if table.Z < table.engthresh and not c.always_plot: + if args.verbose: + gprint('Channel not significant at white noise false alarm ' + 'rate %s Hz' % far) + del series, hpseries, wseries, asd, table + block.channels.remove(c) + continue + Q = table.q + rtable = eventgram(gps, hpseries, frange=table.frange, qrange=(Q, Q), + snrthresh=c.snrthresh, mismatch=c.mismatch) + + # compute Q-transforms + tres = min(c.pranges) / 500 + fres = c.frange[0] / 5 + qscan = wseries.q_transform(qrange=(Q, Q), frange=c.frange, + tres=tres, fres=fres, gps=gps, + search=0.25, whiten=False) + rqscan = hpseries.q_transform(qrange=(Q, Q), frange=c.frange, + tres=tres, fres=fres, gps=gps, + search=0.25, whiten=False) + + # prepare plots + if args.verbose: + gprint('Plotting omega scans for channel %s...' % c.name) + # work out figure size + width = min(16 / len(c.pranges), 8) + figsize = [width, 5] + for span, png1, png2, png3, png4, png5, png6, png7, png8, png9 in zip( + c.pranges, c.plots['qscan_whitened'], + c.plots['qscan_autoscaled'], c.plots['qscan_raw'], + c.plots['timeseries_raw'], c.plots['timeseries_highpassed'], + c.plots['timeseries_whitened'], c.plots['eventgram_raw'], + c.plots['eventgram_whitened'], c.plots['eventgram_autoscaled'] + ): + # plot whitened qscan + fig1 = plot.omega_plot(qscan, gps, span, c.name, qscan=True, + clim=(0, 25), colormap=args.colormap, + figsize=figsize) + fig1.savefig(str(png1)) + # plot autoscaled, whitened qscan + fig2 = plot.omega_plot(qscan, gps, span, c.name, qscan=True, + colormap=args.colormap, figsize=figsize) + fig2.savefig(str(png2)) + # plot raw qscan + fig3 = plot.omega_plot(rqscan, gps, span, c.name, qscan=True, + clim=(0, 25), colormap=args.colormap, + figsize=figsize) + fig3.savefig(str(png3)) + # plot raw timeseries + fig4 = plot.omega_plot(series, gps, span, c.name, + ylabel='Amplitude', figsize=figsize) + fig4.savefig(str(png4)) + # plot highpassed timeseries + fig5 = plot.omega_plot(hpseries, gps, span, c.name, + ylabel='Highpassed Amplitude', + figsize=figsize) + fig5.savefig(str(png5)) + # plot whitened timeseries + fig6 = plot.omega_plot(wseries, gps, span, c.name, + ylabel='Whitened Amplitude', + figsize=figsize) + fig6.savefig(str(png6)) + # plot raw eventgram + fig7 = plot.omega_plot(rtable, gps, span, c.name, eventgram=True, + clim=(0, 25), colormap=args.colormap, + figsize=figsize) + fig7.savefig(str(png7)) + # plot raw eventgram + fig8 = plot.omega_plot(table, gps, span, c.name, eventgram=True, + clim=(0, 25), colormap=args.colormap, + figsize=figsize) + fig8.savefig(str(png8)) + # plot raw eventgram + fig9 = plot.omega_plot(table, gps, span, c.name, eventgram=True, + colormap=args.colormap, figsize=figsize) + fig9.savefig(str(png9)) + + # save parameters + c.Q = Q + c.energy = table.Z + c.snr = table.snr + c.t = table.tc + c.f = table.fc + + # delete intermediate data products + del fig1, fig2, fig3, fig4, fig5, fig6 + del qscan, rqscan, table, rtable, series, hpseries, wseries, asd + + # delete data + del data + + # if the entire block is unprocessed, delete it + if not block.channels: + blocks.remove(block) + + # update html output + html.write_qscan_page(ifo, gps, blocks, **htmlv) + + +# -- Prepare HTML ------------------------------------------------------------- + +# write HTML page and finish +gprint('Finalizing HTML at %s/index.html...' % outdir) +html.write_qscan_page(ifo, gps, blocks, **htmlv) +gprint("-- index.html written, all done --") diff --git a/gwdetchar/omega/__init__.py b/gwdetchar/omega/__init__.py index 2a7dfc472..ba4fb3d37 100755 --- a/gwdetchar/omega/__init__.py +++ b/gwdetchar/omega/__init__.py @@ -18,7 +18,7 @@ """Methods and utilties for performing Omega pipline scans -See Chatterji 20015 [thesis] for details on the Q-pipeline. +See Chatterji 2005 [thesis] for details on the Q-pipeline. """ import os diff --git a/gwdetchar/omega/config.py b/gwdetchar/omega/config.py new file mode 100644 index 000000000..cf66ee364 --- /dev/null +++ b/gwdetchar/omega/config.py @@ -0,0 +1,90 @@ +# coding=utf-8 +# Copyright (C) Duncan Macleod (2015) +# +# This file is part of the GW DetChar python package. +# +# GW DetChar 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 3 of the License, or +# (at your option) any later version. +# +# GW DetChar 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 GW DetChar. If not, see . + +""" +Configuration files for Omega Scans +################################### + +How to write a configuration file +================================= +""" + +try: + import configparser +except ImportError: # python 2.x + import ConfigParser as configparser + +__author__ = 'Alex Urban ' +__credits__ = 'Duncan Macleod ' + +OMEGA_DEFAULTS = {} + + +# -- define parser ------------------------------------------------------------ + +class OmegaConfigParser(configparser.ConfigParser): + def __init__(self, ifo=None, defaults=dict(), **kwargs): + if ifo is not None: + defaults.setdefault('IFO', ifo) + configparser.ConfigParser.__init__(self, defaults=defaults, **kwargs) + self.set_omega_defaults() + + def set_omega_defaults(self): + for section in OMEGA_DEFAULTS: + self.add_section(section) + for key, val in OMEGA_DEFAULTS[section].iteritems(): + if key.endswith('channels') and isinstance(val, (tuple, list)): + self.set(section, key, '\n'.join(list(val))) + elif isinstance(val, tuple): + self.set(section, key, ', '.join(map(str, val))) + else: + self.set(section, key, str(val)) + + def read(self, filenames): + readok = configparser.ConfigParser.read(self, filenames) + for f in filenames: + if f not in readok: + raise IOError("Cannot read file %r" % f) + return readok + read.__doc__ = configparser.ConfigParser.read.__doc__ + + def getfloats(self, section, option): + return self._get(section, comma_separated_floats, option) + + def getparams(self, section, prefix): + nchar = len(prefix) + params = dict((key[nchar:], val) for (key, val) in + self.items(section) if key.startswith(prefix)) + # try simple typecasting + for key in params: + if params[key].lower() in ('true', 'false'): + params[key] = bool(params[key]) + if key == 'frequency-range': + params[key] = tuple([float(s) for s in params[key].split(',')]) + if key == 'channels': + params[key] = params[key].split(',\n') + else: + try: + params[key] = float(params[key]) + except ValueError: + pass + return params + + +def comma_separated_floats(string): + return map(float, string.split(',')) diff --git a/gwdetchar/omega/html.py b/gwdetchar/omega/html.py new file mode 100644 index 000000000..ee5eacbb1 --- /dev/null +++ b/gwdetchar/omega/html.py @@ -0,0 +1,901 @@ +# coding=utf-8 +# Copyright (C) Duncan Macleod (2015) +# +# This file is part of the GW DetChar python package. +# +# GW DetChar 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 3 of the License, or +# (at your option) any later version. +# +# GW DetChar 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 GW DetChar. If not, see . + +"""Utilties for writing Omega scan HTML pages +""" + +from __future__ import division + +import os +import sys +import datetime +import warnings +import subprocess +from functools import wraps +from getpass import getuser +from pytz import timezone + +from glue import markup +from gwpy.time import tconvert +from gwpy.plotter.colors import GW_OBSERVATORY_COLORS +from ..io.html import (JQUERY_JS, BOOTSTRAP_CSS, BOOTSTRAP_JS) +from .. import __version__ + +__author__ = 'Alex Urban ' +__credit__ = 'Duncan Macleod ' + +# -- give context for ifo names + +OBSERVATORY_MAP = { + 'G1': { + 'name': 'GEO', + 'context': 'default' + }, + 'H1': { + 'name': 'LIGO Hanford', + 'context': 'danger' + }, + 'I1': { + 'name': 'LIGO India', + 'context': 'success' + }, + 'K1': { + 'name': 'KAGRA', + 'context': 'warning' + }, + 'L1': { + 'name': 'LIGO Livingston', + 'context': 'info' + }, + 'V1': { + 'name': 'Virgo', + 'context': 'primary' + } +} + +# -- set up default JS and CSS files + +FANCYBOX_CSS = ( + "//cdnjs.cloudflare.com/ajax/libs/fancybox/2.1.5/jquery.fancybox.min.css") +FANCYBOX_JS = ( + "//cdnjs.cloudflare.com/ajax/libs/fancybox/2.1.5/jquery.fancybox.min.js") + +FONT_LATO_CSS = ( + "//fonts.googleapis.com/css?family=Lato:300,700" +) + +CSS_FILES = [BOOTSTRAP_CSS, FANCYBOX_CSS, FONT_LATO_CSS] +JS_FILES = [JQUERY_JS, BOOTSTRAP_JS, FANCYBOX_JS] + +OMEGA_CSS = """ +html { + position: relative; + min-height: 100%; +} +body { + padding-top: 75px; + margin-bottom: 120px; + min-height: 100%; + font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; + -webkit-font-smoothing: antialiased; +} +.navbar { + color: #eee; + padding-top: 8px; + padding-bottom: 8px; + -moz-box-shadow: 0 1px 10px 2px #ccc; + -webkit-box-shadow: 0 1px 10px 2px #ccc; + box-shadow: 0 1px 10px 2px #ccc; +} +.with-margin { + margin-bottom: 15px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + height: 100px; + color: #eee; + background-color: #4c4c4c; + padding-top: 20px; + padding-bottom: 20px; +} +.fancybox-skin { + background: white; +} +""" # nopep8 + +OMEGA_JS = """ +$(document).ready(function() { + $(\".fancybox\").fancybox({ + nextEffect: 'none', + prevEffect: 'none', + helpers: {title: {type: 'inside'}} + }); +}); +function showImage(channelName, tRanges, imageType, captions) { + for (var tIndex in tRanges) { + var idBase = channelName + "_" + tRanges[tIndex]; + var fileBase = channelName + "-" + imageType + "-" + tRanges[tIndex]; + document.getElementById("a_" + idBase).href = + "plots/" + fileBase + ".png"; + document.getElementById("a_" + idBase).title = captions[tIndex]; + document.getElementById("img_" + idBase).src = + "plots/" + fileBase + ".png"; + document.getElementById("img_" + idBase).alt = fileBase + ".png"; + }; +}; +""" # nopep8 + + +# -- Plot construction -------------------------------------------------------- + +class FancyPlot(object): + """A helpful class of objects that coalesce image links and caption text + for fancybox figures. + + Parameters + ---------- + img : `str` or `FancyPlot` + either a filename (including relative or absolute path) or another + FancyPlot instance + caption : `str` + the text to be displayed in a fancybox as this figure's caption + """ + def __init__(self, img, caption=None): + if isinstance(img, FancyPlot): + caption = caption if caption else img.caption + self.img = str(img) + self.caption = caption if caption else os.path.basename(self.img) + + def __str__(self): + return self.img + + +# -- HTML construction -------------------------------------------------------- + +def write_static_files(static): + """Write static CSS and javascript files into the given directory + + Parameters + ---------- + static : `str` + the target directory for the static files, will be created if + necessary + + Returns + ------- + `/omega.css` + `/omega.js` + the paths of the two files written by this method, which will be + `omega.css` and `omega.js` inside `static` + + Notes + ----- + This method modifies the module-level variables ``CSS_FILES`` and + ``JS_FILES`` to guarantee that the static files are actually only + written once per process. + """ + if not os.path.isdir(static): + os.makedirs(static) + omegacss = os.path.join(static, 'omega.css') + if omegacss not in CSS_FILES: + with open(omegacss, 'w') as f: + f.write(OMEGA_CSS) + CSS_FILES.append(omegacss) + omegajs = os.path.join(static, 'omega.js') + if omegajs not in JS_FILES: + with open(omegajs, 'w') as f: + f.write(OMEGA_JS) + JS_FILES.append(omegajs) + return omegacss, omegajs + + +def init_page(ifo, gpstime, css=[], script=[], base=os.path.curdir, + **kwargs): + """Initialise a new `markup.page` + This method constructs an HTML page with the following structure + .. code-block:: html + + + + + +
+ +
+
+ + Parameters + ---------- + ifo : `str` + the interferometer prefix + gpstime : `float` + the central GPS time of the analysis + css : `list`, optional + the list of stylesheets to link in the `` + script : `list`, optional + the list of javascript files to link in the `` + base : `str`, optional, default '.' + the path for the `` tag to link in the `` + + Returns + ------- + page : `markup.page` + the structured markup to open an HTML document + """ + # write CSS to static dir + staticdir = os.path.join(os.path.curdir, 'static') + write_static_files(staticdir) + # create page + page = markup.page() + page.header.append('') + page.html(lang='en') + page.head() + page.base(href=base) + page._full = True + # link stylesheets (appending bootstrap if needed) + css = css[:] + for cssf in CSS_FILES[::-1]: + b = os.path.basename(cssf) + if not any(f.endswith(b) for f in css): + css.insert(0, cssf) + for f in css: + page.link(href=f, rel='stylesheet', type='text/css', media='all') + # link javascript + script = script[:] + for jsf in JS_FILES[::-1]: + b = os.path.basename(jsf) + if not any(f.endswith(b) for f in script): + script.insert(0, jsf) + for f in script: + page.script('', src=f, type='text/javascript') + # add other attributes + for key in kwargs: + getattr(page, key)(kwargs[key]) + # finalize header + page.head.close() + page.body() + # write banner + page.div(class_='navbar navbar-fixed-top', role='banner', + style='background-color:%s;' % GW_OBSERVATORY_COLORS[ifo]) + page.div(class_='container') + page.h4('%s Omega Scan %s' + % (ifo, gpstime), style="text-align:left;") + page.div.close() # container + page.div.close() # navbar + + # open container + page.div(class_='container') + return page + + +def close_page(page, target, about=None, date=None): + """Close an HTML document with markup then write to disk + This method writes the closing markup to complement the opening + written by `init_page`, something like: + .. code-block:: html +
+
+ +
+ + + + Parameters + ---------- + page : `markup.page` + the markup object to close + target : `str` + the output filename for HTML + about : `str`, optional + the path of the 'about' page to link in the footer + date : `datetime.datetime`, optional + the timestamp to place in the footer, defaults to + `~datetime.datetime.now` + """ + page.div.close() # container + page.add(str(write_footer(about=about, date=date))) + if not page._full: + page.body.close() + page.html.close() + with open(target, 'w') as f: + f.write(page()) + return page + + +def wrap_html(func): + """Decorator to wrap a function with `init_page` and `close_page` calls + This allows inner HTML methods to be written with minimal arguments + and content, hopefully making things simpler + """ + @wraps(func) + def decorated_func(ifo, gpstime, *args, **kwargs): + # set page init args + initargs = { + 'title': '%s Qscan | %s' % (ifo, gpstime), + 'base': os.path.curdir, + } + for key in ['title', 'base']: + if key in kwargs: + initargs[key] = kwargs.pop(key) + # find outdir + outdir = kwargs.pop('outdir', initargs['base']) + if not os.path.isdir(outdir): + os.makedirs(outdir) + # write about page + try: + config = kwargs.pop('config') + except KeyError: + about = None + else: + iargs = initargs.copy() + aboutdir = os.path.join(outdir, 'about') + if iargs['base'] == os.path.curdir: + iargs['base'] = os.path.pardir + about = write_about_page(ifo, gpstime, config, outdir=aboutdir, + **iargs) + if os.path.basename(about) == 'index.html': + about = about[:-10] + # open page + page = init_page(ifo, gpstime, **initargs) + # write analysis summary + # (but only on the main results page) + if about: + page.add(write_summary(ifo, gpstime)) + kwargs['context'] = OBSERVATORY_MAP[ifo]['context'] + # write content + contentf = os.path.join(outdir, '_inner.html') + with open(contentf, 'w') as f: + f.write(str(func(*args, **kwargs))) + # embed content + page.div('', id_='content') + page.script("$('#content').load('%s');" % contentf) + # close page + index = os.path.join(outdir, 'index.html') + close_page(page, index, about=about) + return index + return decorated_func + +# -- Utilities ---------------------------------------------------------------- + + +def html_link(href, txt, target="_blank", **params): + """Write an HTML tag + + Parameters + ---------- + href : `str` + the URL to point to + txt : `str` + the text for the link + target : `str`, optional + the ``target`` of this link + **params + other HTML parameters for the ```` tag + + Returns + ------- + html : `str` + """ + if target is not None: + params.setdefault('target', target) + return markup.oneliner.a(txt, href=href, **params) + + +def toggle_button(plottype, channel, pranges, context): + """Create a Bootstrap button object that toggles between plot types. + + Parameters + ---------- + plottype : `str` + the type of plot to toggle toward + channel : `OmegaChannel` + the channel object corresponding to the plots shown + pranges : `list` of `int`s + a list of ranges for the time axis of each plot + context : `str` + the Bootstrap context that controls color-coding + + Returns + ------- + page : `page` + a markup page object + """ + page = markup.page() + text = plottype.split('_')[1] + pstrings = ["'%s'" % p for p in pranges] + chanstring = channel.name.replace('-', '_').replace(':', '-') + captions = [p.caption for p in channel.plots[plottype]] + page.button('%s' % text, type_='button', + class_='btn btn-%s' % context, style='opacity:0.6;', + onclick=u"showImage('%s', [%s], '%s', %s);" + % (chanstring, ','.join(pstrings), plottype, captions)) + return page() + + +def cis_link(channel, **params): + """Write a channel name as a link to the Channel Information System + + Parameters + ---------- + channel : `str` + the name of the channel to link + **params + other HTML parmeters for the ```` tag + + Returns + ------- + html : `str` + """ + kwargs = { + 'title': "CIS entry for %s" % channel, + 'style': "font-family: Monaco, \"Courier New\", monospace;", + } + kwargs.update(params) + return html_link("https://cis.ligo.org/channel/byname/%s" % channel, + channel, **kwargs) + + +def fancybox_img(img, linkparams=dict(), **params): + """Return the markup to embed an in HTML + + Parameters + ---------- + img : `FancyPlot` + a `FancyPlot` object containing the path of the image to embed + and its caption to be displayed + linkparams : `dict` + the HTML attributes for the ```` tag + **params + the HTML attributes for the ```` tag + + Returns + ------- + html : `str` + Notes + ----- + See `~gwdetchar.omega.plot.FancyPlot` for more about the `FancyPlot` class. + """ + page = markup.page() + aparams = { + 'title': img.caption, + 'class_': 'fancybox', + 'target': '_blank', + 'data-fancybox-group': 'qscan-image', + } + aparams.update(linkparams) + img = str(img) + substrings = os.path.basename(img).split('-') + channel = '%s-%s' % tuple(substrings[:2]) + duration = substrings[-1].split('.')[0] + page.a(href=img, id_='a_%s_%s' % (channel, duration), **aparams) + imgparams = { + 'alt': os.path.basename(img), + 'class_': 'img-responsive', + } + imgparams['src'] = img + imgparams.update(params) + page.img(id_='img_%s_%s' % (channel, duration), **imgparams) + page.a.close() + return str(page) + + +def scaffold_plots(plots, nperrow=2): + """Embed a `list` of images in a bootstrap scaffold + + Parameters + ---------- + plot : `list` of `FancyPlot` + the list of image paths to embed + nperrow : `int` + the number of images to place in a row (on a desktop screen) + + Returns + ------- + page : `~glue.markup.page` + the markup object containing the scaffolded HTML + """ + page = markup.page() + x = int(12//nperrow) + # scaffold plots + for i, p in enumerate(plots): + if i % nperrow == 0: + page.div(class_='row', style="width:96%;") + page.div(class_='col-sm-%d' % x) + page.add(fancybox_img(p)) + page.div.close() # col + if i % nperrow == nperrow - 1: + page.div.close() # row + if i % nperrow < nperrow-1: + page.div.close() # row + return page() + + +def write_footer(about=None, date=None): + """Write a