Source code for gwsumm.tabs.etg

# -*- coding: utf-8 -*-
# Copyright (C) Duncan Macleod (2013)
#
# This file is part of GWSumm
#
# GWSumm 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.
#
# GWSumm 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 GWSumm.  If not, see <http://www.gnu.org/licenses/>

"""Custom `SummaryTab` for the output of an ETG.
"""

import os
from configparser import NoOptionError
from warnings import warn

from MarkupPy import markup

from glue.lal import Cache

from gwpy.time import from_gps

from gwdetchar.io import html

from ..channels import get_channel
from ..html.html5 import ldvw_qscan
from ..state import (get_state, ALLSTATE, generate_all_state)
from ..triggers import (get_triggers, get_time_column)
from ..utils import re_quote
from ..mode import (Mode, get_mode)
from .registry import (get_tab, register_tab)

__author__ = 'Duncan Macleod <duncan.macleod@ligo.org>'
__all__ = ['EventTriggerTab']


[docs] class EventTriggerTab(get_tab('default')): """Custom `DataTab` displaying a summary of event trigger generation Parameters ---------- name : `str` name of this tab (required) start : `LIGOTimeGPS`, `str` start time of this `DataTab`, anything that can be parsed by `~gwpy.time.to_gps` is fine end : `LIGOTimeGPS`, `str` end time of this `DataTab`, format as for `start` channel : `str` name of the channel of interest etg : `str`, optional name of this event trigger generator (ETG), defaults to the name of this `EventTriggerTab` states : `list` of `states <gwsumm.state.SummaryState>` the `list` of states (`~gwsumm.state.SummaryState`) over which this `DataTab` should be processed. More states can be added later (but before running :meth:`~DataTab.process`) via :meth:`~DataTab.add_state`. table : `type`, `str`, optional LIGO_LW `~glue.ligolw.table.Table` class to use for this ETG, e.g. use `~glue.ligolw.lsctables.SnglBurstTable` for Omicron, or `~glue.ligolw.lsctables.SnglInspiralTable` for CBC cache : `~glue.lal.Cache`, `str`, optional `Cache` object, or path to a LAL-format cache file on disk, from which to read the event triggers. If no cache is given, the `gwtrigfind` module will be used to automatically locate the trigger files. url : `str`, optional URL for linking to more details results for this tab. **kwargs all other keyword arguments accepted by the `DataTab` See Also -------- gwsumm.tabs.DataTab for details on the other keyword arguments (``**kwargs``) accepted by the constructor for this `EventTriggerTab`. """ type = 'triggers' def __init__(self, name, channel=None, etg=None, cache=None, url=None, **kwargs): """Create a new `EventTriggerTab` """ super(EventTriggerTab, self).__init__(name, **kwargs) self.channel = channel and get_channel(channel) or None self.cache = cache self.url = url self.error = dict() # parse ETG and LIGO_LW table class self.filterstr = None if etg is None: etg = self.name self.etg = etg
[docs] @classmethod def from_ini(cls, config, section, **kwargs): """Define a new `EventTriggerTab` from a `ConfigParser`. Parameters ---------- cp : :class:`~gwsumm.config.GWConfigParser` customised configuration parser containing given section section : `str` name of section to parse *args, **kwargs other positional and keyword arguments to pass to the class constructor (`__init__`) Notes ----- In addition to those attributes parsed by the parent `DataTab.from_ini` method, this method will parse the following attributes from a `ConfigParser`:: .. autosummary:: ~EventTriggerTab.channel ~EventTriggerTab.etg ~EventTriggerTab.url ~EventTriggerTab.cache ~EventTriggerTab.table Additionally, the loudest events table is configured by giving the following options ----------------- --------------------------------------------- `loudest` the number of events to include in the table `loudest-rank` the statistic to use in sorting the table `loudest-dt` the minimum time separation for unique events `loudest-columns` the columns to print in the table `loudest-labels` the labels for each of the given columns ----------------- --------------------------------------------- See Also -------- DataTab.from_ini the parent parsing method that handles parsing plot defintions, amongst other things """ for key in ['channel', 'etg', 'url', 'cache', 'table']: try: kwargs.setdefault( key, re_quote.sub('', config.get(section, key))) except NoOptionError: pass new = super(EventTriggerTab, cls).from_ini(config, section, **kwargs) # get trigger filter if config.has_option(section, 'event-filter'): new.filterstr = config.get(section, 'event-filter') else: new.filterstr = None # set ETG and trigger filter for plots for p in new.plots + new.subplots: p.etg = new.etg.lower() p.filterstr = new.filterstr # get loudest options if config.has_option(section, 'loudest'): # set defaults new.loudest = { 'N': config.getint(section, 'loudest'), 'rank': ['new_snr'], 'dt': 8, } if 'hope' in new.etg.lower(): new.loudest['columns'] = [ 'end', 'new_snr', 'snr', 'mchirp', 'mass1', 'mass2', 'reduced_chisq', ] else: new.loudest['columns'] = [ 'peak', 'duration', 'snr', 'peak_frequency', 'bandwidth', 'Q', ] # override from config if config.has_option(section, 'loudest-columns'): new.loudest['columns'] = list(map( lambda s: re_quote.sub('', s), config.get(section, 'loudest-columns').split(',') )) if config.has_option(section, 'loudest-labels'): new.loudest['labels'] = list(map( lambda s: re_quote.sub('', s), config.get(section, 'loudest-labels').split(',') )) else: new.loudest['labels'] = [' '.join(map(str.title, s.split('_'))) for s in new.loudest['columns']] if config.has_option(section, 'loudest-rank'): new.loudest['rank'] = list(map( lambda c: re_quote.sub('', c), config.get(section, 'loudest-rank').split(',') )) if config.has_option(section, 'loudest-dt'): new.loudest['dt'] = config.getfloat(section, 'loudest-dt') else: new.loudest = None return new
[docs] def finalize_states(self, config=None, **kwargs): # finalize all-state try: allstate = get_state(ALLSTATE) except ValueError: allstate = generate_all_state(self.start, self.end) allstate.fetch(config=config) for state in self.states: # if no segments, don't do anything: if (state.filename is not None and not os.path.isdir(os.path.split(state.filename)[0])): self.error[state] = ( 'warning', 'No analysis has been run.') else: state.fetch(config=config, **kwargs) if state.filename is not None and len(state.active) == 0: self.error[state] = ( 'warning', 'This analysis found no segments over which to run.')
[docs] def process(self, *args, **kwargs): error = None # read the cache files if isinstance(self.cache, str) and os.path.isfile(self.cache): with open(self.cache, 'r') as fobj: try: self.cache = Cache.fromfile(fobj).sieve( segment=self.span) except ValueError as e: if "could not convert \'\\n\' to CacheEntry" in str(e): error = 'could not parse event cache file' else: raise elif isinstance(self.cache, str): error = 'could not locate event cache file' warn("Cache file %s not found." % self.cache) elif self.cache is not None and not isinstance(self.cache, Cache): raise ValueError("Cannot parse EventTriggerTab.cache of type %r" % type(self.cache)) # push error to all states for HTML writing if error: for state in self.states: self.error[state] = ( 'danger', 'This analysis seems to have failed: %s.' % error) # only process if the cachfile was found if kwargs.get('trigcache', None) is None: kwargs['trigcache'] = self.cache try: super(EventTriggerTab, self).process(*args, **kwargs) except IOError as e: msg = "GWSumm failed to process these data.<pre>%s</pre>" % str(e) for state in self.states: self.error[state] = ('danger', msg)
[docs] def process_state(self, state, *args, **kwargs): if self.error.get(state, None): return super(EventTriggerTab, self).process_state(state, *args, **kwargs)
[docs] def write_state_html(self, state, pre=None): """Write the '#main' HTML content for this `EventTriggerTab`. """ page = markup.page() if self.error.get(state, None): level, message = self.error[state] # no segments, print warning page.add(html.alert(( message, "If you believe this to be an error, please contact %s." % markup.oneliner.a('the DetChar group', class_='alert-link', href='mailto:detchar@ligo.org'), ), context=level, dismiss=False)) else: # otherwise, carry on... if pre is not None: page.add(str(pre)) elif self.foreword: page.add(str(self.foreword)) page.add(str(self.scaffold_plots(state=state))) # link full results if self.url: page.hr(class_='row-divider') page.div(class_='btn-group') page.a('Click here for the full %s results' % self.name, href=self.url, rel='external', target='_blank', class_='btn btn-info btn-xl') page.div.close() page.hr(class_='row-divider') if self.loudest: # get triggers table = get_triggers(self.channel, self.plots[0].etg, state, query=False) if self.filterstr is not None: table = table.filter(self.filterstr) tcol = get_time_column(table, self.etg) # set table headers headers = list(self.loudest['labels']) + ['LDVW Action'] columns = list(self.loudest['columns']) if tcol in columns: headers.insert(1, 'UTC time') date = True else: date = False # loop over rank columns for rank in self.loudest['rank']: try: rankstr = self.loudest['labels'][columns.index(rank)] except ValueError: rankstr = repr(rank) page.h2('Loudest events by %s' % rankstr, class_='mt-4 mb-4') rank = table[rank].argsort()[::-1] loudest = [] i = 0 dt = self.loudest['dt'] while len(loudest) < self.loudest['N'] and i < rank.size: e = table[rank[i]] t = e[tcol] if i == 0 or all( abs((t - e2[tcol])) >= dt for e2 in loudest): loudest.append(e) i += 1 data = [] for row in loudest: data.append([]) for column in columns: data[-1].append( '%.3f' % float(row[column])) if date: data[-1].insert( 1, from_gps(row[tcol]).strftime( '%B %d %Y %H:%M:%S.%f')[:-3]) data[-1].append(ldvw_qscan(self.channel, data[-1][0])) # construct table times = tuple(row[0] for row in data) launch = ldvw_qscan(self.channel, times) page.add(str(html.table( headers, data, id='%s-loudest-table' % self.etg, caption=("%d loudest <samp>%s</samp> (%s) events " "by %s with minimum %ss separation. %s" % (self.loudest['N'], self.channel, self.etg, rankstr, self.loudest['dt'], str(launch))) ))) if self.subplots: page.h1('Sub-plots', class_='mt-4') layout = get_mode() == Mode.week and [7] or [4] plist = [p for p in self.subplots if p.state in [state, None]] page.add(str(self.scaffold_plots(plots=plist, state=state, layout=layout))) # link full results if self.url: page.hr(class_='row-divider') page.div(class_='btn-group') page.a('Click here for the full %s results' % self.name, href=self.url, rel='external', target='_blank', class_='btn btn-info btn-xl') page.div.close() page.hr(class_='row-divider') # write state information page.add(str(self.write_state_information(state))) # write to file idx = self.states.index(state) with open(self.frames[idx], 'w') as fobj: fobj.write(str(page)) return self.frames[idx]
register_tab(EventTriggerTab)