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