• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3#   Copyright 2019 - The Android Open Source Project
4#
5#   Licensed under the Apache License, Version 2.0 (the 'License');
6#   you may not use this file except in compliance with the License.
7#   You may obtain a copy of the License at
8#
9#       http://www.apache.org/licenses/LICENSE-2.0
10#
11#   Unless required by applicable law or agreed to in writing, software
12#   distributed under the License is distributed on an 'AS IS' BASIS,
13#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14#   See the License for the specific language governing permissions and
15#   limitations under the License.
16
17import math
18import numpy as np
19
20# Metrics timestamp keys
21START_TIMESTAMP = 'start'
22END_TIMESTAMP = 'end'
23
24# Unit type constants
25CURRENT = 'current'
26POWER = 'power'
27TIME = 'time'
28VOLTAGE = 'voltage'
29
30# Unit constants
31MILLIVOLT = 'mV'
32VOLT = 'V'
33MILLIAMP = 'mA'
34AMP = 'A'
35AMPERE = AMP
36MILLIWATT = 'mW'
37WATT = 'W'
38MILLISECOND = 'ms'
39SECOND = 's'
40MINUTE = 'm'
41HOUR = 'h'
42
43CONVERSION_TABLES = {
44    CURRENT: {
45        MILLIAMP: 0.001,
46        AMP: 1
47    },
48    POWER: {
49        MILLIWATT: 0.001,
50        WATT: 1
51    },
52    TIME: {
53        MILLISECOND: 0.001,
54        SECOND: 1,
55        MINUTE: 60,
56        HOUR: 3600
57    },
58    VOLTAGE: {
59        MILLIVOLT: 0.001,
60        VOLT : 1
61    }
62}
63
64
65class Metric(object):
66    """Base class for describing power measurement values. Each object contains
67    an value and a unit. Enables some basic arithmetic operations with other
68    measurements of the same unit type.
69
70    Attributes:
71        value: Numeric value of the measurement
72        _unit_type: Unit type of the measurement (e.g. current, power)
73        unit: Unit of the measurement (e.g. W, mA)
74    """
75
76    def __init__(self, value, unit_type, unit, name=None):
77        if unit_type not in CONVERSION_TABLES:
78            raise TypeError(
79                '%s is not a valid unit type, valid unit types are %s' % (
80                    unit_type, str(CONVERSION_TABLES.keys)))
81        self.value = value
82        self.unit = unit
83        self.name = name
84        self._unit_type = unit_type
85
86    # Convenience constructor methods
87    @staticmethod
88    def amps(amps, name=None):
89        """Create a new current measurement, in amps."""
90        return Metric(amps, CURRENT, AMP, name=name)
91
92    @staticmethod
93    def watts(watts, name=None):
94        """Create a new power measurement, in watts."""
95        return Metric(watts, POWER, WATT, name=name)
96
97    @staticmethod
98    def seconds(seconds, name=None):
99        """Create a new time measurement, in seconds."""
100        return Metric(seconds, TIME, SECOND, name=name)
101
102    # Comparison methods
103
104    def __eq__(self, other):
105        return self.value == other.to_unit(self.unit).value
106
107    def __lt__(self, other):
108        return self.value < other.to_unit(self.unit).value
109
110    def __le__(self, other):
111        return self == other or self < other
112
113    # Addition and subtraction with other measurements
114
115    def __add__(self, other):
116        """Adds measurements of compatible unit types. The result will be in the
117        same units as self.
118        """
119        return Metric(self.value + other.to_unit(self.unit).value,
120                      self._unit_type, self.unit, name=self.name)
121
122    def __sub__(self, other):
123        """Subtracts measurements of compatible unit types. The result will be
124        in the same units as self.
125        """
126        return Metric(self.value - other.to_unit(self.unit).value,
127                      self._unit_type, self.unit, name=self.name)
128
129    # String representation
130
131    def __str__(self):
132        return '%g%s' % (self.value, self.unit)
133
134    def __repr__(self):
135        return str(self)
136
137    def to_unit(self, new_unit):
138        """Create an equivalent measurement under a different unit.
139        e.g. 0.5W -> 500mW
140
141        Args:
142            new_unit: Target unit. Must be compatible with current unit.
143
144        Returns: A new measurement with the converted value and unit.
145        """
146        try:
147            new_value = self.value * (
148                CONVERSION_TABLES[self._unit_type][self.unit] /
149                CONVERSION_TABLES[self._unit_type][new_unit])
150        except KeyError:
151            raise TypeError('Incompatible units: %s, %s' %
152                            (self.unit, new_unit))
153        return Metric(new_value, self._unit_type, new_unit, self.name)
154
155
156def import_raw_data(path):
157    """Create a generator from a Monsoon data file.
158
159    Args:
160        path: path to raw data file
161
162    Returns: generator that yields (timestamp, sample) per line
163    """
164    with open(path, 'r') as f:
165        for line in f:
166            time, sample = line.split()
167            yield float(time[:-1]), float(sample)
168
169
170def generate_percentiles(monsoon_file, timestamps, percentiles):
171    """Generates metrics .
172
173    Args:
174        monsoon_file: monsoon-like file where each line has two
175            numbers separated by a space, in the format:
176            seconds_since_epoch amperes
177            seconds_since_epoch amperes
178        timestamps: dict following the output format of
179            instrumentation_proto_parser.get_test_timestamps()
180        percentiles: percentiles to be returned
181    """
182    if timestamps is None:
183        timestamps = {}
184    test_starts = {}
185    test_ends = {}
186    for seg_name, times in timestamps.items():
187        if START_TIMESTAMP in times and END_TIMESTAMP in times:
188            test_starts[seg_name] = Metric(
189                times[START_TIMESTAMP], TIME, MILLISECOND).to_unit(
190                SECOND).value
191            test_ends[seg_name] = Metric(
192                times[END_TIMESTAMP], TIME, MILLISECOND).to_unit(
193                SECOND).value
194
195    arrays = {}
196    for seg_name in test_starts:
197        arrays[seg_name] = []
198
199    with open(monsoon_file, 'r') as m:
200        for line in m:
201            timestamp = float(line.strip().split()[0])
202            value = float(line.strip().split()[1])
203            for seg_name in arrays.keys():
204                if test_starts[seg_name] <= timestamp <= test_ends[seg_name]:
205                    arrays[seg_name].append(value)
206
207    results = {}
208    for seg_name in arrays:
209        if len(arrays[seg_name]) == 0:
210            continue
211
212        pairs = zip(percentiles, np.percentile(arrays[seg_name],
213                                               percentiles))
214        results[seg_name] = [
215            Metric.amps(p[1], 'percentile_%s' % p[0]).to_unit(MILLIAMP) for p in
216            pairs
217        ]
218    return results
219
220
221def generate_test_metrics(raw_data, timestamps=None,
222                          voltage=None):
223    """Split the data into individual test metrics, based on the timestamps
224    given as a dict.
225
226    Args:
227        raw_data: raw data as list or generator of (timestamp, sample)
228        timestamps: dict following the output format of
229            instrumentation_proto_parser.get_test_timestamps()
230        voltage: voltage used during measurements
231    """
232
233    # Initialize metrics for each test
234    if timestamps is None:
235        timestamps = {}
236    test_starts = {}
237    test_ends = {}
238    test_metrics = {}
239    for seg_name, times in timestamps.items():
240        if START_TIMESTAMP in times and END_TIMESTAMP in times:
241            test_metrics[seg_name] = PowerMetrics(voltage)
242            test_starts[seg_name] = Metric(
243                times[START_TIMESTAMP], TIME, MILLISECOND).to_unit(
244                SECOND).value
245            test_ends[seg_name] = Metric(
246                times[END_TIMESTAMP], TIME, MILLISECOND).to_unit(
247                SECOND).value
248
249    # Assign data to tests based on timestamps
250    for timestamp, amps in raw_data:
251        for seg_name in test_metrics.keys():
252            if test_starts[seg_name] <= timestamp <= test_ends[seg_name]:
253                test_metrics[seg_name].update_metrics(amps)
254
255    result = {}
256    for seg_name, power_metrics in test_metrics.items():
257        result[seg_name] = [
258            power_metrics.avg_current,
259            power_metrics.max_current,
260            power_metrics.min_current,
261            power_metrics.stdev_current,
262            power_metrics.avg_power]
263    return result
264
265
266class PowerMetrics(object):
267    """Class for processing raw power metrics generated by Monsoon measurements.
268    Provides useful metrics such as average current, max current, and average
269    power. Can generate individual test metrics.
270
271    See section "Numeric metrics" below for available metrics.
272    """
273
274    def __init__(self, voltage):
275        """Create a PowerMetrics.
276
277        Args:
278            voltage: Voltage of the measurement
279        """
280        self._voltage = voltage
281        self._num_samples = 0
282        self._sum_currents = 0
283        self._sum_squares = 0
284        self._max_current = None
285        self._min_current = None
286        self.test_metrics = {}
287
288    def update_metrics(self, sample):
289        """Update the running metrics with the current sample.
290
291        Args:
292            sample: A current sample in Amps.
293        """
294        self._num_samples += 1
295        self._sum_currents += sample
296        self._sum_squares += sample ** 2
297        if self._max_current is None or sample > self._max_current:
298            self._max_current = sample
299        if self._min_current is None or sample < self._min_current:
300            self._min_current = sample
301
302    # Numeric metrics
303    @property
304    def avg_current(self):
305        """Average current, in milliamps."""
306        if not self._num_samples:
307            return Metric.amps(0).to_unit(MILLIAMP)
308        return (Metric.amps(self._sum_currents / self._num_samples,
309                            'avg_current')
310                .to_unit(MILLIAMP))
311
312    @property
313    def max_current(self):
314        """Max current, in milliamps."""
315        return Metric.amps(self._max_current or 0, 'max_current').to_unit(
316            MILLIAMP)
317
318    @property
319    def min_current(self):
320        """Min current, in milliamps."""
321        return Metric.amps(self._min_current or 0, 'min_current').to_unit(
322            MILLIAMP)
323
324    @property
325    def stdev_current(self):
326        """Standard deviation of current values, in milliamps."""
327        if self._num_samples < 2:
328            return Metric.amps(0, 'stdev_current').to_unit(MILLIAMP)
329        stdev = math.sqrt(
330            (self._sum_squares - (
331                self._num_samples * self.avg_current.to_unit(AMP).value ** 2))
332            / (self._num_samples - 1))
333        return Metric.amps(stdev, 'stdev_current').to_unit(MILLIAMP)
334
335    @property
336    def avg_power(self):
337        """Average power, in milliwatts."""
338        return Metric.watts(self.avg_current.to_unit(AMP).value * self._voltage,
339                            'avg_power').to_unit(MILLIWATT)
340