# -*- 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/>.
"""Definition of the `SummaryState` class.
"""
import datetime
import re
import operator
from configparser import (NoOptionError, DEFAULTSECT)
from astropy.time import Time
from gwpy.detector import get_timezone_offset
from gwpy.segments import (Segment, SegmentList, DataQualityFlag)
from gwpy.time import (to_gps, from_gps)
from .. import globalv
from ..config import (GWSummConfigParser)
from ..utils import re_cchar
from ..segments import get_segments
from ..data import get_timeseries
MATHOPS = {
'<': operator.lt,
'<=': operator.le,
'=': operator.eq,
'>=': operator.ge,
'>': operator.gt,
'==': operator.is_,
'!=': operator.is_not,
}
[docs]
class SummaryState(DataQualityFlag):
"""An operating state over which to process a `~gwsumm.tabs.DataTab`.
Parameters
----------
name : `str`
name for this state
known : `~gwpy.segments.SegmentList`, optional
list of known segments
active : `~gwpy.segments.SegmentList`, optional
list of active segments
description : `str`, optional
text describing what this state means
definition : `str`, optional
logical combination of flags that define known and active segments
for this state (see :attr:`documentation <SummaryState.definition>`
for details)
key : `str`, optional
registry key for this state, defaults to :attr:`~SummaryState.name`
"""
MATH_DEFINITION = re.compile(r'(%s)' % '|'.join(MATHOPS.keys()))
def __init__(self, name, known=SegmentList(), active=SegmentList(),
description=None, definition=None, hours=None, key=None,
filename=None, url=None):
"""Initialise a new `SummaryState`
"""
# allow users to specify known as (start, end)
if (isinstance(known, Segment) or
(isinstance(known, tuple) and len(known) == 2 and
not isinstance(known[0], tuple))):
known = [known]
super(SummaryState, self).__init__(name=name, known=known,
active=active)
self.description = description
if definition:
self.definition = re.sub(r'(\s|\n)', '', definition)
else:
self.definition = None
self.key = key
self.hours = hours
self.url = url
if known and active:
self.ready = True
else:
self.ready = False
self.filename = filename
@property
def name(self):
"""Name of this state
"""
return self._name
@name.setter
def name(self, n):
self._name = n
@property
def tag(self):
"""File tag for images generated using this state
"""
return re_cchar.sub("_", self.name).upper()
@property
def start(self):
"""GPS start time of this state's validity.
"""
return self.extent[0]
@property
def end(self):
"""GPS end time of this state's validity
"""
return self.extent[1]
@property
def definition(self):
r"""The combination of data-quality flags that define this
`SummaryState`
For example::
>>> state = SummaryState(definition='L1:DMT-SCIENCE:1')
would define a `SummaryState` based on the validity and activity of
the single flag ``'L1:DMT-SCIENCE:1'`` (the science-mode flag for the
LIGO Livingston Observatory interferometer).
Similarly::
>>> state = SummaryState(
definition='L1:DMT-SCIENCE:1&!L1:DMT-LIGHTDIP_10_PERCENT:1')
would define a `SummaryState` as active when the ``'L1:DMT-SCIENCE:1'``
flag was active and the ``'L1:DMT-LIGHTDIP_10_PERCENT:1'`` flag was
not active.
The following logical identifiers are acceptable:
== ===================================================================
& Union (i.e. flag1 **and** flag2 must be active)
\| Intersection (i.e. flag1 **or** flag2 must be active)
&! One-sided difference (i.e. flag1 is active and flag2 **is not**\
active)
!= Two-sided difference (i.e. flag1 is active and flag2 is not **OR**\
flag2 is active and flag2 is not)
== ===================================================================
:type: str
"""
return self._definition
@definition.setter
def definition(self, d):
if d:
self._definition = str(d)
else:
self._definition = None
@property
def key(self):
"""The registry key for this `SummaryState`.
:type: `str`
"""
if self._key is not None:
return self._key
else:
return self._name
@key.setter
def key(self, k):
self._key = k
[docs]
@classmethod
def from_ini(cls, config, section):
"""Create a new `SummaryState` from a section in a `ConfigParser`.
Parameters
----------
config : :class:`~gwsumm.config.GWConfigParser`
customised configuration parser containing given section
section : `str`
name of section to parse
Returns
-------
`SummaryState`
a new state, with attributes set from the options in the
configuration
"""
config = GWSummConfigParser.from_configparser(config)
# get span times
start = config.getint(section, 'gps-start-time')
end = min(globalv.NOW, config.getint(section, 'gps-end-time'))
# get parameters
params = dict(config.nditems(section))
# parse name
name = params.pop('name', section)
if re.match(r'state[-\s]', name):
name = section[6:]
# get hours
hours = params.pop('hours', None)
if hours is not None:
segs = re.split(r'(,|, )', hours)[::2]
hours = []
offset = 0
for seg in segs:
try:
# parse hour segment
hours.append(list(map(float, seg.split('-', 1))))
except ValueError:
# parse time-zone
if seg == segs[-1]:
if seg.lower() == 'utc':
offset = 0
elif seg.lower() == 'local':
try:
ifo = config.get(DEFAULTSECT, 'ifo')
except NoOptionError:
raise ValueError("The relevant IFO must be "
"given either from the --ifo "
"command-line option, or the "
"[DEFAULT] section of any "
"INI file")
offset = get_timezone_offset(ifo, from_gps(start))
else:
offset = get_timezone_offset(seg, from_gps(start))
else:
raise
# apply time-zone
for i, (h0, h1) in enumerate(hours):
hours[i] = (h0 - offset / 3600., h1 - offset / 3600.)
# generate state
return cls(name, known=[(start, end)], hours=hours, **params)
def _fetch_segments(self, config=GWSummConfigParser(), **kwargs):
kwargs.setdefault('url', self.url)
segs = get_segments([self.definition], self.known, config=config,
**kwargs)[self.definition].round(contract=True)
self.known = segs.known
self.active = segs.active
return self
def _fetch_data(self, channel, thresh, op, config=GWSummConfigParser(),
**kwargs):
# if 0 is out-of-state, allowing padding gaps with 0.
if (
(op == '<' and thresh <= 0.) or
(op == '<=' and thresh < 0.) or
(op == '>' and thresh >= 0.) or
(op == '>=' and thresh > 0.) or
(op in ('=', '==') and thresh != 0.) or
(op == '!=' and thresh == 0.)
):
kwargs.setdefault('pad', 0.)
data = get_timeseries(channel, self.known, config=config, **kwargs)
for ts in data:
if isinstance(thresh, (float, int)) and ts.unit is not None:
thresh *= ts.unit
segs = MATHOPS[op](ts, thresh).to_dqflag()
try:
globalv.SEGMENTS[self.definition] += segs
except KeyError:
globalv.SEGMENTS[self.definition] = segs
return self._fetch_segments(query=False)
def _read_segments(self, filename):
segs = DataQualityFlag.read(filename, self.definition)
# XXX HACK around malformed segment files with no segment_summary table
if segs.active and not segs.known:
segs.known = type(segs.active)(segs.active)
if self.known:
self.known = self.known & segs.known
self.active = self.known & segs.active
else:
self.known = segs.known
self.active = segs.active
return self
[docs]
def fetch(self, config=GWSummConfigParser(),
segmentcache=None, segdb_error='raise',
datacache=None, datafind_error='raise', nproc=1, nds=None,
**kwargs):
"""Finalise this state by fetching its defining segments,
either from global memory, or from the segment database
"""
# check we haven't done this before
if self.ready:
return self
# fetch data
match = self.MATH_DEFINITION.search(str(self.definition))
if self.filename:
self._read_segments(self.filename)
elif self.definition and match is not None:
channel, thresh = self.definition.split(match.groups()[0])
channel = channel.rstrip()
thresh = float(thresh.strip())
self._fetch_data(channel, thresh, match.groups()[0], config=config,
cache=datacache, nproc=nproc, nds=nds,
datafind_error=datafind_error, **kwargs)
# fetch segments
elif self.definition:
self._fetch_segments(config=config, cache=segmentcache,
segdb_error=segdb_error, **kwargs)
# fetch null
else:
start = config.getfloat(DEFAULTSECT, 'gps-start-time')
end = config.getfloat(DEFAULTSECT, 'gps-end-time')
self.known = [(start, end)]
self.active = self.known
# restrict to given hours
if self.hours:
segs_ = SegmentList()
# get start day
d = Time(float(self.start), format='gps', scale='utc').datetime
d = d.replace(hour=0, minute=0, second=0, microsecond=0)
end_ = Time(float(self.end), format='gps', scale='utc').datetime
while d < end_:
# get GPS of day
t = to_gps(d)
# for each [start, end) hour pair, build a segment
for h0, h1 in self.hours:
segs_.append(Segment(t + h0 * 3600, t + h1*3600))
# increment and return
d += datetime.timedelta(1)
self.known &= segs_
self.active &= segs_
# FIXME
self.ready = True
return self
[docs]
def copy(self):
new = super(SummaryState, self).copy()
new.description = self.description
new.definition = self.definition
new.ready = self.ready
return new
copy.__doc__ = DataQualityFlag.copy.__doc__
def __str__(self):
return self.name