1#!/usr/bin/env python3
  2
  3"""Functions to merge together events which overlap in time and are sufficiently similar
  4to be considered a single event.
  5
  6'chronic' events are those that occur almost every orbit and may be reported as a single
  7event in a report, event if there are a few orbits where they don't occur.
  8
  9"""
 10
 11import copy
 12import logging
 13import operator
 14import collections
 15from datetime import timedelta
 16from functools import reduce
 17
 18
 19def merge(in_events, chronic_count=None):
 20    """Merge together a list of per-orbit events from `in_events`,
 21    squashing together events whose durations overlap each other.
 22    If any event class occurs more than `chronic_count` times it is classed as a chronic event
 23    and removed from the normal returned list.
 24    Return value is a tuple of (merged_events, chronic_events).
 25    """
 26
 27    # logging.debug('chronic_count '+str(chronic_count))
 28    # todo: copy events before merging them so we don't modify the originals
 29    def merge_hashable(a):
 30        """Return a hashable value representing the class of event `a`."""
 31
 32        if a.event_classname in ('MHS-DATA-GAP',
 33                                 'MHS-SPIN-STATE',
 34                                 'MHS-OBCT-JUMP',
 35                                 'MHS-NEDT-PERF',
 36                                 'MHS-NEDT-PEAK',
 37                                 'MHS-NEDT-TREND'):
 38            return a.event_classname
 39
 40        elif a.event_classname == 'MHS-INSTABILITY':
 41            try:
 42                return (a.event_classname,
 43                        a.instance_properties['parameter'],
 44                        a.instance_properties['test'])
 45            except:
 46                raise Exception('Incomplete properties in event ' + str(a.event_id))
 47
 48        elif a.event_classname.startswith('OPERATOR-'):
 49            # never merge operator events
 50            return a.event_id
 51
 52        else:
 53            # default - this is ok for events where 2 or more events with the same class,
 54            # whose time periods overlap, can be displayed as a single event in the
 55            # events table of a report.
 56            return a.event_classname
 57
 58    events = collections.defaultdict(list)
 59    # logging.debug('unsorted:')
 60    for e in in_events:
 61        # logging.debug(str(e))
 62        c = merge_hashable(e)
 63        events[c].append(e)
 64
 65    for l in events.values():
 66        l.sort(key=operator.attrgetter('start_time'))
 67
 68    def merge_test(a, b):
 69        """Test if events `a` and `b` can be merged together.
 70        (for per-orbit events)."""
 71
 72        if a.stop_time is not None and abs(a.stop_time - b.start_time) < timedelta(seconds=3):
 73            return True
 74        else:
 75            return False
 76
 77    def merge_events(a, c):
 78        """Merge event `c` into `a` and return the combined event."""
 79
 80        b = copy.deepcopy(a)
 81        b.stop_time = c.stop_time
 82
 83        if b.event_classname == 'MHS-INSTABILITY':
 84            if b.instance_properties['test'] in ('max-eq', 'max-ge'):
 85                b.instance_properties['value'] = max(a.instance_properties['value'],
 86                                                     c.instance_properties['value'])
 87            elif b.instance_properties['test'] == 'min-eq':
 88                b.instance_properties['value'] = min(a.instance_properties['value'],
 89                                                     c.instance_properties['value'])
 90            elif b.instance_properties['test'] in ('avg-percent-change', 'weekly-variation'):
 91                if (float(a.instance_properties['value']) /
 92                    float(a.instance_properties['prev_value'])) < \
 93                    (float(c.instance_properties['value']) /
 94                     float(c.instance_properties['prev_value'])):
 95                    b.instance_properties['value'] = c.instance_properties['value']
 96                    b.instance_properties['prev_value'] = c.instance_properties['prev_value']
 97
 98        return b
 99
100    res = []
101    if chronic_count is not None:
102        chronic = []
103        # build list of parameters that are chronic
104        chronic_parameters = set()
105        for h, l in events.items():
106            # logging.debug('a test '+str(h))
107            if isinstance(h, tuple) and len(l) >= chronic_count:
108                logging.debug('Adding chronic parameter ' + h[1])
109                chronic_parameters.add(h[1])
110
111    for h, l in events.items():
112        # logging.debug('datatype '+str(h)+' len '+str(len(l)))
113        if chronic_count is not None and \
114                ((isinstance(h, tuple) and h[1] in chronic_parameters) or
115                 len(l) >= chronic_count):
116
117            # logging.debug('    is chronic')
118            # handle chronic events
119            ce = reduce(merge_events, l)
120            # ce = l[0]
121            # for e in l[1:]:
122            #     merge_events(ce,e)
123
124            setattr(ce, 'occurances', len(l))
125            chronic.append(ce)
126
127        else:
128            # handle regular events by merging together mergeable events
129            nl = []
130            acc = None
131            # logging.debug('merging type '+str(c))
132            for e in l:
133                # logging.debug(str(e))
134                if acc is None:
135                    acc = e
136                    continue
137
138                if merge_test(acc, e):
139                    merge_events(acc, e)
140                else:
141                    nl.append(acc)
142                    acc = e
143
144            if acc is not None:
145                nl.append(acc)
146
147            res.extend(nl)
148
149    res.sort(key=operator.attrgetter('start_time'))
150
151    if chronic_count is None:
152        return res
153
154    else:
155        return res, chronic