diff options
-rwxr-xr-x | bin/eval-online-model-accuracy.py | 45 | ||||
-rwxr-xr-x | bin/eval-rel-energy.py | 22 | ||||
-rw-r--r-- | lib/dfatool.py | 546 | ||||
-rwxr-xr-x | lib/plotter.py | 113 | ||||
-rw-r--r-- | lib/runner.py | 82 |
5 files changed, 420 insertions, 388 deletions
diff --git a/bin/eval-online-model-accuracy.py b/bin/eval-online-model-accuracy.py index 75e2a51..49157e5 100755 --- a/bin/eval-online-model-accuracy.py +++ b/bin/eval-online-model-accuracy.py @@ -22,18 +22,14 @@ Options: """ import getopt -import json import re -import runner import sys -import time -import io import itertools import yaml from automata import PTA -from codegen import * -from harness import OnboardTimerHarness +from codegen import get_simulated_accountingmethod from dfatool import regression_measures +import numpy as np opt = dict() @@ -64,16 +60,16 @@ if __name__ == '__main__': raw_opts, args = getopt.getopt(sys.argv[1:], "", optspec.split(' ')) opt_default = { - 'depth' : 3, - 'sleep' : 0, - 'timer-freq' : 1e6, - 'timer-type' : 'uint16_t', - 'timestamp-type' : 'uint16_t', - 'energy-type' : 'uint32_t', - 'power-type' : 'uint16_t', - 'timestamp-granularity' : 1e-6, - 'power-granularity' : 1e-6, - 'energy-granularity' : 1e-12, + 'depth': 3, + 'sleep': 0, + 'timer-freq': 1e6, + 'timer-type': 'uint16_t', + 'timestamp-type': 'uint16_t', + 'energy-type': 'uint32_t', + 'power-type': 'uint16_t', + 'timestamp-granularity': 1e-6, + 'power-granularity': 1e-6, + 'energy-granularity': 1e-12, } for option, parameter in raw_opts: @@ -121,7 +117,7 @@ if __name__ == '__main__': pta.set_random_energy_model() - runs = list(pta.dfs(opt['depth'], with_arguments = True, with_parameters = True, trace_filter = opt['trace-filter'], sleep = opt['sleep'])) + runs = list(pta.dfs(opt['depth'], with_arguments=True, with_parameters=True, trace_filter=opt['trace-filter'], sleep=opt['sleep'])) num_transitions = len(runs) @@ -134,8 +130,8 @@ if __name__ == '__main__': model_energies = list() for run in runs: accounting_method = get_simulated_accountingmethod(opt['accounting'])(pta, opt['timer-freq'], opt['timer-type'], opt['timestamp-type'], - opt['power-type'], opt['energy-type']) - real_energy, real_duration, _, _ = pta.simulate(run, accounting = accounting_method) + opt['power-type'], opt['energy-type']) + real_energy, real_duration, _, _ = pta.simulate(run, accounting=accounting_method) model_energy = accounting_method.get_energy() real_energies.append(real_energy) real_durations.append(real_duration) @@ -144,7 +140,6 @@ if __name__ == '__main__': measures = regression_measures(np.array(model_energies), np.array(real_energies)) print('SMAPE {:.0f}%, MAE {}'.format(measures['smape'], measures['mae'])) - timer_freqs = [1e3, 2e3, 5e3, 1e4, 2e4, 5e4, 1e5, 2e5, 5e5, 1e6, 2e6, 5e6] timer_types = timestamp_types = power_types = energy_types = 'uint8_t uint16_t uint32_t uint64_t'.split() @@ -161,7 +156,7 @@ if __name__ == '__main__': base_weight += 8 return base_weight - #sys.exit(0) + # sys.exit(0) mean_errors = list() for timer_freq, timer_type, ts_type, power_type, energy_type in itertools.product(timer_freqs, timer_types, timestamp_types, power_types, energy_types): @@ -171,10 +166,10 @@ if __name__ == '__main__': # duration in µs # Bei kurzer Dauer (z.B. nur [1e2]) performt auc uint32_t für Energie gut, sonst nicht so (weil overflow) for sleep_duration in [1e2, 1e3, 1e4, 1e5, 1e6]: - runs = pta.dfs(opt['depth'], with_arguments = True, with_parameters = True, trace_filter = opt['trace-filter'], sleep = sleep_duration) + runs = pta.dfs(opt['depth'], with_arguments=True, with_parameters=True, trace_filter=opt['trace-filter'], sleep=sleep_duration) for run in runs: accounting_method = get_simulated_accountingmethod(opt['accounting'])(pta, timer_freq, timer_type, ts_type, power_type, energy_type) - real_energy, real_duration, _, _ = pta.simulate(run, accounting = accounting_method) + real_energy, real_duration, _, _ = pta.simulate(run, accounting=accounting_method) model_energy = accounting_method.get_energy() real_energies.append(real_energy) real_durations.append(real_duration) @@ -182,8 +177,8 @@ if __name__ == '__main__': measures = regression_measures(np.array(model_energies), np.array(real_energies)) mean_errors.append(((timer_freq, timer_type, ts_type, power_type, energy_type), config_weight(timer_freq, timer_type, ts_type, power_type, energy_type), measures)) - mean_errors.sort(key = lambda x: x[1]) - mean_errors.sort(key = lambda x: x[2]['mae']) + mean_errors.sort(key=lambda x: x[1]) + mean_errors.sort(key=lambda x: x[2]['mae']) for result in mean_errors: config, weight, measures = result diff --git a/bin/eval-rel-energy.py b/bin/eval-rel-energy.py index 123fe9f..4803c51 100755 --- a/bin/eval-rel-energy.py +++ b/bin/eval-rel-energy.py @@ -7,16 +7,18 @@ from dfatool import PTAModel, RawData, pta_trace_to_aggregate opts = {} + def get_file_groups(args): groups = [] index_low = 0 - while ':' in args[index_low : ]: - index_high = args[index_low : ].index(':') + index_low - groups.append(args[index_low : index_high]) + while ':' in args[index_low:]: + index_high = args[index_low:].index(':') + index_low + groups.append(args[index_low: index_high]) index_low = index_high + 1 - groups.append(args[index_low : ]) + groups.append(args[index_low:]) return groups + if __name__ == '__main__': ignored_trace_indexes = [] @@ -72,14 +74,14 @@ if __name__ == '__main__': print('{}:'.format(' '.join(file_group))) raw_data = RawData(file_group) - preprocessed_data = raw_data.get_preprocessed_data(verbose = False) + preprocessed_data = raw_data.get_preprocessed_data(verbose=False) by_name, parameters, arg_count = pta_trace_to_aggregate(preprocessed_data, ignored_trace_indexes) model = PTAModel(by_name, parameters, arg_count, - traces = preprocessed_data, - ignore_trace_indexes = ignored_trace_indexes, - discard_outliers = discard_outliers, - function_override = function_override, - verbose = False) + traces=preprocessed_data, + ignore_trace_indexes=ignored_trace_indexes, + discard_outliers=discard_outliers, + function_override=function_override, + verbose=False) lut_quality = model.assess(model.get_param_lut()) diff --git a/lib/dfatool.py b/lib/dfatool.py index 012f636..6b5b523 100644 --- a/lib/dfatool.py +++ b/lib/dfatool.py @@ -9,11 +9,9 @@ import re from scipy import optimize from sklearn.metrics import r2_score import struct -import sys import tarfile import hashlib from multiprocessing import Pool -from automata import PTA from functions import analytic from functions import AnalyticFunction from parameters import ParamStats @@ -30,6 +28,7 @@ except ImportError: arg_support_enabled = True + def running_mean(x: np.ndarray, N: int) -> np.ndarray: """ Compute `N` elements wide running average over `x`. @@ -59,13 +58,13 @@ def gplearn_to_function(function_str: str): inv -- 1 / x if |x| > 0.001, otherwise 0 """ eval_globals = { - 'add' : lambda x, y : x + y, - 'sub' : lambda x, y : x - y, - 'mul' : lambda x, y : x * y, - 'div' : lambda x, y : np.divide(x, y) if np.abs(y) > 0.001 else 1., - 'sqrt': lambda x : np.sqrt(np.abs(x)), - 'log' : lambda x : np.log(np.abs(x)) if np.abs(x) > 0.001 else 0., - 'inv' : lambda x : 1. / x if np.abs(x) > 0.001 else 0., + 'add': lambda x, y: x + y, + 'sub': lambda x, y: x - y, + 'mul': lambda x, y: x * y, + 'div': lambda x, y: np.divide(x, y) if np.abs(y) > 0.001 else 1., + 'sqrt': lambda x: np.sqrt(np.abs(x)), + 'log': lambda x: np.log(np.abs(x)) if np.abs(x) > 0.001 else 0., + 'inv': lambda x: 1. / x if np.abs(x) > 0.001 else 0., } last_arg_index = 0 @@ -74,18 +73,20 @@ def gplearn_to_function(function_str: str): last_arg_index = i arg_list = [] - for i in range(0, last_arg_index+1): + for i in range(0, last_arg_index + 1): arg_list.append('X{:d}'.format(i)) eval_str = 'lambda {}, *whatever: {}'.format(','.join(arg_list), function_str) print(eval_str) return eval(eval_str, eval_globals) + def append_if_set(aggregate: dict, data: dict, key: str): """Append data[key] to aggregate if key in data.""" if key in data: aggregate.append(data[key]) + def mean_or_none(arr): """ Compute mean of NumPy array `arr`, return -1 if empty. @@ -96,6 +97,7 @@ def mean_or_none(arr): return np.mean(arr) return -1 + def aggregate_measures(aggregate: float, actual: list) -> dict: """ Calculate error measures for model value on data list. @@ -110,6 +112,7 @@ def aggregate_measures(aggregate: float, actual: list) -> dict: aggregate_array = np.array([aggregate] * len(actual)) return regression_measures(aggregate_array, np.array(actual)) + def regression_measures(predicted: np.ndarray, actual: np.ndarray): """ Calculate error measures by comparing model values to reference values. @@ -135,33 +138,34 @@ def regression_measures(predicted: np.ndarray, actual: np.ndarray): if type(actual) != np.ndarray: raise ValueError('second arg must be ndarray, is {}'.format(type(actual))) deviations = predicted - actual - #mean = np.mean(actual) + # mean = np.mean(actual) if len(deviations) == 0: return {} measures = { - 'mae' : np.mean(np.abs(deviations), dtype=np.float64), - 'msd' : np.mean(deviations**2, dtype=np.float64), - 'rmsd' : np.sqrt(np.mean(deviations**2), dtype=np.float64), - 'ssr' : np.sum(deviations**2, dtype=np.float64), - 'rsq' : r2_score(actual, predicted), - 'count' : len(actual), + 'mae': np.mean(np.abs(deviations), dtype=np.float64), + 'msd': np.mean(deviations**2, dtype=np.float64), + 'rmsd': np.sqrt(np.mean(deviations**2), dtype=np.float64), + 'ssr': np.sum(deviations**2, dtype=np.float64), + 'rsq': r2_score(actual, predicted), + 'count': len(actual), } - #rsq_quotient = np.sum((actual - mean)**2, dtype=np.float64) * np.sum((predicted - mean)**2, dtype=np.float64) + # rsq_quotient = np.sum((actual - mean)**2, dtype=np.float64) * np.sum((predicted - mean)**2, dtype=np.float64) if np.all(actual != 0): - measures['mape'] = np.mean(np.abs(deviations / actual)) * 100 # bad measure + measures['mape'] = np.mean(np.abs(deviations / actual)) * 100 # bad measure else: measures['mape'] = np.nan if np.all(np.abs(predicted) + np.abs(actual) != 0): - measures['smape'] = np.mean(np.abs(deviations) / (( np.abs(predicted) + np.abs(actual)) / 2 )) * 100 + measures['smape'] = np.mean(np.abs(deviations) / ((np.abs(predicted) + np.abs(actual)) / 2)) * 100 else: measures['smape'] = np.nan - #if np.all(rsq_quotient != 0): + # if np.all(rsq_quotient != 0): # measures['rsq'] = (np.sum((actual - mean) * (predicted - mean), dtype=np.float64)**2) / rsq_quotient return measures + class KeysightCSV: """Simple loader for Keysight CSV data, as exported by the windows software.""" @@ -178,8 +182,8 @@ class KeysightCSV: with open(filename) as f: for i, _ in enumerate(f): pass - timestamps = np.ndarray((i-3), dtype=float) - currents = np.ndarray((i-3), dtype=float) + timestamps = np.ndarray((i - 3), dtype=float) + currents = np.ndarray((i - 3), dtype=float) # basically seek back to start with open(filename) as f: for _ in range(4): @@ -200,6 +204,7 @@ def _xv_partitions_kfold(length, num_slices): pairs.append((training, validation)) return pairs + def _xv_partition_montecarlo(length): shuffled = np.random.permutation(np.arange(length)) border = int(length * float(2) / 3) @@ -207,6 +212,7 @@ def _xv_partition_montecarlo(length): validation = shuffled[border:] return (training, validation) + class CrossValidator: """ Cross-Validation helper for model generation. @@ -240,7 +246,7 @@ class CrossValidator: self.parameters = sorted(parameters) self.arg_count = arg_count - def montecarlo(self, model_getter, count = 200): + def montecarlo(self, model_getter, count=200): """ Perform Monte Carlo cross-validation and return average model quality. @@ -272,7 +278,7 @@ class CrossValidator: } """ ret = { - 'by_name' : dict() + 'by_name': dict() } for name in self.names: @@ -302,10 +308,10 @@ class CrossValidator: validation = dict() for name in self.names: training[name] = { - 'attributes' : self.by_name[name]['attributes'] + 'attributes': self.by_name[name]['attributes'] } validation[name] = { - 'attributes' : self.by_name[name]['attributes'] + 'attributes': self.by_name[name]['attributes'] } if 'isa' in self.by_name[name]: @@ -328,9 +334,9 @@ class CrossValidator: for idx in validation_subset: validation[name]['param'].append(self.by_name[name]['param'][idx]) - training_data = self.model_class(training, self.parameters, self.arg_count, verbose = False) + training_data = self.model_class(training, self.parameters, self.arg_count, verbose=False) training_model = model_getter(training_data) - validation_data = self.model_class(validation, self.parameters, self.arg_count, verbose = False) + validation_data = self.model_class(validation, self.parameters, self.arg_count, verbose=False) return validation_data.assess(training_model) @@ -348,12 +354,12 @@ def _preprocess_mimosa(measurement): if len(trigidx) == 0: mim.errors.append('MIMOSA log has no triggers') return { - 'fileno' : measurement['fileno'], - 'info' : measurement['info'], - 'has_datasource_error' : len(mim.errors) > 0, - 'datasource_errors' : mim.errors, - 'expected_trace' : measurement['expected_trace'], - 'repeat_id' : measurement['repeat_id'], + 'fileno': measurement['fileno'], + 'info': measurement['info'], + 'has_datasource_error': len(mim.errors) > 0, + 'datasource_errors': mim.errors, + 'expected_trace': measurement['expected_trace'], + 'repeat_id': measurement['repeat_id'], } cal_edges = mim.calibration_edges(running_mean(mim.currents_nocal(charges[0:trigidx[0]]), 10)) @@ -361,14 +367,14 @@ def _preprocess_mimosa(measurement): vcalfunc = np.vectorize(calfunc, otypes=[np.float64]) processed_data = { - 'fileno' : measurement['fileno'], - 'info' : measurement['info'], - 'triggers' : len(trigidx), - 'first_trig' : trigidx[0] * 10, - 'calibration' : caldata, - 'energy_trace' : mim.analyze_states(charges, trigidx, vcalfunc), - 'has_datasource_error' : len(mim.errors) > 0, - 'datasource_errors' : mim.errors, + 'fileno': measurement['fileno'], + 'info': measurement['info'], + 'triggers': len(trigidx), + 'first_trig': trigidx[0] * 10, + 'calibration': caldata, + 'energy_trace': mim.analyze_states(charges, trigidx, vcalfunc), + 'has_datasource_error': len(mim.errors) > 0, + 'datasource_errors': mim.errors, } for key in ['expected_trace', 'repeat_id']: @@ -377,6 +383,7 @@ def _preprocess_mimosa(measurement): return processed_data + def _preprocess_etlog(measurement): setup = measurement['setup'] etlog = EnergyTraceLog(float(setup['voltage']), int(setup['state_duration']), measurement['transition_names']) @@ -385,20 +392,20 @@ def _preprocess_etlog(measurement): states_and_transitions = etlog.analyze_states(measurement['expected_trace'], measurement['repeat_id']) except EOFError as e: etlog.errors.append('EnergyTrace logfile error: {}'.format(e)) - trigidx = list() processed_data = { - 'fileno' : measurement['fileno'], - 'repeat_id' : measurement['repeat_id'], - 'info' : measurement['info'], - 'expected_trace' : measurement['expected_trace'], - 'energy_trace' : states_and_transitions, - 'has_datasource_error' : len(etlog.errors) > 0, - 'datasource_errors' : etlog.errors, + 'fileno': measurement['fileno'], + 'repeat_id': measurement['repeat_id'], + 'info': measurement['info'], + 'expected_trace': measurement['expected_trace'], + 'energy_trace': states_and_transitions, + 'has_datasource_error': len(etlog.errors) > 0, + 'datasource_errors': etlog.errors, } return processed_data + class TimingData: """ Loader for timing model traces measured with on-board timers using `harness.OnboardTimerHarness`. @@ -427,7 +434,7 @@ class TimingData: # TimingHarness logs states, but does not aggregate any data for them at the moment -> throw all states away transitions = list(filter(lambda x: x['isa'] == 'transition', trace['trace'])) self.traces.append({ - 'id' : trace['id'], + 'id': trace['id'], 'trace': transitions, }) for i, trace in enumerate(self.traces): @@ -435,7 +442,7 @@ class TimingData: trace['id'] = i for log_entry in trace['trace']: paramkeys = sorted(log_entry['parameter'].keys()) - if not 'param' in log_entry['offline_aggregates']: + if 'param' not in log_entry['offline_aggregates']: log_entry['offline_aggregates']['param'] = list() if 'duration' in log_entry['offline_aggregates']: for i in range(len(log_entry['offline_aggregates']['duration'])): @@ -456,7 +463,7 @@ class TimingData: self.traces_by_fileno.extend(log_data['traces']) self._concatenate_analyzed_traces() - def get_preprocessed_data(self, verbose = True): + def get_preprocessed_data(self, verbose=True): """ Return a list of DFA traces annotated with timing and parameter data. @@ -471,14 +478,15 @@ class TimingData: self.preprocessed = True return self.traces + def sanity_check_aggregate(aggregate): for key in aggregate: - if not 'param' in aggregate[key]: + if 'param' not in aggregate[key]: raise RuntimeError('aggregate[{}][param] does not exist'.format(key)) - if not 'attributes' in aggregate[key]: + if 'attributes' not in aggregate[key]: raise RuntimeError('aggregate[{}][attributes] does not exist'.format(key)) for attribute in aggregate[key]['attributes']: - if not attribute in aggregate[key]: + if attribute not in aggregate[key]: raise RuntimeError('aggregate[{}][{}] does not exist, even though it is contained in aggregate[{}][attributes]'.format(key, attribute, key)) param_len = len(aggregate[key]['param']) attr_len = len(aggregate[key][attribute]) @@ -586,8 +594,8 @@ class RawData: pass with open(self.cache_file, 'w') as f: cache_data = { - 'traces' : self.traces, - 'preprocessing_stats' : self.preprocessing_stats + 'traces': self.traces, + 'preprocessing_stats': self.preprocessing_stats } json.dump(cache_data, f) @@ -630,8 +638,6 @@ class RawData: - W_mean_delta_prev: Differenz zwischen W_mean und W_mean des vorherigen Zustands - W_mean_delta_next: Differenz zwischen W_mean und W_mean des Folgezustands """ - setup = self.setup_by_fileno[processed_data['fileno']] - traces = processed_data['expected_trace'] # Check for low-level parser errors if processed_data['has_datasource_error']: @@ -690,8 +696,8 @@ class RawData: sched_trigger_count += len(run['trace']) if sched_trigger_count != processed_data['triggers']: processed_data['error'] = 'got {got:d} trigger edges, expected {exp:d}'.format( - got = processed_data['triggers'], - exp = sched_trigger_count + got=processed_data['triggers'], + exp=sched_trigger_count ) return False # Check state durations. Very short or long states can indicate a @@ -706,56 +712,55 @@ class RawData: offline_trace_part = processed_data['energy_trace'][offline_idx] online_trace_part = traces[online_run_idx]['trace'][online_trace_part_idx] - if self._parameter_names == None: + if self._parameter_names is None: self._parameter_names = sorted(online_trace_part['parameter'].keys()) if sorted(online_trace_part['parameter'].keys()) != self._parameter_names: processed_data['error'] = 'Offline #{off_idx:d} (online {on_name:s} @ {on_idx:d}/{on_sub:d}) has inconsistent parameter set: should be {param_want:s}, is {param_is:s}'.format( - off_idx = offline_idx, on_idx = online_run_idx, - on_sub = online_trace_part_idx, - on_name = online_trace_part['name'], - param_want = self._parameter_names, - param_is = sorted(online_trace_part['parameter'].keys()) + off_idx=offline_idx, on_idx=online_run_idx, + on_sub=online_trace_part_idx, + on_name=online_trace_part['name'], + param_want=self._parameter_names, + param_is=sorted(online_trace_part['parameter'].keys()) ) if online_trace_part['isa'] != offline_trace_part['isa']: processed_data['error'] = 'Offline #{off_idx:d} (online {on_name:s} @ {on_idx:d}/{on_sub:d}) claims to be {off_isa:s}, but should be {on_isa:s}'.format( - off_idx = offline_idx, on_idx = online_run_idx, - on_sub = online_trace_part_idx, - on_name = online_trace_part['name'], - off_isa = offline_trace_part['isa'], - on_isa = online_trace_part['isa']) + off_idx=offline_idx, on_idx=online_run_idx, + on_sub=online_trace_part_idx, + on_name=online_trace_part['name'], + off_isa=offline_trace_part['isa'], + on_isa=online_trace_part['isa']) return False # Clipping in UNINITIALIZED (offline_idx == 0) can happen during # calibration and is handled by MIMOSA if offline_idx != 0 and offline_trace_part['clip_rate'] != 0 and not self.ignore_clipping: processed_data['error'] = 'Offline #{off_idx:d} (online {on_name:s} @ {on_idx:d}/{on_sub:d}) was clipping {clip:f}% of the time'.format( - off_idx = offline_idx, on_idx = online_run_idx, - on_sub = online_trace_part_idx, - on_name = online_trace_part['name'], - clip = offline_trace_part['clip_rate'] * 100, + off_idx=offline_idx, on_idx=online_run_idx, + on_sub=online_trace_part_idx, + on_name=online_trace_part['name'], + clip=offline_trace_part['clip_rate'] * 100, ) return False - - if online_trace_part['isa'] == 'state' and online_trace_part['name'] != 'UNINITIALIZED' and len(traces[online_run_idx]['trace']) > online_trace_part_idx+1: - online_prev_transition = traces[online_run_idx]['trace'][online_trace_part_idx-1] - online_next_transition = traces[online_run_idx]['trace'][online_trace_part_idx+1] + if online_trace_part['isa'] == 'state' and online_trace_part['name'] != 'UNINITIALIZED' and len(traces[online_run_idx]['trace']) > online_trace_part_idx + 1: + online_prev_transition = traces[online_run_idx]['trace'][online_trace_part_idx - 1] + online_next_transition = traces[online_run_idx]['trace'][online_trace_part_idx + 1] try: if self._state_is_too_short(online_trace_part, offline_trace_part, state_duration, online_next_transition): processed_data['error'] = 'Offline #{off_idx:d} (online {on_name:s} @ {on_idx:d}/{on_sub:d}) is too short (duration = {dur:d} us)'.format( - off_idx = offline_idx, on_idx = online_run_idx, - on_sub = online_trace_part_idx, - on_name = online_trace_part['name'], - dur = offline_trace_part['us']) + off_idx=offline_idx, on_idx=online_run_idx, + on_sub=online_trace_part_idx, + on_name=online_trace_part['name'], + dur=offline_trace_part['us']) return False if self._state_is_too_long(online_trace_part, offline_trace_part, state_duration, online_prev_transition): processed_data['error'] = 'Offline #{off_idx:d} (online {on_name:s} @ {on_idx:d}/{on_sub:d}) is too long (duration = {dur:d} us)'.format( - off_idx = offline_idx, on_idx = online_run_idx, - on_sub = online_trace_part_idx, - on_name = online_trace_part['name'], - dur = offline_trace_part['us']) + off_idx=offline_idx, on_idx=online_run_idx, + on_sub=online_trace_part_idx, + on_name=online_trace_part['name'], + dur=offline_trace_part['us']) return False except KeyError: pass @@ -781,7 +786,7 @@ class RawData: offline_trace_part = measurement['energy_trace'][offline_idx] online_trace_part = traces[online_run_idx]['trace'][online_trace_part_idx] - if not 'offline' in online_trace_part: + if 'offline' not in online_trace_part: online_trace_part['offline'] = [offline_trace_part] else: online_trace_part['offline'].append(offline_trace_part) @@ -802,14 +807,14 @@ class RawData: if arg_support_enabled and 'args' in online_trace_part: paramvalues.extend(map(soft_cast_int, online_trace_part['args'])) - if not 'offline_aggregates' in online_trace_part: + if 'offline_aggregates' not in online_trace_part: online_trace_part['offline_attributes'] = ['power', 'duration', 'energy'] online_trace_part['offline_aggregates'] = { - 'power' : [], - 'duration' : [], - 'power_std' : [], - 'energy' : [], - 'paramkeys' : [], + 'power': [], + 'duration': [], + 'power_std': [], + 'energy': [], + 'paramkeys': [], 'param': [], } if online_trace_part['isa'] == 'transition': @@ -853,7 +858,7 @@ class RawData: offline_trace_part = measurement['energy_trace'][offline_idx] online_trace_part = traces[online_run_idx]['trace'][online_trace_part_idx] - if not 'offline' in online_trace_part: + if 'offline' not in online_trace_part: online_trace_part['offline'] = [offline_trace_part] else: online_trace_part['offline'].append(offline_trace_part) @@ -876,18 +881,18 @@ class RawData: if 'offline_aggregates' not in online_trace_part: online_trace_part['offline_aggregates'] = { - 'offline_attributes' : ['power', 'duration', 'energy'], - 'duration' : list(), - 'power' : list(), - 'power_std' : list(), - 'energy' : list(), - 'paramkeys' : list(), - 'param' : list() + 'offline_attributes': ['power', 'duration', 'energy'], + 'duration': list(), + 'power': list(), + 'power_std': list(), + 'energy': list(), + 'paramkeys': list(), + 'param': list() } offline_aggregates = online_trace_part['offline_aggregates'] - #if online_trace_part['isa'] == 'transitions': + # if online_trace_part['isa'] == 'transitions': # online_trace_part['offline_attributes'].extend(['rel_energy_prev', 'rel_energy_next']) # offline_aggregates['rel_energy_prev'] = list() # offline_aggregates['rel_energy_next'] = list() @@ -899,11 +904,10 @@ class RawData: offline_aggregates['paramkeys'].append(paramkeys) offline_aggregates['param'].append(paramvalues) - #if online_trace_part['isa'] == 'transition': + # if online_trace_part['isa'] == 'transition': # offline_aggregates['rel_energy_prev'].append(offline_trace_part['W_mean_delta_prev'] * offline_trace_part['s'] * 1e12) # offline_aggregates['rel_energy_next'].append(offline_trace_part['W_mean_delta_next'] * offline_trace_part['s'] * 1e12) - def _concatenate_traces(self, list_of_traces): trace_output = list() for trace in list_of_traces: @@ -913,7 +917,7 @@ class RawData: trace['id'] = i return trace_output - def get_preprocessed_data(self, verbose = True): + def get_preprocessed_data(self, verbose=True): """ Return a list of DFA traces annotated with energy, timing, and parameter data. @@ -953,7 +957,7 @@ class RawData: If isa == transition, it contains power, duration, energy, rel_energy_prev, rel_energy_next, timeout * `online`: List of online estimations for this state/transition. Each entry contains a result for this state/transition during one benchmark execution. Entry contents for isa == state: - - `time`: state/transition + - `time`: state/transition Entry contents for isa == transition: - `timeout`: Duration of previous state, measured using on-board timers * `parameter`: dictionary describing parameter values for this state/transition. Parameter values refer to the begin of the state/transition and do not account for changes made by the transition. @@ -992,10 +996,10 @@ class RawData: _, extension = os.path.splitext(member.name) if extension == '.mim': offline_data.append({ - 'content' : tf.extractfile(member).read(), - 'fileno' : i, - 'info' : member, - 'setup' : self.setup_by_fileno[i], + 'content': tf.extractfile(member).read(), + 'fileno': i, + 'info': member, + 'setup': self.setup_by_fileno[i], }) elif version == 1: @@ -1031,19 +1035,19 @@ class RawData: new_filenames.append('{}#{}'.format(filename, j)) self.traces_by_fileno.append(traces) self.setup_by_fileno.append({ - 'mimosa_voltage' : ptalog['configs'][j]['voltage'], - 'mimosa_shunt' : ptalog['configs'][j]['shunt'], - 'state_duration' : ptalog['opt']['sleep'], + 'mimosa_voltage': ptalog['configs'][j]['voltage'], + 'mimosa_shunt': ptalog['configs'][j]['shunt'], + 'state_duration': ptalog['opt']['sleep'], }) for repeat_id, mim_file in enumerate(ptalog['files'][j]): member = tf.getmember(mim_file) offline_data.append({ - 'content' : tf.extractfile(member).read(), - 'fileno' : j, - 'info' : member, - 'setup' : self.setup_by_fileno[j], - 'repeat_id' : repeat_id, - 'expected_trace' : ptalog['traces'][j], + 'content': tf.extractfile(member).read(), + 'fileno': j, + 'info': member, + 'setup': self.setup_by_fileno[j], + 'repeat_id': repeat_id, + 'expected_trace': ptalog['traces'][j], }) self.filenames = new_filenames @@ -1097,19 +1101,19 @@ class RawData: new_filenames.append('{}#{}'.format(filename, j)) self.traces_by_fileno.append(traces) self.setup_by_fileno.append({ - 'voltage' : ptalog['configs'][j]['voltage'], - 'state_duration' : ptalog['opt']['sleep'], + 'voltage': ptalog['configs'][j]['voltage'], + 'state_duration': ptalog['opt']['sleep'], }) for repeat_id, etlog_file in enumerate(ptalog['files'][j]): member = tf.getmember(etlog_file) offline_data.append({ - 'content' : tf.extractfile(member).read(), - 'fileno' : j, - 'info' : member, - 'setup' : self.setup_by_fileno[j], - 'repeat_id' : repeat_id, - 'expected_trace' : ptalog['traces'][j], - 'transition_names' : list(map(lambda x: x['name'], ptalog['pta']['transitions'])) + 'content': tf.extractfile(member).read(), + 'fileno': j, + 'info': member, + 'setup': self.setup_by_fileno[j], + 'repeat_id': repeat_id, + 'expected_trace': ptalog['traces'][j], + 'transition_names': list(map(lambda x: x['name'], ptalog['pta']['transitions'])) }) self.filenames = new_filenames # TODO remove 'offline_aggregates' from pre-parse data and place @@ -1125,27 +1129,22 @@ class RawData: measurements = pool.map(_preprocess_etlog, offline_data) num_valid = 0 - valid_traces = list() for measurement in measurements: - if not 'energy_trace' in measurement: + if 'energy_trace' not in measurement: vprint(self.verbose, '[W] Skipping {ar:s}/{m:s}: {e:s}'.format( - ar = self.filenames[measurement['fileno']], - m = measurement['info'].name, - e = '; '.join(measurement['datasource_errors']))) + ar=self.filenames[measurement['fileno']], + m=measurement['info'].name, + e='; '.join(measurement['datasource_errors']))) continue if version == 0: # Strip the last state (it is not part of the scheduled measurement) measurement['energy_trace'].pop() - repeat = 0 elif version == 1: # The first online measurement is the UNINITIALIZED state. In v1, # it is not part of the expected PTA trace -> remove it. measurement['energy_trace'].pop(0) - repeat = ptalog['opt']['repeat'] - elif version == 2: - repeat = ptalog['opt']['repeat'] if version == 0 or version == 1: if self._measurement_is_valid_01(measurement): @@ -1153,21 +1152,21 @@ class RawData: num_valid += 1 else: vprint(self.verbose, '[W] Skipping {ar:s}/{m:s}: {e:s}'.format( - ar = self.filenames[measurement['fileno']], - m = measurement['info'].name, - e = measurement['error'])) + ar=self.filenames[measurement['fileno']], + m=measurement['info'].name, + e=measurement['error'])) elif version == 2: if self._measurement_is_valid_2(measurement): self._merge_online_and_etlog(measurement) num_valid += 1 else: vprint(self.verbose, '[W] Skipping {ar:s}/{m:s}: {e:s}'.format( - ar = self.filenames[measurement['fileno']], - m = measurement['info'].name, - e = measurement['error'])) + ar=self.filenames[measurement['fileno']], + m=measurement['info'].name, + e=measurement['error'])) vprint(self.verbose, '[I] {num_valid:d}/{num_total:d} measurements are valid'.format( - num_valid = num_valid, - num_total = len(measurements))) + num_valid=num_valid, + num_total=len(measurements))) if version == 0: self.traces = self._concatenate_traces(self.traces_by_fileno) elif version == 1: @@ -1176,10 +1175,11 @@ class RawData: elif version == 2: self.traces = self._concatenate_traces(self.traces_by_fileno) self.preprocessing_stats = { - 'num_runs' : len(measurements), - 'num_valid' : num_valid + 'num_runs': len(measurements), + 'num_valid': num_valid } + class ParallelParamFit: """ Fit a set of functions on parameterized measurements. @@ -1193,15 +1193,15 @@ class ParallelParamFit: self.fit_queue = [] self.by_param = by_param - def enqueue(self, state_or_tran, attribute, param_index, param_name, safe_functions_enabled = False, param_filter = None): + def enqueue(self, state_or_tran, attribute, param_index, param_name, safe_functions_enabled=False, param_filter=None): """ Add state_or_tran/attribute/param_name to fit queue. This causes fit() to compute the best-fitting function for this model part. """ self.fit_queue.append({ - 'key' : [state_or_tran, attribute, param_name, param_filter], - 'args' : [self.by_param, state_or_tran, attribute, param_index, safe_functions_enabled, param_filter] + 'key': [state_or_tran, attribute, param_name, param_filter], + 'args': [self.by_param, state_or_tran, attribute, param_index, safe_functions_enabled, param_filter] }) def fit(self): @@ -1215,6 +1215,7 @@ class ParallelParamFit: with Pool() as pool: self.results = pool.map(_try_fits_parallel, self.fit_queue) + def _try_fits_parallel(arg): """ Call _try_fits(*arg['args']) and return arg['key'] and the _try_fits result. @@ -1222,11 +1223,12 @@ def _try_fits_parallel(arg): Must be a global function as it is called from a multiprocessing Pool. """ return { - 'key' : arg['key'], - 'result' : _try_fits(*arg['args']) + 'key': arg['key'], + 'result': _try_fits(*arg['args']) } -def _try_fits(by_param, state_or_tran, model_attribute, param_index, safe_functions_enabled = False, param_filter: dict = None): + +def _try_fits(by_param, state_or_tran, model_attribute, param_index, safe_functions_enabled=False, param_filter: dict = None): """ Determine goodness-of-fit for prediction of `by_param[(state_or_tran, *)][model_attribute]` dependence on `param_index` using various functions. @@ -1250,13 +1252,13 @@ def _try_fits(by_param, state_or_tran, model_attribute, param_index, safe_functi :param model_attribute: attribute for which goodness-of-fit will be calculated. Example: `'bar'` - + :param param_index: index of the parameter used as model input :param safe_functions_enabled: Include "safe" variants of functions with limited argument range. :param param_filter: Only use measurements whose parameters match param_filter for fitting. """ - functions = analytic.functions(safe_functions_enabled = safe_functions_enabled) + functions = analytic.functions(safe_functions_enabled=safe_functions_enabled) for param_key in filter(lambda x: x[0] == state_or_tran, by_param.keys()): # We might remove elements from 'functions' while iterating over @@ -1271,8 +1273,8 @@ def _try_fits(by_param, state_or_tran, model_attribute, param_index, safe_functi raw_results = dict() raw_results_by_param = dict() ref_results = { - 'mean' : list(), - 'median' : list() + 'mean': list(), + 'median': list() } results = dict() results_by_param = dict() @@ -1305,17 +1307,17 @@ def _try_fits(by_param, state_or_tran, model_attribute, param_index, safe_functi raw_results_by_param[other_parameters] = dict() results_by_param[other_parameters] = dict() for function_name, param_function in functions.items(): - if not function_name in raw_results: + if function_name not in raw_results: raw_results[function_name] = dict() error_function = param_function.error_function res = optimize.least_squares(error_function, [0, 1], args=(X, Y), xtol=2e-15) measures = regression_measures(param_function.eval(res.x, X), Y) raw_results_by_param[other_parameters][function_name] = measures for measure, error_rate in measures.items(): - if not measure in raw_results[function_name]: + if measure not in raw_results[function_name]: raw_results[function_name][measure] = list() raw_results[function_name][measure].append(error_rate) - #print(function_name, res, measures) + # print(function_name, res, measures) mean_measures = aggregate_measures(np.mean(Y), Y) ref_results['mean'].append(mean_measures['rmsd']) raw_results_by_param[other_parameters]['mean'] = mean_measures @@ -1325,13 +1327,13 @@ def _try_fits(by_param, state_or_tran, model_attribute, param_index, safe_functi if not len(ref_results['mean']): # Insufficient data for fitting - #print('[W] Insufficient data for fitting {}/{}/{}'.format(state_or_tran, model_attribute, param_index)) + # print('[W] Insufficient data for fitting {}/{}/{}'.format(state_or_tran, model_attribute, param_index)) return { - 'best' : None, - 'best_rmsd' : np.inf, - 'results' : results + 'best': None, + 'best_rmsd': np.inf, + 'results': results } - + for other_parameter_combination, other_parameter_results in raw_results_by_param.items(): best_fit_val = np.inf best_fit_name = None @@ -1346,9 +1348,9 @@ def _try_fits(by_param, state_or_tran, model_attribute, param_index, safe_functi results_by_param[other_parameter_combination] = { 'best': best_fit_name, 'best_rmsd': best_fit_val, - 'mean_rmsd' : results['mean']['rmsd'], - 'median_rmsd' : results['median']['rmsd'], - 'results' : results + 'mean_rmsd': results['mean']['rmsd'], + 'median_rmsd': results['median']['rmsd'], + 'results': results } best_fit_val = np.inf @@ -1365,14 +1367,15 @@ def _try_fits(by_param, state_or_tran, model_attribute, param_index, safe_functi best_fit_name = function_name return { - 'best' : best_fit_name, - 'best_rmsd' : best_fit_val, - 'mean_rmsd' : np.mean(ref_results['mean']), - 'median_rmsd' : np.mean(ref_results['median']), - 'results' : results, - 'results_by_other_param' : results_by_param + 'best': best_fit_name, + 'best_rmsd': best_fit_val, + 'mean_rmsd': np.mean(ref_results['mean']), + 'median_rmsd': np.mean(ref_results['median']), + 'results': results, + 'results_by_other_param': results_by_param } + def _num_args_from_by_name(by_name): num_args = dict() for key, value in by_name.items(): @@ -1380,7 +1383,8 @@ def _num_args_from_by_name(by_name): num_args[key] = len(value['args'][0]) return num_args -def get_fit_result(results, name, attribute, verbose = False, param_filter: dict = None): + +def get_fit_result(results, name, attribute, verbose=False, param_filter: dict = None): """ Parse and sanitize fit results for state/transition/... 'name' and model attribute 'attribute'. @@ -1390,12 +1394,12 @@ def get_fit_result(results, name, attribute, verbose = False, param_filter: dict :param name: state/transition/... name, e.g. 'TX' :param attribute: model attribute, e.g. 'duration' :param verbose: print debug message to stdout when deliberately not using a determined fit function - :param param_filter: + :param param_filter: :returns: dict with fit result (see `_try_fits`) for each successfully fitted parameter. E.g. {'param 1': {'best' : 'function name', ...} } """ fit_result = dict() for result in results: - if result['key'][0] == name and result['key'][1] == attribute and result['key'][3] == param_filter and result['result']['best'] != None: # dürfte an ['best'] != None liegen-> Fit für gefilterten Kram schlägt fehl? + if result['key'][0] == name and result['key'][1] == attribute and result['key'][3] == param_filter and result['result']['best'] is not None: # dürfte an ['best'] != None liegen-> Fit für gefilterten Kram schlägt fehl? this_result = result['result'] if this_result['best_rmsd'] >= min(this_result['mean_rmsd'], this_result['median_rmsd']): vprint(verbose, '[I] Not modeling {} {} as function of {}: best ({:.0f}) is worse than ref ({:.0f}, {:.0f})'.format( @@ -1410,6 +1414,7 @@ def get_fit_result(results, name, attribute, verbose = False, param_filter: dict fit_result[result['key'][2]] = this_result return fit_result + class AnalyticModel: u""" Parameter-aware analytic energy/data size/... model. @@ -1452,10 +1457,10 @@ class AnalyticModel: assess -- calculate model quality """ - def __init__(self, by_name, parameters, arg_count = None, function_override = dict(), verbose = True, use_corrcoef = False): + def __init__(self, by_name, parameters, arg_count=None, function_override=dict(), verbose=True, use_corrcoef=False): """ Create a new AnalyticModel and compute parameter statistics. - + :param by_name: measurements aggregated by (function/state/...) name. Layout: dictionary with one key per name ('send', 'TX', ...) or one key per name and parameter combination @@ -1502,7 +1507,7 @@ class AnalyticModel: if self._num_args is None: self._num_args = _num_args_from_by_name(by_name) - self.stats = ParamStats(self.by_name, self.by_param, self.parameters, self._num_args, verbose = verbose, use_corrcoef = use_corrcoef) + self.stats = ParamStats(self.by_name, self.by_param, self.parameters, self._num_args, verbose=verbose, use_corrcoef=use_corrcoef) def _get_model_from_dict(self, model_dict, model_function): model = {} @@ -1545,7 +1550,7 @@ class AnalyticModel: return static_model_getter - def get_param_lut(self, fallback = False): + def get_param_lut(self, fallback=False): """ Get parameter-look-up-table model function: name, attribute, parameter values -> model value. @@ -1558,7 +1563,7 @@ class AnalyticModel: static_model = self._get_model_from_dict(self.by_name, np.median) lut_model = self._get_model_from_dict(self.by_param, np.median) - def lut_median_getter(name, key, param, arg = [], **kwargs): + def lut_median_getter(name, key, param, arg=[], **kwargs): param.extend(map(soft_cast_int, arg)) try: return lut_model[(name, tuple(param))][key] @@ -1569,7 +1574,7 @@ class AnalyticModel: return lut_median_getter - def get_fitted(self, safe_functions_enabled = False): + def get_fitted(self, safe_functions_enabled=False): """ Get paramete-aware model function and model information function. @@ -1610,7 +1615,7 @@ class AnalyticModel: if x.fit_success: param_model[name][attribute] = { 'fit_result': fit_result, - 'function' : x + 'function': x } elif len(fit_result.keys()): x = analytic.function_powerset(fit_result, self.parameters, num_args) @@ -1619,7 +1624,7 @@ class AnalyticModel: if x.fit_success: param_model[name][attribute] = { 'fit_result': fit_result, - 'function' : x + 'function': x } def model_getter(name, key, **kwargs): @@ -1664,7 +1669,7 @@ class AnalyticModel: detailed_results[name][attribute] = measures return { - 'by_name' : detailed_results, + 'by_name': detailed_results, } def to_json(self): @@ -1675,9 +1680,9 @@ class AnalyticModel: def _add_trace_data_to_aggregate(aggregate, key, element): # Only cares about element['isa'], element['offline_aggregates'], and # element['plan']['level'] - if not key in aggregate: + if key not in aggregate: aggregate[key] = { - 'isa' : element['isa'] + 'isa': element['isa'] } for datakey in element['offline_aggregates'].keys(): aggregate[key][datakey] = [] @@ -1687,7 +1692,7 @@ def _add_trace_data_to_aggregate(aggregate, key, element): # TODO do not hardcode values aggregate[key]['attributes'] = ['duration', 'energy', 'rel_energy_prev', 'rel_energy_next'] # Uncomment this line if you also want to analyze mean transition power - #aggrgate[key]['attributes'].append('power') + # aggrgate[key]['attributes'].append('power') if 'plan' in element and element['plan']['level'] == 'epilogue': aggregate[key]['attributes'].insert(0, 'timeout') attributes = aggregate[key]['attributes'].copy() @@ -1698,7 +1703,7 @@ def _add_trace_data_to_aggregate(aggregate, key, element): aggregate[key][datakey].extend(dataval) -def pta_trace_to_aggregate(traces, ignore_trace_indexes = []): +def pta_trace_to_aggregate(traces, ignore_trace_indexes=[]): u""" Convert preprocessed DFA traces from peripherals/drivers to by_name aggregate for PTAModel. @@ -1761,8 +1766,8 @@ def pta_trace_to_aggregate(traces, ignore_trace_indexes = []): if elem['name'] != 'UNINITIALIZED': _add_trace_data_to_aggregate(by_name, elem['name'], elem) for elem in by_name.values(): - for key in elem['attributes']: - elem[key] = np.array(elem[key]) + for key in elem['attributes']: + elem[key] = np.array(elem[key]) return by_name, parameter_names, arg_count @@ -1798,7 +1803,7 @@ class PTAModel: - rel_energy_next: transition energy relative to next state mean power in pJ """ - def __init__(self, by_name, parameters, arg_count, traces = [], ignore_trace_indexes = [], discard_outliers = None, function_override = {}, verbose = True, use_corrcoef = False, pta = None): + def __init__(self, by_name, parameters, arg_count, traces=[], ignore_trace_indexes=[], discard_outliers=None, function_override={}, verbose=True, use_corrcoef=False, pta=None): """ Prepare a new PTA energy model. @@ -1835,7 +1840,7 @@ class PTAModel: self._num_args = arg_count self._use_corrcoef = use_corrcoef self.traces = traces - self.stats = ParamStats(self.by_name, self.by_param, self._parameter_names, self._num_args, self._use_corrcoef, verbose = verbose) + self.stats = ParamStats(self.by_name, self.by_param, self._parameter_names, self._num_args, self._use_corrcoef, verbose=verbose) self.cache = {} np.seterr('raise') self._outlier_threshold = discard_outliers @@ -1892,7 +1897,7 @@ class PTAModel: return static_model_getter - def get_param_lut(self, fallback = False): + def get_param_lut(self, fallback=False): """ Get parameter-look-up-table model function: name, attribute, parameter values -> model value. @@ -1905,7 +1910,7 @@ class PTAModel: static_model = self._get_model_from_dict(self.by_name, np.median) lut_model = self._get_model_from_dict(self.by_param, np.median) - def lut_median_getter(name, key, param, arg = [], **kwargs): + def lut_median_getter(name, key, param, arg=[], **kwargs): param.extend(map(soft_cast_int, arg)) try: return lut_model[(name, tuple(param))][key] @@ -1926,7 +1931,7 @@ class PTAModel: return self._parameter_names[param_index] return str(param_index) - def get_fitted(self, safe_functions_enabled = False): + def get_fitted(self, safe_functions_enabled=False): """ Get parameter-aware model function and model information function. @@ -1974,7 +1979,7 @@ class PTAModel: if x.fit_success: param_model[state_or_tran][model_attribute] = { 'fit_result': fit_results, - 'function' : x + 'function': x } elif len(fit_results.keys()): x = analytic.function_powerset(fit_results, self._parameter_names, num_args) @@ -1982,7 +1987,7 @@ class PTAModel: if x.fit_success: param_model[state_or_tran][model_attribute] = { 'fit_result': fit_results, - 'function' : x + 'function': x } def model_getter(name, key, **kwargs): @@ -2053,10 +2058,10 @@ class PTAModel: detailed_results[name][key] = measures return { - 'by_name' : detailed_results + 'by_name': detailed_results } - def assess_states(self, model_function, model_attribute = 'power', distribution: dict = None): + def assess_states(self, model_function, model_attribute='power', distribution: dict = None): """ Calculate overall model error assuming equal distribution of states """ @@ -2065,21 +2070,20 @@ class PTAModel: model_quality = self.assess(model_function) num_states = len(self.states()) if distribution is None: - distribution = dict(map(lambda x: [x, 1/num_states], self.states())) + distribution = dict(map(lambda x: [x, 1 / num_states], self.states())) if not np.isclose(sum(distribution.values()), 1): raise ValueError('distribution must be a probability distribution with sum 1') - total_value = None - try: - total_value = sum(map(lambda x: model_function(x, model_attribute) * distribution[x], self.states())) - except KeyError: - pass + # total_value = None + # try: + # total_value = sum(map(lambda x: model_function(x, model_attribute) * distribution[x], self.states())) + # except KeyError: + # pass total_error = np.sqrt(sum(map(lambda x: np.square(model_quality['by_name'][x][model_attribute]['mae'] * distribution[x]), self.states()))) return total_error - def assess_on_traces(self, model_function): """ Calculate MAE, SMAPE, etc. of model_function for each trace known to this PTAModel instance. @@ -2110,15 +2114,15 @@ class PTAModel: real_timeout = 0. for i, trace_part in enumerate(trace['trace']): name = trace_part['name'] - prev_name = trace['trace'][i-1]['name'] + prev_name = trace['trace'][i - 1]['name'] isa = trace_part['isa'] if name != 'UNINITIALIZED': try: param = trace_part['offline_aggregates']['param'][rep_id] - prev_param = trace['trace'][i-1]['offline_aggregates']['param'][rep_id] + prev_param = trace['trace'][i - 1]['offline_aggregates']['param'][rep_id] power = trace_part['offline'][rep_id]['uW_mean'] duration = trace_part['offline'][rep_id]['us'] - prev_duration = trace['trace'][i-1]['offline'][rep_id]['us'] + prev_duration = trace['trace'][i - 1]['offline'][rep_id]['us'] real_energy += power * duration if isa == 'state': model_energy += model_function(name, 'power', param=param) * duration @@ -2149,13 +2153,14 @@ class PTAModel: model_timeout_list.append(model_timeout) return { - 'duration_by_trace' : regression_measures(np.array(model_duration_list), np.array(real_duration_list)), - 'energy_by_trace' : regression_measures(np.array(model_energy_list), np.array(real_energy_list)), - 'timeout_by_trace' : regression_measures(np.array(model_timeout_list), np.array(real_timeout_list)), - 'rel_energy_by_trace' : regression_measures(np.array(model_rel_energy_list), np.array(real_energy_list)), - 'state_energy_by_trace' : regression_measures(np.array(model_state_energy_list), np.array(real_energy_list)), + 'duration_by_trace': regression_measures(np.array(model_duration_list), np.array(real_duration_list)), + 'energy_by_trace': regression_measures(np.array(model_energy_list), np.array(real_energy_list)), + 'timeout_by_trace': regression_measures(np.array(model_timeout_list), np.array(real_timeout_list)), + 'rel_energy_by_trace': regression_measures(np.array(model_rel_energy_list), np.array(real_energy_list)), + 'state_energy_by_trace': regression_measures(np.array(model_state_energy_list), np.array(real_energy_list)), } + class EnergyTraceLog: """ EnergyTrace log loader for DFA traces. @@ -2221,19 +2226,17 @@ class EnergyTraceLog: if len(fields) == 4: timestamp, current, voltage, total_energy = map(int, fields) elif len(fields) == 5: - cpustate = fields[0] + # cpustate = fields[0] timestamp, current, voltage, total_energy = map(int, fields[1:]) else: raise RuntimeError('cannot parse line "{}"'.format(line)) data[i] = [timestamp, current, voltage, total_energy] - self.interval_start_timestamp = data[:-1, 0] * 1e-6 self.interval_duration = (data[1:, 0] - data[:-1, 0]) * 1e-6 self.interval_power = ((data[1:, 3] - data[:-1, 3]) * 1e-9) / ((data[1:, 0] - data[:-1, 0]) * 1e-6) m_duration_us = data[-1, 0] - data[0, 0] - m_energy_nj = data[-1, 3] - data[0, 3] self.sample_rate = data_count / (m_duration_us * 1e-6) @@ -2335,10 +2338,10 @@ class EnergyTraceLog: energy_trace.append({ 'isa': 'transition', - 'W_mean' : np.mean(self.interval_power[transition_start_index : transition_done_index]), - 'W_std' : np.std(self.interval_power[transition_start_index : transition_done_index]), - 's' : duration, - 's_coarse' : self.interval_start_timestamp[transition_done_index] - self.interval_start_timestamp[transition_start_index] + 'W_mean': np.mean(self.interval_power[transition_start_index: transition_done_index]), + 'W_std': np.std(self.interval_power[transition_start_index: transition_done_index]), + 's': duration, + 's_coarse': self.interval_start_timestamp[transition_done_index] - self.interval_start_timestamp[transition_start_index] }) @@ -2347,10 +2350,10 @@ class EnergyTraceLog: energy_trace.append({ 'isa': 'state', - 'W_mean' : np.mean(self.interval_power[state_start_index : state_done_index]), - 'W_std' : np.std(self.interval_power[state_start_index : state_done_index]), - 's' : self.state_duration, - 's_coarse' : self.interval_start_timestamp[state_done_index] - self.interval_start_timestamp[state_start_index] + 'W_mean': np.mean(self.interval_power[state_start_index: state_done_index]), + 'W_std': np.std(self.interval_power[state_start_index: state_done_index]), + 's': self.state_duration, + 's_coarse': self.interval_start_timestamp[state_done_index] - self.interval_start_timestamp[state_start_index] }) energy_trace[-2]['W_mean_delta_next'] = energy_trace[-2]['W_mean'] - energy_trace[-1]['W_mean'] @@ -2365,7 +2368,7 @@ class EnergyTraceLog: def find_first_sync(self): # LED Power is approx. self.led_power W, use self.led_power/2 W above surrounding median as threshold - sync_threshold_power = np.median(self.interval_power[: int(3 * self.sample_rate)]) + self.led_power/3 + sync_threshold_power = np.median(self.interval_power[: int(3 * self.sample_rate)]) + self.led_power / 3 for i, ts in enumerate(self.interval_start_timestamp): if ts > 2 and self.interval_power[i] > sync_threshold_power: return self.interval_start_timestamp[i - 300] @@ -2388,9 +2391,8 @@ class EnergyTraceLog: # Lookaround: 100 ms in both directions lookaround = int(0.1 * self.sample_rate) - # LED Power is approx. self.led_power W, use self.led_power/2 W above surrounding median as threshold - sync_threshold_power = np.median(self.interval_power[start_position - lookaround : start_position + lookaround]) + self.led_power/3 + sync_threshold_power = np.median(self.interval_power[start_position - lookaround: start_position + lookaround]) + self.led_power / 3 vprint(self.verbose, 'looking for barcode starting at {:0.2f} s, threshold is {:0.1f} mW'.format(start_ts, sync_threshold_power * 1e3)) @@ -2407,7 +2409,7 @@ class EnergyTraceLog: sync_end_ts = ts break - barcode_data = self.interval_power[sync_area_start : sync_area_end] + barcode_data = self.interval_power[sync_area_start: sync_area_end] vprint(self.verbose, 'barcode search area: {:0.2f} .. {:0.2f} seconds ({} samples)'.format(sync_start_ts, sync_end_ts, len(barcode_data))) @@ -2446,8 +2448,8 @@ class EnergyTraceLog: image_data = bytes(map(int, image_data)) * height - #img = Image.frombytes('L', (width, height), image_data).resize((width, 100)) - #img.save('/tmp/test-{}.png'.format(os.getpid())) + # img = Image.frombytes('L', (width, height), image_data).resize((width, 100)) + # img.save('/tmp/test-{}.png'.format(os.getpid())) zbimg = zbar.Image(width, height, 'Y800', image_data) scanner = zbar.ImageScanner() @@ -2483,7 +2485,6 @@ class EnergyTraceLog: return None, None, None, None - class MIMOSA: """ MIMOSA log loader for DFA traces with auto-calibration. @@ -2491,16 +2492,16 @@ class MIMOSA: Expects a MIMOSA log file generated via dfatool and a dfatool-generated benchmark. A MIMOSA log consists of a series of measurements. Each measurement gives the total charge (in pJ) and binary buzzer/trigger value during a 10µs interval. - + There must be a calibration run consisting of at least two seconds with disconnected DUT, two seconds with 1 kOhm (984 Ohm), and two seconds with 100 kOhm (99013 Ohm) resistor at the start. The first ten seconds of data are reserved for calbiration and must not contain measurements, as trigger/buzzer signals are ignored in this time range. - + Resulting data is a list of state/transition/state/transition/... measurements. """ - def __init__(self, voltage: float, shunt: int, verbose = True): + def __init__(self, voltage: float, shunt: int, verbose=True): """ Initialize MIMOSA loader for a specific voltage and shunt setting. @@ -2511,8 +2512,8 @@ class MIMOSA: self.voltage = voltage self.shunt = shunt self.verbose = verbose - self.r1 = 984 # "1k" - self.r2 = 99013 # "100k" + self.r1 = 984 # "1k" + self.r2 = 99013 # "100k" self.errors = list() def charge_to_current_nocal(self, charge): @@ -2548,7 +2549,6 @@ class MIMOSA: i += 1 return charges, triggers - def load_data(self, raw_data): u""" Load MIMOSA log data from a MIMOSA log file passed as raw byte string @@ -2558,7 +2558,7 @@ class MIMOSA: :returns: (numpy array of charges (pJ per 10µs), numpy array of triggers (0/1 int, per 10µs)) """ with io.BytesIO(raw_data) as data_object: - with tarfile.open(fileobj = data_object) as tf: + with tarfile.open(fileobj=data_object) as tf: return self._load_tf(tf) def load_file(self, filename): @@ -2575,7 +2575,7 @@ class MIMOSA: def currents_nocal(self, charges): u""" Convert charges (pJ per 10µs) to mean currents without accounting for calibration. - + :param charges: numpy array of charges (pJ per 10µs) :returns: numpy array of currents (mean µA per 10µs)""" @@ -2621,7 +2621,7 @@ class MIMOSA: if trig != prevtrig: # Due to MIMOSA's integrate-read-reset cycle, the charge/current # interval belonging to this trigger comes two intervals (20µs) later - trigidx.append(i+2) + trigidx.append(i + 2) prevtrig = trig return trigidx @@ -2717,24 +2717,24 @@ class MIMOSA: return charge * b_upper + a_upper + ua_r2 caldata = { - 'edges' : [x * 10 for x in cal_edges], + 'edges': [x * 10 for x in cal_edges], 'offset': cal_0_mean, - 'offset2' : cal_r2_mean, - 'slope_low' : b_lower, - 'slope_high' : b_upper, - 'add_low' : a_lower, - 'add_high' : a_upper, - 'r0_err_uW' : np.mean(self.currents_nocal(chg_r0)) * self.voltage, - 'r0_std_uW' : np.std(self.currents_nocal(chg_r0)) * self.voltage, - 'r1_err_uW' : (np.mean(self.currents_nocal(chg_r1)) - ua_r1) * self.voltage, - 'r1_std_uW' : np.std(self.currents_nocal(chg_r1)) * self.voltage, - 'r2_err_uW' : (np.mean(self.currents_nocal(chg_r2)) - ua_r2) * self.voltage, - 'r2_std_uW' : np.std(self.currents_nocal(chg_r2)) * self.voltage, + 'offset2': cal_r2_mean, + 'slope_low': b_lower, + 'slope_high': b_upper, + 'add_low': a_lower, + 'add_high': a_upper, + 'r0_err_uW': np.mean(self.currents_nocal(chg_r0)) * self.voltage, + 'r0_std_uW': np.std(self.currents_nocal(chg_r0)) * self.voltage, + 'r1_err_uW': (np.mean(self.currents_nocal(chg_r1)) - ua_r1) * self.voltage, + 'r1_std_uW': np.std(self.currents_nocal(chg_r1)) * self.voltage, + 'r2_err_uW': (np.mean(self.currents_nocal(chg_r2)) - ua_r2) * self.voltage, + 'r2_std_uW': np.std(self.currents_nocal(chg_r2)) * self.voltage, } - #print("if charge < %f : return 0" % cal_0_mean) - #print("if charge <= %f : return charge * %f + %f" % (cal_r2_mean, b_lower, a_lower)) - #print("else : return charge * %f + %f + %f" % (b_upper, a_upper, ua_r2)) + # print("if charge < %f : return 0" % cal_0_mean) + # print("if charge <= %f : return charge * %f + %f" % (cal_r2_mean, b_lower, a_lower)) + # print("else : return charge * %f + %f + %f" % (b_upper, a_upper, ua_r2)) return calfunc, caldata @@ -2819,20 +2819,20 @@ class MIMOSA: substates = {} if previdx != 0 and idx - previdx > 200: - thr, subst = 0, [] #self.gradfoo(range_ua) + thr, subst = 0, [] # self.gradfoo(range_ua) if len(subst): statelist = [] prevsubidx = 0 for subidx in subst: statelist.append({ 'duration': (subidx - prevsubidx) * 10, - 'uW_mean' : np.mean(range_ua[prevsubidx : subidx] * self.voltage), - 'uW_std' : np.std(range_ua[prevsubidx : subidx] * self.voltage), + 'uW_mean': np.mean(range_ua[prevsubidx: subidx] * self.voltage), + 'uW_std': np.std(range_ua[prevsubidx: subidx] * self.voltage), }) prevsubidx = subidx substates = { - 'threshold' : thr, - 'states' : statelist, + 'threshold': thr, + 'states': statelist, } isa = 'state' @@ -2841,17 +2841,17 @@ class MIMOSA: data = { 'isa': isa, - 'clip_rate' : np.mean(range_raw == 65535), + 'clip_rate': np.mean(range_raw == 65535), 'raw_mean': np.mean(range_raw), - 'raw_std' : np.std(range_raw), - 'uW_mean' : np.mean(range_ua * self.voltage), - 'uW_std' : np.std(range_ua * self.voltage), - 'us' : (idx - previdx) * 10, + 'raw_std': np.std(range_raw), + 'uW_mean': np.mean(range_ua * self.voltage), + 'uW_std': np.std(range_ua * self.voltage), + 'us': (idx - previdx) * 10, } if 'states' in substates: data['substates'] = substates - ssum = np.sum(list(map(lambda x : x['duration'], substates['states']))) + ssum = np.sum(list(map(lambda x: x['duration'], substates['states']))) if ssum != data['us']: vprint(self.verbose, "ERR: duration %d vs %d" % (data['us'], ssum)) diff --git a/lib/plotter.py b/lib/plotter.py index 30b5b82..b9d5c3e 100755 --- a/lib/plotter.py +++ b/lib/plotter.py @@ -4,69 +4,79 @@ import itertools import numpy as np import matplotlib.pyplot as plt import re -from matplotlib.patches import Polygon -from utils import flatten + def is_state(aggregate, name): """Return true if name is a state and not UNINITIALIZED.""" return aggregate[name]['isa'] == 'state' and name != 'UNINITIALIZED' + def plot_states(model, aggregate): keys = [key for key in sorted(aggregate.keys()) if is_state(aggregate, key)] data = [aggregate[key]['means'] for key in keys] mdata = [int(model['state'][key]['power']['static']) for key in keys] - boxplot(keys, data, 'Zustand', 'µW', modeldata = mdata) + boxplot(keys, data, 'Zustand', 'µW', modeldata=mdata) + def plot_transitions(model, aggregate): keys = [key for key in sorted(aggregate.keys()) if aggregate[key]['isa'] == 'transition'] data = [aggregate[key]['rel_energies'] for key in keys] mdata = [int(model['transition'][key]['rel_energy']['static']) for key in keys] - boxplot(keys, data, 'Transition', 'pJ (rel)', modeldata = mdata) + boxplot(keys, data, 'Transition', 'pJ (rel)', modeldata=mdata) data = [aggregate[key]['energies'] for key in keys] mdata = [int(model['transition'][key]['energy']['static']) for key in keys] - boxplot(keys, data, 'Transition', 'pJ', modeldata = mdata) + boxplot(keys, data, 'Transition', 'pJ', modeldata=mdata) + def plot_states_duration(model, aggregate): keys = [key for key in sorted(aggregate.keys()) if is_state(aggregate, key)] data = [aggregate[key]['durations'] for key in keys] boxplot(keys, data, 'Zustand', 'µs') + def plot_transitions_duration(model, aggregate): keys = [key for key in sorted(aggregate.keys()) if aggregate[key]['isa'] == 'transition'] data = [aggregate[key]['durations'] for key in keys] boxplot(keys, data, 'Transition', 'µs') + def plot_transitions_timeout(model, aggregate): keys = [key for key in sorted(aggregate.keys()) if aggregate[key]['isa'] == 'transition'] data = [aggregate[key]['timeouts'] for key in keys] boxplot(keys, data, 'Timeout', 'µs') + def plot_states_clips(model, aggregate): keys = [key for key in sorted(aggregate.keys()) if is_state(aggregate, key)] data = [np.array([100]) * aggregate[key]['clip_rate'] for key in keys] boxplot(keys, data, 'Zustand', '% Clipping') + def plot_transitions_clips(model, aggregate): keys = [key for key in sorted(aggregate.keys()) if aggregate[key]['isa'] == 'transition'] data = [np.array([100]) * aggregate[key]['clip_rate'] for key in keys] boxplot(keys, data, 'Transition', '% Clipping') + def plot_substate_thresholds(model, aggregate): keys = [key for key in sorted(aggregate.keys()) if is_state(aggregate, key)] data = [aggregate[key]['sub_thresholds'] for key in keys] boxplot(keys, data, 'Zustand', 'substate threshold (mW/dmW)') + def plot_histogram(data): n, bins, patches = plt.hist(data, 1000, normed=1, facecolor='green', alpha=0.75) plt.show() + def plot_states_param(model, aggregate): keys = [key for key in sorted(aggregate.keys()) if aggregate[key]['isa'] == 'state' and key[0] != 'UNINITIALIZED'] data = [aggregate[key]['means'] for key in keys] mdata = [int(model['state'][key[0]]['power']['static']) for key in keys] - boxplot(keys, data, 'Transition', 'µW', modeldata = mdata) + boxplot(keys, data, 'Transition', 'µW', modeldata=mdata) -def plot_attribute(aggregate, attribute, attribute_unit = '', key_filter = lambda x: True, **kwargs): + +def plot_attribute(aggregate, attribute, attribute_unit='', key_filter=lambda x: True, **kwargs): """ Boxplot measurements of a single attribute according to the partitioning provided by aggregate. @@ -82,23 +92,26 @@ def plot_attribute(aggregate, attribute, attribute_unit = '', key_filter = lambd data = [aggregate[key][attribute] for key in keys] boxplot(keys, data, attribute, attribute_unit, **kwargs) + def plot_substate_thresholds_p(model, aggregate): keys = [key for key in sorted(aggregate.keys()) if aggregate[key]['isa'] == 'state' and key[0] != 'UNINITIALIZED'] data = [aggregate[key]['sub_thresholds'] for key in keys] boxplot(keys, data, 'Zustand', '% Clipping') + def plot_y(Y, **kwargs): plot_xy(np.arange(len(Y)), Y, **kwargs) -def plot_xy(X, Y, xlabel = None, ylabel = None, title = None, output = None): - fig, ax1 = plt.subplots(figsize=(10,6)) - if title != None: + +def plot_xy(X, Y, xlabel=None, ylabel=None, title=None, output=None): + fig, ax1 = plt.subplots(figsize=(10, 6)) + if title is not None: fig.canvas.set_window_title(title) - if xlabel != None: + if xlabel is not None: ax1.set_xlabel(xlabel) - if ylabel != None: + if ylabel is not None: ax1.set_ylabel(ylabel) - plt.subplots_adjust(left = 0.1, bottom = 0.1, right = 0.99, top = 0.99) + plt.subplots_adjust(left=0.1, bottom=0.1, right=0.99, top=0.99) plt.plot(X, Y, "bo", markersize=2) if output: plt.savefig(output) @@ -109,18 +122,20 @@ def plot_xy(X, Y, xlabel = None, ylabel = None, title = None, output = None): else: plt.show() + def _param_slice_eq(a, b, index): - return (*a[1][:index], *a[1][index+1:]) == (*b[1][:index], *b[1][index+1:]) and a[0] == b[0] + return (*a[1][:index], *a[1][index + 1:]) == (*b[1][:index], *b[1][index + 1:]) and a[0] == b[0] + -def plot_param(model, state_or_trans, attribute, param_idx, xlabel = None, ylabel = None, title = None, extra_function = None, output = None): - fig, ax1 = plt.subplots(figsize=(10,6)) - if title != None: +def plot_param(model, state_or_trans, attribute, param_idx, xlabel=None, ylabel=None, title=None, extra_function=None, output=None): + fig, ax1 = plt.subplots(figsize=(10, 6)) + if title is not None: fig.canvas.set_window_title(title) - if xlabel != None: + if xlabel is not None: ax1.set_xlabel(xlabel) - if ylabel != None: + if ylabel is not None: ax1.set_ylabel(ylabel) - plt.subplots_adjust(left = 0.1, bottom = 0.1, right = 0.99, top = 0.99) + plt.subplots_adjust(left=0.1, bottom=0.1, right=0.99, top=0.99) param_name = model.param_name(param_idx) @@ -137,8 +152,8 @@ def plot_param(model, state_or_trans, attribute, param_idx, xlabel = None, ylabe for k, v in model.by_param.items(): if k[0] == state_or_trans: - other_param_key = (*k[1][:param_idx], *k[1][param_idx+1:]) - if not other_param_key in by_other_param: + other_param_key = (*k[1][:param_idx], *k[1][param_idx + 1:]) + if other_param_key not in by_other_param: by_other_param[other_param_key] = {'X': [], 'Y': []} by_other_param[other_param_key]['X'].extend([float(k[1][param_idx])] * len(v[attribute])) by_other_param[other_param_key]['Y'].extend(v[attribute]) @@ -153,7 +168,7 @@ def plot_param(model, state_or_trans, attribute, param_idx, xlabel = None, ylabe YY2_legend = [] cm = plt.get_cmap('brg', len(by_other_param)) - for i, k in sorted(enumerate(by_other_param), key = lambda x: x[1]): + for i, k in sorted(enumerate(by_other_param), key=lambda x: x[1]): v = by_other_param[k] v['X'] = np.array(v['X']) v['Y'] = np.array(v['Y']) @@ -169,17 +184,17 @@ def plot_param(model, state_or_trans, attribute, param_idx, xlabel = None, ylabe for i in range(len(v['X'])): print('{} {}'.format(v['X'][i], v['Y'][i]), file=f) - #x_range = int((v['X'].max() - v['X'].min()) * 10) - #xsp = np.linspace(v['X'].min(), v['X'].max(), x_range) + # x_range = int((v['X'].max() - v['X'].min()) * 10) + # xsp = np.linspace(v['X'].min(), v['X'].max(), x_range) if param_model: ysp = [] for x in xsp: xarg = [*k[:param_idx], x, *k[param_idx:]] - ysp.append(param_model(state_or_trans, attribute, param = xarg)) + ysp.append(param_model(state_or_trans, attribute, param=xarg)) plt.plot(xsp, ysp, "r-", color=cm(i), linewidth=0.5) YY.append(ysp) YY_legend.append(legend_sanitizer.sub('_', 'regr_{}'.format(k))) - if extra_function != None: + if extra_function is not None: ysp = [] with np.errstate(divide='ignore', invalid='ignore'): for x in xsp: @@ -202,7 +217,7 @@ def plot_param(model, state_or_trans, attribute, param_idx, xlabel = None, ylabe def plot_param_fit(function, name, fitfunc, funp, parameters, datatype, index, X, Y, xaxis=None, yaxis=None): - fig, ax1 = plt.subplots(figsize=(10,6)) + fig, ax1 = plt.subplots(figsize=(10, 6)) fig.canvas.set_window_title("fit %s" % (function)) plt.subplots_adjust(left=0.14, right=0.99, top=0.99, bottom=0.14) @@ -214,16 +229,16 @@ def plot_param_fit(function, name, fitfunc, funp, parameters, datatype, index, X xsp = np.linspace(X[index].min(), X[index].max(), 100) x_range = 100 - if xaxis != None: + if xaxis is not None: ax1.set_xlabel(xaxis) else: ax1.set_xlabel(parameters[index]) - if yaxis != None: + if yaxis is not None: ax1.set_ylabel(yaxis) else: ax1.set_ylabel('%s %s' % (name, datatype)) - otherparams = list(set(itertools.product(*X[:index], *X[index+1:]))) + otherparams = list(set(itertools.product(*X[:index], *X[index + 1:]))) cm = plt.get_cmap('brg', len(otherparams)) for i in range(len(otherparams)): elem = otherparams[i] @@ -234,7 +249,7 @@ def plot_param_fit(function, name, fitfunc, funp, parameters, datatype, index, X if k < index: tt &= X[k] == elem[k] elif k > index: - tt &= X[k] == elem[k-1] + tt &= X[k] == elem[k - 1] plt.plot(X[index][tt], Y[tt], "rx", color=color) @@ -245,8 +260,8 @@ def plot_param_fit(function, name, fitfunc, funp, parameters, datatype, index, X plt.show() -def boxplot(ticks, measurements, xlabel = '', ylabel = '', modeldata = None, output = None): - fig, ax1 = plt.subplots(figsize=(10,6)) +def boxplot(ticks, measurements, xlabel='', ylabel='', modeldata=None, output=None): + fig, ax1 = plt.subplots(figsize=(10, 6)) fig.canvas.set_window_title('DriverEval') plt.subplots_adjust(left=0.1, right=0.95, top=0.95, bottom=0.1) @@ -256,10 +271,10 @@ def boxplot(ticks, measurements, xlabel = '', ylabel = '', modeldata = None, out plt.setp(bp['fliers'], color='red', marker='+') ax1.yaxis.grid(True, linestyle='-', which='major', color='lightgrey', - alpha=0.5) + alpha=0.5) ax1.set_axisbelow(True) - #ax1.set_title('DriverEval') + # ax1.set_title('DriverEval') ax1.set_xlabel(xlabel) ax1.set_ylabel(ylabel) @@ -268,7 +283,7 @@ def boxplot(ticks, measurements, xlabel = '', ylabel = '', modeldata = None, out xtickNames = plt.setp(ax1, xticklabels=ticks) plt.setp(xtickNames, rotation=0, fontsize=10) - boxColors = ['darkkhaki', 'royalblue'] + # boxColors = ['darkkhaki', 'royalblue'] medians = list(range(numBoxes)) for i in range(numBoxes): box = bp['boxes'][i] @@ -277,11 +292,11 @@ def boxplot(ticks, measurements, xlabel = '', ylabel = '', modeldata = None, out for j in range(5): boxX.append(box.get_xdata()[j]) boxY.append(box.get_ydata()[j]) - boxCoords = list(zip(boxX, boxY)) + # boxCoords = list(zip(boxX, boxY)) # Alternate between Dark Khaki and Royal Blue - k = i % 2 - boxPolygon = Polygon(boxCoords, facecolor=boxColors[k]) - #ax1.add_patch(boxPolygon) + # k = i % 2 + # boxPolygon = Polygon(boxCoords, facecolor=boxColors[k]) + # ax1.add_patch(boxPolygon) # Now draw the median lines back over what we just filled in med = bp['medians'][i] medianX = [] @@ -294,22 +309,22 @@ def boxplot(ticks, measurements, xlabel = '', ylabel = '', modeldata = None, out # Finally, overplot the sample averages, with horizontal alignment # in the center of each box plt.plot([np.average(med.get_xdata())], [np.average(measurements[i])], - color='w', marker='*', markeredgecolor='k') + color='w', marker='*', markeredgecolor='k') if modeldata: plt.plot([np.average(med.get_xdata())], [modeldata[i]], - color='w', marker='o', markeredgecolor='k') + color='w', marker='o', markeredgecolor='k') pos = np.arange(numBoxes) + 1 upperLabels = [str(np.round(s, 2)) for s in medians] - weights = ['bold', 'semibold'] + # weights = ['bold', 'semibold'] for tick, label in zip(range(numBoxes), ax1.get_xticklabels()): - k = tick % 2 + # k = tick % 2 y0, y1 = ax1.get_ylim() - textpos = y0 + (y1 - y0)*0.97 - ypos = ax1.get_ylim()[0] + textpos = y0 + (y1 - y0) * 0.97 + # ypos = ax1.get_ylim()[0] ax1.text(pos[tick], textpos, upperLabels[tick], - horizontalalignment='center', size='small', - color='royalblue') + horizontalalignment='center', size='small', + color='royalblue') if output: plt.savefig(output) diff --git a/lib/runner.py b/lib/runner.py index 1525b56..32eb950 100644 --- a/lib/runner.py +++ b/lib/runner.py @@ -18,6 +18,7 @@ import subprocess import sys import time + class SerialReader(serial.threaded.Protocol): """ Character- to line-wise data buffer for serial interfaces. @@ -25,7 +26,8 @@ class SerialReader(serial.threaded.Protocol): Reads in new data whenever it becomes available and exposes a line-based interface to applications. """ - def __init__(self, callback = None): + + def __init__(self, callback=None): """Create a new SerialReader object.""" self.callback = callback self.recv_buf = '' @@ -53,7 +55,7 @@ class SerialReader(serial.threaded.Protocol): except UnicodeDecodeError: pass - #sys.stderr.write('UART output contains garbage: {data}\n'.format(data = data)) + # sys.stderr.write('UART output contains garbage: {data}\n'.format(data = data)) def get_lines(self) -> list: """ @@ -79,10 +81,11 @@ class SerialReader(serial.threaded.Protocol): return ret return None + class SerialMonitor: """SerialMonitor captures serial output for a specific amount of time.""" - def __init__(self, port: str, baud: int, callback = None): + def __init__(self, port: str, baud: int, callback=None): """ Create a new SerialMonitor connected to port at the specified baud rate. @@ -101,7 +104,7 @@ class SerialMonitor: sys.stderr.write('Could not open serial port {}: {}\n'.format(self.ser.name, e)) sys.exit(1) - self.reader = SerialReader(callback = callback) + self.reader = SerialReader(callback=callback) self.worker = serial.threaded.ReaderThread(self.ser, self.reader) self.worker.start() @@ -128,10 +131,17 @@ class SerialMonitor: self.worker.stop() self.ser.close() +# TODO Optionale Kalibrierung mit bekannten Widerständen an GPIOs am Anfang +# TODO Sync per LED? -> Vor und ggf nach jeder Transition kurz pulsen +# TODO Für Verbraucher mit wenig Energiebedarf: Versorgung direkt per GPIO +# -> Zu Beginn der Messung ganz ausknipsen + + class EnergyTraceMonitor(SerialMonitor): """EnergyTraceMonitor captures serial timing output and EnergyTrace energy data.""" - def __init__(self, port: str, baud: int, callback = None, voltage = 3.3): - super().__init__(port = port, baud = baud, callback = callback) + + def __init__(self, port: str, baud: int, callback=None, voltage=3.3): + super().__init__(port=port, baud=baud, callback=callback) self._voltage = voltage self._output = time.strftime('%Y%m%d-%H%M%S.etlog') self._start_energytrace() @@ -139,26 +149,28 @@ class EnergyTraceMonitor(SerialMonitor): def _start_energytrace(self): cmd = ['msp430-etv', '--save', self._output, '0'] self._logger = subprocess.Popen(cmd, - stdout = subprocess.PIPE, stderr = subprocess.PIPE, - universal_newlines = True) + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + universal_newlines=True) def close(self): super().close() self._logger.send_signal(subprocess.signal.SIGINT) - stdout, stderr = self._logger.communicate(timeout = 15) + stdout, stderr = self._logger.communicate(timeout=15) def get_files(self) -> list: return [self._output] def get_config(self) -> dict: return { - 'voltage' : self._voltage, + 'voltage': self._voltage, } + class MIMOSAMonitor(SerialMonitor): """MIMOSAMonitor captures serial output and MIMOSA energy data for a specific amount of time.""" - def __init__(self, port: str, baud: int, callback = None, offset = 130, shunt = 330, voltage = 3.3): - super().__init__(port = port, baud = baud, callback = callback) + + def __init__(self, port: str, baud: int, callback=None, offset=130, shunt=330, voltage=3.3): + super().__init__(port=port, baud=baud, callback=callback) self._offset = offset self._shunt = shunt self._voltage = voltage @@ -188,9 +200,9 @@ class MIMOSAMonitor(SerialMonitor): self._mimosacmd(['--parameter', 'voltage', str(self._voltage)]) self._mimosacmd(['--mimosa-start']) time.sleep(2) - self._mimosactl('1k') # 987 ohm + self._mimosactl('1k') # 987 ohm time.sleep(2) - self._mimosactl('100k') # 99.3 kohm + self._mimosactl('100k') # 99.3 kohm time.sleep(2) self._mimosactl('connect') @@ -205,7 +217,7 @@ class MIMOSAMonitor(SerialMonitor): # belong to the current measurements. This ensures that older .mim # files lying around in the directory will not confuse our # heuristic. - for filename in sorted(os.listdir(), reverse = True): + for filename in sorted(os.listdir(), reverse=True): if re.search(r'[.]mim$', filename): mim_file = filename break @@ -226,14 +238,16 @@ class MIMOSAMonitor(SerialMonitor): def get_config(self) -> dict: return { - 'offset' : self._offset, - 'shunt' : self._shunt, - 'voltage' : self._voltage, + 'offset': self._offset, + 'shunt': self._shunt, + 'voltage': self._voltage, } + class ShellMonitor: """SerialMonitor runs a program and captures its output for a specific amount of time.""" - def __init__(self, script: str, callback = None): + + def __init__(self, script: str, callback=None): """ Create a new ShellMonitor object. @@ -251,8 +265,8 @@ class ShellMonitor: if type(timeout) != int: raise ValueError('timeout argument must be int') res = subprocess.run(['timeout', '{:d}s'.format(timeout), self.script], - stdout = subprocess.PIPE, stderr = subprocess.PIPE, - universal_newlines = True) + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + universal_newlines=True) if self.callback: for line in res.stdout.split('\n'): self.callback(line) @@ -269,30 +283,33 @@ class ShellMonitor: """ pass -def build(arch, app, opts = []): + +def build(arch, app, opts=[]): command = ['make', 'arch={}'.format(arch), 'app={}'.format(app), 'clean'] command.extend(opts) - res = subprocess.run(command, stdout = subprocess.PIPE, stderr = subprocess.PIPE, - universal_newlines = True) + res = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + universal_newlines=True) if res.returncode != 0: raise RuntimeError('Build failure: ' + res.stderr) command = ['make', '-B', 'arch={}'.format(arch), 'app={}'.format(app)] command.extend(opts) - res = subprocess.run(command, stdout = subprocess.PIPE, stderr = subprocess.PIPE, - universal_newlines = True) + res = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + universal_newlines=True) if res.returncode != 0: raise RuntimeError('Build failure: ' + res.stderr) return command -def flash(arch, app, opts = []): + +def flash(arch, app, opts=[]): command = ['make', 'arch={}'.format(arch), 'app={}'.format(app), 'program'] command.extend(opts) - res = subprocess.run(command, stdout = subprocess.PIPE, stderr = subprocess.PIPE, - universal_newlines = True) + res = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + universal_newlines=True) if res.returncode != 0: raise RuntimeError('Flash failure') return command + def get_info(arch, opts: list = []) -> list: """ Return multipass "make info" output. @@ -301,12 +318,13 @@ def get_info(arch, opts: list = []) -> list: """ command = ['make', 'arch={}'.format(arch), 'info'] command.extend(opts) - res = subprocess.run(command, stdout = subprocess.PIPE, stderr = subprocess.PIPE, - universal_newlines = True) + res = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + universal_newlines=True) if res.returncode != 0: raise RuntimeError('make info Failure') return res.stdout.split('\n') + def get_monitor(arch: str, **kwargs) -> object: """ Return an appropriate monitor for arch, depending on "make info" output. @@ -334,6 +352,7 @@ def get_monitor(arch: str, **kwargs) -> object: return SerialMonitor(port, arg, **kwargs) raise RuntimeError('Monitor failure') + def get_counter_limits(arch: str) -> tuple: """Return multipass max counter and max overflow value for arch.""" for line in get_info(arch): @@ -344,6 +363,7 @@ def get_counter_limits(arch: str) -> tuple: return overflow_value, max_overflow raise RuntimeError('Did not find Counter Overflow limits') + def get_counter_limits_us(arch: str) -> tuple: """Return duration of one counter step and one counter overflow in us.""" cpu_freq = 0 |