Constraint.py revision daf270b6b7176619f542a13a9403d0ed9cf4172d
1#    Copyright 2015-2016 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
33from trappy.utils import handle_duplicate_index
34
35
36class Constraint(object):
37
38    """
39    What is a Constraint?
40        It is collection of data based on two rules:
41
42        - A Pivot
43
44        - A Set of Filters
45
46        - A Data Column
47
48    For Example a :mod:`pandas.DataFrame`
49
50    =====  ======== =========
51    Time    CPU       Latency
52    =====  ======== =========
53    1       x           <val>
54    2       y           <val>
55    3       z           <val>
56    4       a           <val>
57    =====  ======== =========
58
59    The resultant data will be split for each unique pivot value
60    with the filters applied
61    ::
62
63        result["x"] = pd.Series.filtered()
64        result["y"] = pd.Series.filtered()
65        result["z"] = pd.Series.filtered()
66        result["a"] = pd.Series.filtered()
67
68
69    :param trappy_trace: Input Data
70    :type trappy_trace: :mod:`pandas.DataFrame`, :mod:`trappy.trace.FTrace`
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
86    def __init__(self, trappy_trace, pivot, column, template, trace_index,
87                 filters):
88        self._trappy_trace = trappy_trace
89        self._filters = filters
90        self._pivot = pivot
91        self.column = column
92        self._template = template
93        self._dup_resolved = False
94        self._data = self.populate_data_frame()
95
96        try:
97            self.result = self._apply()
98        except ValueError:
99            if not self._dup_resolved:
100                self._handle_duplicate_index()
101                try:
102                    self.result = self._apply()
103                except:
104                    raise ValueError("Unable to handle duplicates")
105
106        self.trace_index = trace_index
107
108    def _apply(self):
109        """This method applies the filter on the resultant data
110        on the input column.
111        """
112        data = self._data
113        result = {}
114
115        try:
116            values = data[self.column]
117        except KeyError:
118            return result
119
120        if self._pivot == AttrConf.PIVOT:
121            criterion = values.map(lambda x: True)
122            for key in self._filters.keys():
123                if key in data.columns:
124                    criterion = criterion & data[key].map(
125                        lambda x: x in self._filters[key])
126                    values = values[criterion]
127            result[AttrConf.PIVOT_VAL] = values
128            return result
129
130        pivot_vals = self.pivot_vals(data)
131
132        for pivot_val in pivot_vals:
133            criterion = values.map(lambda x: True)
134
135            for key in self._filters.keys():
136                if key != self._pivot and key in data.columns:
137                    criterion = criterion & data[key].map(
138                        lambda x: x in self._filters[key])
139                    values = values[criterion]
140
141            val_series = values[data[self._pivot] == pivot_val]
142            if len(val_series) != 0:
143               result[pivot_val] = val_series
144
145        return result
146
147    def _handle_duplicate_index(self):
148        """Handle duplicate values in index"""
149
150        self._data = handle_duplicate_index(self._data)
151        self._dup_resolved = True
152
153    def _uses_trappy_trace(self):
154        if not self._template:
155            return False
156        else:
157            return True
158
159    def populate_data_frame(self):
160        """Return the populated :mod:`pandas.DataFrame`"""
161        if not self._uses_trappy_trace():
162            return self._trappy_trace
163
164        data_container = getattr(
165            self._trappy_trace,
166            decolonize(self._template.name))
167        return data_container.data_frame
168
169    def pivot_vals(self, data):
170        """This method returns the unique pivot values for the
171        Constraint's pivot and the column
172
173        :param data: Input Data
174        :type data: :mod:`pandas.DataFrame`
175        """
176        if self._pivot == AttrConf.PIVOT:
177            return AttrConf.PIVOT_VAL
178
179        if self._pivot not in data.columns:
180            return []
181
182        pivot_vals = set(data[self._pivot])
183        if self._pivot in self._filters:
184            pivot_vals = pivot_vals & set(self._filters[self._pivot])
185
186        return list(pivot_vals)
187
188    def __str__(self):
189
190        name = self.get_data_name()
191
192        if not self._uses_trappy_trace():
193            return name + ":" + self.column
194
195        return name + ":" + \
196            self._template.name + ":" + self.column
197
198
199    def get_data_name(self):
200        """Get name for the data member. This method
201        relies on the "name" attribute for the name.
202        If the name attribute is absent, it associates
203        a numeric name to the respective data element
204
205        :returns: The name of the data member
206        """
207        if self._uses_trappy_trace():
208            if self._trappy_trace.name != "":
209                return self._trappy_trace.name
210            else:
211                return "Trace {}".format(self.trace_index)
212        else:
213            return "DataFrame {}".format(self.trace_index)
214
215class ConstraintManager(object):
216
217    """A class responsible for converting inputs
218    to constraints and also ensuring sanity
219
220
221    :param traces: Input Trace data
222    :type traces: :mod:`trappy.trace.FTrace`, list(:mod:`trappy.trace.FTrace`)
223    :param columns: The column values from the corresponding
224        :mod:`pandas.DataFrame`
225    :type columns: str, list(str)
226    :param pivot: The column around which the data will be
227        pivoted:
228    :type pivot: str
229    :param filters: A dictionary of values to be applied on the
230        respective columns
231    :type filters: dict
232    :param zip_constraints: Permutes the columns and traces instead
233        of a one-to-one correspondence
234    :type zip_constraints: bool
235    """
236
237    def __init__(self, traces, columns, templates, pivot, filters,
238                 zip_constraints=True):
239
240        self._ip_vec = []
241        self._ip_vec.append(listify(traces))
242        self._ip_vec.append(listify(columns))
243        self._ip_vec.append(listify(templates))
244
245        self._lens = map(len, self._ip_vec)
246        self._max_len = max(self._lens)
247        self._pivot = pivot
248        self._filters = filters
249        self._constraints = []
250
251        self._trace_expanded = False
252        self._expand()
253        if zip_constraints:
254            self._populate_zip_constraints()
255        else:
256            self._populate_constraints()
257
258    def _expand(self):
259        """This is really important. We need to
260        meet the following criteria for constraint
261        expansion:
262        ::
263
264            Len[traces] == Len[columns] == Len[templates]
265
266        Or:
267        ::
268
269            Permute(
270                Len[traces] = 1
271                Len[columns] = 1
272                Len[templates] != 1
273            )
274
275            Permute(
276                   Len[traces] = 1
277                   Len[columns] != 1
278                   Len[templates] != 1
279            )
280        """
281        min_len = min(self._lens)
282        max_pos_comp = [
283            i for i,
284            j in enumerate(
285                self._lens) if j != self._max_len]
286
287        if self._max_len == 1 and min_len != 1:
288            raise RuntimeError("Essential Arg Missing")
289
290        if self._max_len > 1:
291
292            # Are they all equal?
293            if len(set(self._lens)) == 1:
294                return
295
296            if min_len > 1:
297                raise RuntimeError("Cannot Expand a list of Constraints")
298
299            for val in max_pos_comp:
300                if val == 0:
301                    self._trace_expanded = True
302                self._ip_vec[val] = normalize_list(self._max_len,
303                                                   self._ip_vec[val])
304
305    def _populate_constraints(self):
306        """Populate the constraints creating one for each column in
307        each trace
308
309        In a multi-trace, multicolumn scenario, constraints are created for
310        all the columns in each of the traces.  _populate_constraints()
311        creates one constraint for the first trace and first column, the
312        next for the second trace and second column,...  This function
313        creates a constraint for every combination of traces and columns
314        possible.
315        """
316
317        for trace_idx, trace in enumerate(self._ip_vec[0]):
318            for col in self._ip_vec[1]:
319                template = self._ip_vec[2][trace_idx]
320                constraint = Constraint(trace, self._pivot, col, template,
321                                        trace_idx, self._filters)
322                self._constraints.append(constraint)
323
324    def get_column_index(self, constraint):
325        return self._ip_vec[1].index(constraint.column)
326
327    def _populate_zip_constraints(self):
328        """Populate the expanded constraints
329
330        In a multitrace, multicolumn scenario, create constraints for
331        the first trace and the first column, second trace and second
332        column,... that is, as if you run zip(traces, columns)
333        """
334
335        for idx in range(self._max_len):
336            if self._trace_expanded:
337                trace_idx = 0
338            else:
339                trace_idx = idx
340
341            trace = self._ip_vec[0][idx]
342            col = self._ip_vec[1][idx]
343            template = self._ip_vec[2][idx]
344            self._constraints.append(
345                Constraint(trace, self._pivot, col, template, trace_idx,
346                           self._filters))
347
348    def generate_pivots(self, permute=False):
349        """Return a union of the pivot values
350
351        :param permute: Permute the Traces and Columns
352        :type permute: bool
353        """
354        pivot_vals = []
355        for constraint in self._constraints:
356            pivot_vals += constraint.result.keys()
357
358        p_list = list(set(pivot_vals))
359        traces = range(self._lens[0])
360
361        try:
362            sorted_plist = sorted(p_list, key=int)
363        except ValueError, TypeError:
364            try:
365                sorted_plist = sorted(p_list, key=lambda x: int(x, 16))
366            except ValueError, TypeError:
367                sorted_plist = sorted(p_list)
368
369        if permute:
370            pivot_gen = ((trace_idx, pivot) for trace_idx in traces for pivot in sorted_plist)
371            return pivot_gen, len(sorted_plist) * self._lens[0]
372        else:
373            return sorted_plist, len(sorted_plist)
374
375    def constraint_labels(self):
376        """
377        :return: string to represent the
378            set of Constraints
379
380        """
381        return map(str, self._constraints)
382
383    def __len__(self):
384        return len(self._constraints)
385
386    def __iter__(self):
387        return iter(self._constraints)
388