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
+
+
+
+
+
+
+
+
IFO
+
GPSTIME
+
+
+
+
+ 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
+