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 18 19# Metrics timestamp keys 20START_TIMESTAMP = 'start' 21END_TIMESTAMP = 'end' 22 23# Unit type constants 24CURRENT = 'current' 25POWER = 'power' 26TIME = 'time' 27VOLTAGE = 'voltage' 28 29# Unit constants 30MILLIVOLT = 'mV' 31VOLT = 'V' 32MILLIAMP = 'mA' 33AMP = 'A' 34AMPERE = AMP 35MILLIWATT = 'mW' 36WATT = 'W' 37MILLISECOND = 'ms' 38SECOND = 's' 39MINUTE = 'm' 40HOUR = 'h' 41 42CONVERSION_TABLES = { 43 CURRENT: { 44 MILLIAMP: 0.001, 45 AMP: 1 46 }, 47 POWER: { 48 MILLIWATT: 0.001, 49 WATT: 1 50 }, 51 TIME: { 52 MILLISECOND: 0.001, 53 SECOND: 1, 54 MINUTE: 60, 55 HOUR: 3600 56 }, 57 VOLTAGE: { 58 MILLIVOLT: 0.001, 59 VOLT : 1 60 } 61} 62 63 64class Metric(object): 65 """Base class for describing power measurement values. Each object contains 66 an value and a unit. Enables some basic arithmetic operations with other 67 measurements of the same unit type. 68 69 Attributes: 70 value: Numeric value of the measurement 71 _unit_type: Unit type of the measurement (e.g. current, power) 72 unit: Unit of the measurement (e.g. W, mA) 73 """ 74 75 def __init__(self, value, unit_type, unit, name=None): 76 if unit_type not in CONVERSION_TABLES: 77 raise TypeError( 78 '%s is not a valid unit type, valid unit types are %s' % ( 79 unit_type, str(CONVERSION_TABLES.keys))) 80 self.value = value 81 self.unit = unit 82 self.name = name 83 self._unit_type = unit_type 84 85 # Convenience constructor methods 86 @staticmethod 87 def amps(amps, name=None): 88 """Create a new current measurement, in amps.""" 89 return Metric(amps, CURRENT, AMP, name=name) 90 91 @staticmethod 92 def watts(watts, name=None): 93 """Create a new power measurement, in watts.""" 94 return Metric(watts, POWER, WATT, name=name) 95 96 @staticmethod 97 def seconds(seconds, name=None): 98 """Create a new time measurement, in seconds.""" 99 return Metric(seconds, TIME, SECOND, name=name) 100 101 # Comparison methods 102 103 def __eq__(self, other): 104 return self.value == other.to_unit(self.unit).value 105 106 def __lt__(self, other): 107 return self.value < other.to_unit(self.unit).value 108 109 def __le__(self, other): 110 return self == other or self < other 111 112 # Addition and subtraction with other measurements 113 114 def __add__(self, other): 115 """Adds measurements of compatible unit types. The result will be in the 116 same units as self. 117 """ 118 return Metric(self.value + other.to_unit(self.unit).value, 119 self._unit_type, self.unit, name=self.name) 120 121 def __sub__(self, other): 122 """Subtracts measurements of compatible unit types. The result will be 123 in the same units as self. 124 """ 125 return Metric(self.value - other.to_unit(self.unit).value, 126 self._unit_type, self.unit, name=self.name) 127 128 # String representation 129 130 def __str__(self): 131 return '%g%s' % (self.value, self.unit) 132 133 def __repr__(self): 134 return str(self) 135 136 def to_unit(self, new_unit): 137 """Create an equivalent measurement under a different unit. 138 e.g. 0.5W -> 500mW 139 140 Args: 141 new_unit: Target unit. Must be compatible with current unit. 142 143 Returns: A new measurement with the converted value and unit. 144 """ 145 try: 146 new_value = self.value * ( 147 CONVERSION_TABLES[self._unit_type][self.unit] / 148 CONVERSION_TABLES[self._unit_type][new_unit]) 149 except KeyError: 150 raise TypeError('Incompatible units: %s, %s' % 151 (self.unit, new_unit)) 152 return Metric(new_value, self._unit_type, new_unit, self.name) 153 154 155def import_raw_data(path): 156 """Create a generator from a Monsoon data file. 157 158 Args: 159 path: path to raw data file 160 161 Returns: generator that yields (timestamp, sample) per line 162 """ 163 with open(path, 'r') as f: 164 for line in f: 165 time, sample = line.split() 166 yield float(time[:-1]), float(sample) 167 168 169def generate_test_metrics(raw_data, timestamps=None, 170 voltage=None): 171 """Split the data into individual test metrics, based on the timestamps 172 given as a dict. 173 174 Args: 175 raw_data: raw data as list or generator of (timestamp, sample) 176 timestamps: dict following the output format of 177 instrumentation_proto_parser.get_test_timestamps() 178 voltage: voltage used during measurements 179 """ 180 181 # Initialize metrics for each test 182 if timestamps is None: 183 timestamps = {} 184 test_starts = {} 185 test_ends = {} 186 test_metrics = {} 187 for seg_name, times in timestamps.items(): 188 test_metrics[seg_name] = PowerMetrics(voltage) 189 try: 190 test_starts[seg_name] = Metric( 191 times[START_TIMESTAMP], TIME, MILLISECOND).to_unit( 192 SECOND).value 193 except KeyError: 194 raise ValueError( 195 'Missing start timestamp for test scenario "%s". Refer to ' 196 'instrumentation_proto.txt for details.' % seg_name) 197 try: 198 test_ends[seg_name] = Metric( 199 times[END_TIMESTAMP], TIME, MILLISECOND).to_unit( 200 SECOND).value 201 except KeyError: 202 raise ValueError( 203 'Missing end timestamp for test scenario "%s". Test ' 204 'scenario may have terminated with errors. Refer to ' 205 'instrumentation_proto.txt for details.' % seg_name) 206 207 # Assign data to tests based on timestamps 208 for timestamp, amps in raw_data: 209 for seg_name in timestamps: 210 if test_starts[seg_name] <= timestamp <= test_ends[seg_name]: 211 test_metrics[seg_name].update_metrics(amps) 212 213 result = {} 214 for seg_name, power_metrics in test_metrics.items(): 215 result[seg_name] = [ 216 power_metrics.avg_current, 217 power_metrics.max_current, 218 power_metrics.min_current, 219 power_metrics.stdev_current, 220 power_metrics.avg_power] 221 return result 222 223 224class PowerMetrics(object): 225 """Class for processing raw power metrics generated by Monsoon measurements. 226 Provides useful metrics such as average current, max current, and average 227 power. Can generate individual test metrics. 228 229 See section "Numeric metrics" below for available metrics. 230 """ 231 232 def __init__(self, voltage): 233 """Create a PowerMetrics. 234 235 Args: 236 voltage: Voltage of the measurement 237 """ 238 self._voltage = voltage 239 self._num_samples = 0 240 self._sum_currents = 0 241 self._sum_squares = 0 242 self._max_current = None 243 self._min_current = None 244 self.test_metrics = {} 245 246 def update_metrics(self, sample): 247 """Update the running metrics with the current sample. 248 249 Args: 250 sample: A current sample in Amps. 251 """ 252 self._num_samples += 1 253 self._sum_currents += sample 254 self._sum_squares += sample ** 2 255 if self._max_current is None or sample > self._max_current: 256 self._max_current = sample 257 if self._min_current is None or sample < self._min_current: 258 self._min_current = sample 259 260 # Numeric metrics 261 @property 262 def avg_current(self): 263 """Average current, in milliamps.""" 264 if not self._num_samples: 265 return Metric.amps(0).to_unit(MILLIAMP) 266 return (Metric.amps(self._sum_currents / self._num_samples, 267 'avg_current') 268 .to_unit(MILLIAMP)) 269 270 @property 271 def max_current(self): 272 """Max current, in milliamps.""" 273 return Metric.amps(self._max_current or 0, 'max_current').to_unit( 274 MILLIAMP) 275 276 @property 277 def min_current(self): 278 """Min current, in milliamps.""" 279 return Metric.amps(self._min_current or 0, 'min_current').to_unit( 280 MILLIAMP) 281 282 @property 283 def stdev_current(self): 284 """Standard deviation of current values, in milliamps.""" 285 if self._num_samples < 2: 286 return Metric.amps(0, 'stdev_current').to_unit(MILLIAMP) 287 stdev = math.sqrt( 288 (self._sum_squares - ( 289 self._num_samples * self.avg_current.to_unit(AMP).value ** 2)) 290 / (self._num_samples - 1)) 291 return Metric.amps(stdev, 'stdev_current').to_unit(MILLIAMP) 292 293 @property 294 def avg_power(self): 295 """Average power, in milliwatts.""" 296 return Metric.watts(self.avg_current.to_unit(AMP).value * self._voltage, 297 'avg_power').to_unit(MILLIWATT) 298