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