# -*- 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)