1#    Copyright 2015-2017 ARM Limited
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14#
15
16"""This module provides the Constraint class for handling
17filters and pivots in a modular fashion. This enable easy
18constraint application.
19
20An implementation of :mod:`trappy.plotter.AbstractDataPlotter`
21is expected to use the :mod:`trappy.plotter.Constraint.ConstraintManager`
22class to pivot and filter data and handle multiple column,
23trace and event inputs.
24
25The underlying object that encapsulates a unique set of
26a data column, data event and the requisite filters is
27:mod:`trappy.plotter.Constraint.Constraint`
28"""
29# pylint: disable=R0913
30from trappy.plotter.Utils import decolonize, normalize_list
31from trappy.utils import listify
32from trappy.plotter import AttrConf
33
34
35class Constraint(object):
36
37    """
38    What is a Constraint?
39        It is collection of data based on two rules:
40
41        - A Pivot
42
43        - A Set of Filters
44
45        - A Data Column
46
47    For Example a :mod:`pandas.DataFrame`
48
49    =====  ======== =========
50    Time    CPU       Latency
51    =====  ======== =========
52    1       x           <val>
53    2       y           <val>
54    3       z           <val>
55    4       a           <val>
56    =====  ======== =========
57
58    The resultant data will be split for each unique pivot value
59    with the filters applied
60    ::
61
62        result["x"] = pd.Series.filtered()
63        result["y"] = pd.Series.filtered()
64        result["z"] = pd.Series.filtered()
65        result["a"] = pd.Series.filtered()
66
67
68    :param trappy_trace: Input Data
69    :type trappy_trace: :mod:`pandas.DataFrame` or a class derived from
70        :mod:`trappy.trace.BareTrace`
71
72    :param column: The data column
73    :type column: str
74
75    :param template: TRAPpy Event
76    :type template: :mod:`trappy.base.Base` event
77
78    :param trace_index: The index of the trace/data in the overall constraint
79        data
80    :type trace_index: int
81
82    :param filters: A dictionary of filter values
83    :type filters: dict
84
85    :param window: A time window to apply to the constraint.
86    E.g. window=(5, 20) will constraint to events that happened
87    between Time=5 to Time=20.
88    :type window: tuple of two ints
89
90    """
91
92    def __init__(self, trappy_trace, pivot, column, template, trace_index,
93                 filters, window):
94        self._trappy_trace = trappy_trace
95        self._filters = filters
96        self._pivot = pivot
97        self.column = column
98        self._template = template
99        self._dup_resolved = False
100        self._data = self.populate_data_frame()
101
102        if window:
103            # We want to include the previous value before the window
104            # and the next after the window in the dataset
105            min_idx = self._data.loc[:window[0]].index.max()
106            max_idx = self._data.loc[window[1]:].index.min()
107            self._data = self._data.loc[min_idx:max_idx]
108
109        self.result = self._apply()
110        self.trace_index = trace_index
111
112    def _apply(self):
113        """This method applies the filter on the resultant data
114        on the input column.
115        """
116        data = self._data
117        result = {}
118
119        try:
120            values = data[self.column]
121        except KeyError:
122            return result
123
124        if self._pivot == AttrConf.PIVOT:
125            pivot_vals = [AttrConf.PIVOT_VAL]
126        else:
127            pivot_vals = self.pivot_vals(data)
128
129        for pivot_val in pivot_vals:
130            criterion = values.map(lambda x: True)
131
132            for key in self._filters.keys():
133                if key != self._pivot and key in data.columns:
134                    criterion = criterion & data[key].map(
135                        lambda x: x in self._filters[key])
136
137            if pivot_val != AttrConf.PIVOT_VAL:
138                criterion &= data[self._pivot] == pivot_val
139
140            val_series = values[criterion]
141            if len(val_series) != 0:
142                result[pivot_val] = val_series
143
144        return result
145
146    def _uses_trappy_trace(self):
147        if not self._template:
148            return False
149        else:
150            return True
151
152    def populate_data_frame(self):
153        """Return the populated :mod:`pandas.DataFrame`"""
154        if not self._uses_trappy_trace():
155            return self._trappy_trace
156
157        data_container = getattr(
158            self._trappy_trace,
159            decolonize(self._template.name))
160        return data_container.data_frame
161
162    def pivot_vals(self, data):
163        """This method returns the unique pivot values for the
164        Constraint's pivot and the column
165
166        :param data: Input Data
167        :type data: :mod:`pandas.DataFrame`
168        """
169        if self._pivot == AttrConf.PIVOT:
170            return AttrConf.PIVOT_VAL
171
172        if self._pivot not in data.columns:
173            return []
174
175        pivot_vals = set(data[self._pivot])
176        if self._pivot in self._filters:
177            pivot_vals = pivot_vals & set(self._filters[self._pivot])
178
179        return list(pivot_vals)
180
181    def __str__(self):
182
183        name = self.get_data_name()
184
185        if not self._uses_trappy_trace():
186            return name + ":" + str(self.column)
187
188        return name + ":" + \
189            self._template.name + ":" + self.column
190
191
192    def get_data_name(self):
193        """Get name for the data member. This method
194        relies on the "name" attribute for the name.
195        If the name attribute is absent, it associates
196        a numeric name to the respective data element
197
198        :returns: The name of the data member
199        """
200        if self._uses_trappy_trace():
201            if self._trappy_trace.name != "":
202                return self._trappy_trace.name
203            else:
204                return "Trace {}".format(self.trace_index)
205        else:
206            return "DataFrame {}".format(self.trace_index)
207
208class ConstraintManager(object):
209
210    """A class responsible for converting inputs
211    to constraints and also ensuring sanity
212
213
214    :param traces: Input Trace data
215    :type traces: :mod:`trappy.trace.BareTrace`, list(:mod:`trappy.trace.BareTrace`)
216        (or a class derived from :mod:`trappy.trace.BareTrace`)
217    :param columns: The column values from the corresponding
218        :mod:`pandas.DataFrame`
219    :type columns: str, list(str)
220    :param pivot: The column around which the data will be
221        pivoted:
222    :type pivot: str
223    :param templates: TRAPpy events
224    :type templates: :mod:`trappy.base.Base`
225    :param filters: A dictionary of values to be applied on the
226        respective columns
227    :type filters: dict
228    :param window: A time window to apply to the constraints
229    :type window: tuple of ints
230    :param zip_constraints: Permutes the columns and traces instead
231        of a one-to-one correspondence
232    :type zip_constraints: bool
233    """
234
235    def __init__(self, traces, columns, templates, pivot, filters,
236                 window=None, zip_constraints=True):
237
238        self._ip_vec = []
239        self._ip_vec.append(listify(traces))
240        self._ip_vec.append(listify(columns))
241        self._ip_vec.append(listify(templates))
242
243        self._lens = map(len, self._ip_vec)
244        self._max_len = max(self._lens)
245        self._pivot = pivot
246        self._filters = filters
247        self.window = window
248        self._constraints = []
249
250        self._trace_expanded = False
251        self._expand()
252        if zip_constraints:
253            self._populate_zip_constraints()
254        else:
255            self._populate_constraints()
256
257    def _expand(self):
258        """This is really important. We need to
259        meet the following criteria for constraint
260        expansion:
261        ::
262
263            Len[traces] == Len[columns] == Len[templates]
264
265        Or:
266        ::
267
268            Permute(
269                Len[traces] = 1
270                Len[columns] = 1
271                Len[templates] != 1
272            )
273
274            Permute(
275                   Len[traces] = 1
276                   Len[columns] != 1
277                   Len[templates] != 1
278            )
279        """
280        min_len = min(self._lens)
281        max_pos_comp = [
282            i for i,
283            j in enumerate(
284                self._lens) if j != self._max_len]
285
286        if self._max_len == 1 and min_len != 1:
287            raise RuntimeError("Essential Arg Missing")
288
289        if self._max_len > 1:
290
291            # Are they all equal?
292            if len(set(self._lens)) == 1:
293                return
294
295            if min_len > 1:
296                raise RuntimeError("Cannot Expand a list of Constraints")
297
298            for val in max_pos_comp:
299                if val == 0:
300                    self._trace_expanded = True
301                self._ip_vec[val] = normalize_list(self._max_len,
302                                                   self._ip_vec[val])
303
304    def _populate_constraints(self):
305        """Populate the constraints creating one for each column in
306        each trace
307
308        In a multi-trace, multicolumn scenario, constraints are created for
309        all the columns in each of the traces.  _populate_constraints()
310        creates one constraint for the first trace and first column, the
311        next for the second trace and second column,...  This function
312        creates a constraint for every combination of traces and columns
313        possible.
314        """
315
316        for trace_idx, trace in enumerate(self._ip_vec[0]):
317            for col in self._ip_vec[1]:
318                template = self._ip_vec[2][trace_idx]
319                constraint = Constraint(trace, self._pivot, col, template,
320                                        trace_idx, self._filters, self.window)
321                self._constraints.append(constraint)
322
323    def get_column_index(self, constraint):
324        return self._ip_vec[1].index(constraint.column)
325
326    def _populate_zip_constraints(self):
327        """Populate the expanded constraints
328
329        In a multitrace, multicolumn scenario, create constraints for
330        the first trace and the first column, second trace and second
331        column,... that is, as if you run zip(traces, columns)
332        """
333
334        for idx in range(self._max_len):
335            if self._trace_expanded:
336                trace_idx = 0
337            else:
338                trace_idx = idx
339
340            trace = self._ip_vec[0][idx]
341            col = self._ip_vec[1][idx]
342            template = self._ip_vec[2][idx]
343            self._constraints.append(
344                Constraint(trace, self._pivot, col, template, trace_idx,
345                           self._filters, self.window))
346
347    def generate_pivots(self, permute=False):
348        """Return a union of the pivot values
349
350        :param permute: Permute the Traces and Columns
351        :type permute: bool
352        """
353        pivot_vals = []
354        for constraint in self._constraints:
355            pivot_vals += constraint.result.keys()
356
357        p_list = list(set(pivot_vals))
358        traces = range(self._lens[0])
359
360        try:
361            sorted_plist = sorted(p_list, key=int)
362        except (ValueError, TypeError):
363            try:
364                sorted_plist = sorted(p_list, key=lambda x: int(x, 16))
365            except (ValueError, TypeError):
366                sorted_plist = sorted(p_list)
367
368        if permute:
369            pivot_gen = ((trace_idx, pivot) for trace_idx in traces for pivot in sorted_plist)
370            return pivot_gen, len(sorted_plist) * self._lens[0]
371        else:
372            return sorted_plist, len(sorted_plist)
373
374    def constraint_labels(self):
375        """
376        :return: string to represent the
377            set of Constraints
378
379        """
380        return map(str, self._constraints)
381
382    def __len__(self):
383        return len(self._constraints)
384
385    def __iter__(self):
386        return iter(self._constraints)
387