diff options
-rw-r--r-- | lib/functions.py | 177 |
1 files changed, 175 insertions, 2 deletions
diff --git a/lib/functions.py b/lib/functions.py index fd9063f..cc97f9a 100644 --- a/lib/functions.py +++ b/lib/functions.py @@ -1,3 +1,9 @@ +""" +Utilities for analytic description of parameter-dependent model attributes. + +This module provides classes and helper functions useful for least-squares +regression and general handling of model functions. +""" from itertools import chain, combinations import numpy as np import re @@ -7,28 +13,110 @@ from utils import is_numeric arg_support_enabled = True def powerset(iterable): + """ + Calculate powerset of given items. + + Returns an iterable containing one tuple for each powerset element. + + Example: powerset([1, 2]) -> [(), (1), (2), (1, 2)] + """ s = list(iterable) return chain.from_iterable(combinations(s, r) for r in range(len(s)+1)) class ParamFunction: + """ + A one-dimensional model function, ready for least squares optimization and similar. + + Supports validity checks (e.g. if it is undefined for x <= 0) and an + error measure. + """ def __init__(self, param_function, validation_function, num_vars): + """ + Create function object suitable for regression analysis. + + This documentation assumes that 1-dimensional functions + (-> single float as model input) are used. However, n-dimensional + functions (-> list of float as model input) are also supported. + + arguments: + param_function -- regression function. Must have the signature + (reg_param, model_param) -> float. + reg_param is a list of regression variable values, + model_param is the model input value (float). + Example: lambda rp, mp: rp[0] + rp[1] * mp + validation_function -- function used to check whether param_function + is defined for a given model_param. Signature: + model_param -> bool + Example: lambda mp: mp > 0 + num_vars -- How many regression variables are used by this function, + i.e., the length of param_function's reg_param argument. + """ self._param_function = param_function self._validation_function = validation_function self._num_variables = num_vars def is_valid(self, arg): + """ + Check whether the regression function is defined for the given argument. + + Returns bool. + """ return self._validation_function(arg) def eval(self, param, args): + """ + Evaluate regression function. + + arguments: + param -- regression variable values (list of float) + arg -- model input (float) + """ return self._param_function(param, args) def error_function(self, P, X, y): + """ + Calculate model error. + + arguments: + P -- optimized regression variables (list of float) + X -- model input (float) + y -- expected output from ground truth (float) + + Returns deviation between model and ground truth (float). + """ return self._param_function(P, X) - y class AnalyticFunction: + """ + A multi-dimensional model function, generated from a string, which can be optimized using regression. + + The function describes a single model attribute (e.g. TX duration or send(...) energy) + and how it is influenced by model parameters such as configured bit rate or + packet length. + """ def __init__(self, function_str, parameters, num_args, verbose = True, regression_args = None): + """ + Create a new AnalyticFunction object from a function string. + + arguments: + function_str -- the function. + Refer to regression variables using regression_arg(123), + to parameters using parameter(name), + and to function arguments (if any) using function_arg(123). + Example: "regression_arg(0) + regression_arg(1) * parameter(txbytes)" + parameters -- list containing the names of all model parameters, + including those not used in function_str, sorted lexically. + Sorting is mandatory, as parameter indexes (and not names) are used internally. + num_args -- number of local function arguments, if any. Set to 0 if + the model attribute does not belong to a function or if function + arguments are not included in the model. + verbose -- complain about odd events + regression_args -- Initial regression variable values, + both for function usage and least squares optimization. + If unset, defaults to [1, 1, 1, ...] + """ self._parameter_names = parameters self._num_args = num_args self._model_str = function_str @@ -65,6 +153,23 @@ class AnalyticFunction: self._regression_args = [] def get_fit_data(self, by_param, state_or_tran, model_attribute): + """ + Return training data suitable for scipy.optimize.least_squares. + + arguments: + by_param -- measurement data, partitioned by state/transition name and parameter/arg values + state_or_tran -- state or transition name, e.g. "TX" or "send" + model_attribute -- model attribute name, e.g. "power" or "duration" + + returns (X, Y, num_valid, num_total): + X -- 2-D NumPy array of parameter combinations (model input). + First dimension is the parameter/argument index, the second + dimension contains its values. + Example: X[0] contains the first parameter's values. + Y -- 1-D NumPy array of training data (desired model output). + num_valid -- amount of distinct parameter values suitable for optimization + num_total -- total amount of distinct parameter values + """ dimension = len(self._parameter_names) + self._num_args X = [[] for i in range(dimension)] Y = [] @@ -95,6 +200,14 @@ class AnalyticFunction: return X, Y, num_valid, num_total def fit(self, by_param, state_or_tran, model_attribute): + """ + Fit the function on measurements via least squares regression. + + arguments: + by_param -- measurement data, partitioned by state/transition name and parameter/arg values + state_or_tran -- state or transition name, e.g. "TX" or "send" + model_attribute -- model attribute name, e.g. "power" or "duration" + """ X, Y, num_valid, num_total = self.get_fit_data(by_param, state_or_tran, model_attribute) if num_valid > 2: error_function = lambda P, X, y: self._function(P, X) - y @@ -112,17 +225,42 @@ class AnalyticFunction: vprint(self.verbose, '[W] Insufficient amount of valid parameter keys, cannot fit {}/{}'.format(state_or_tran, model_attribute)) def is_predictable(self, param_list): + """ + Return whether the model function can be evaluated on the given parameter values. + + The first value corresponds to the lexically first model parameter, etc. + All parameters must be set, not just the ones this function depends on. + + Returns False iff a parameter the function depends on is not numeric + (e.g. None). + """ for i, param in enumerate(param_list): if self._dependson[i] and not is_numeric(param): return False return True def eval(self, param_list, arg_list = []): + """ + Evaluate model function with specified param/arg values. + + arguments: + param_list -- parameter values (list of float). First item + corresponds to lexically first parameter, etc. + arg_list -- argument values (list of float), if arguments are used. + """ if len(self._regression_args) == 0: return self._function(param_list, arg_list) return self._function(self._regression_args, param_list) class analytic: + """ + Utilities for analytic description of parameter-dependent model attributes and regression analysis. + + provided functions: + functions -- retrieve pre-defined set of regression function candidates + function_powerset -- combine several per-parameter functions into a single AnalyticFunction + """ + _num0_8 = np.vectorize(lambda x: 8 - bin(int(x)).count("1")) _num0_16 = np.vectorize(lambda x: 16 - bin(int(x)).count("1")) _num1 = np.vectorize(lambda x: bin(int(x)).count("1")) @@ -147,6 +285,21 @@ class analytic: } def functions(safe_functions_enabled = False): + """ + Retrieve pre-defined set of regression function candidates. + + Returns a dict of functions which are typical for energy/timing + behaviour of embedded hardware, e.g. linear, exponential or inverse + dependency on a configuration setting/runtime variable. + + arguments: + safe_functions_enabled -- Include "safe" variants of functions with + limited argument range, e.g. a safe + inverse which returns 1 when dividing by 0. + + Each function is a ParamFunction object. In most cases, two regression + variables are expected. + """ functions = { 'linear' : ParamFunction( lambda reg_param, model_param: reg_param[0] + reg_param[1] * model_param, @@ -221,6 +374,7 @@ class analytic: return functions def _fmap(reference_type, reference_name, function_type): + """Map arg/parameter name and best-fit function name to function text suitable for AnalyticFunction.""" ref_str = '{}({})'.format(reference_type,reference_name) if function_type == 'linear': return ref_str @@ -240,10 +394,29 @@ class analytic: return 'np.sqrt({})'.format(ref_str) return 'analytic._{}({})'.format(function_type, ref_str) - def function_powerset(function_descriptions, parameter_names, num_args): + def function_powerset(fit_results, parameter_names, num_args): + """ + Combine per-parameter regression results into a single multi-dimensional function. + + arguments: + fit_results -- results dict. One element per parameter, each containing + a dict of the form {'best' : name of function with best fit}. + Must not include parameters which do not influence the model attribute. + Example: {'txpower' : {'best': 'exponential'}} + parameter_names -- Parameter names, including those left + out in fit_results because they do not influence the model attribute. + Must be sorted lexically. + Example: ['bitrate', 'txpower'] + num_args -- number of local function arguments, if any. Set to 0 if + the model attribute does not belong to a function or if function + arguments are not included in the model. + + Returns an AnalyticFunction instantce corresponding to the combined + function. + """ buf = '0' arg_idx = 0 - for combination in powerset(function_descriptions.items()): + for combination in powerset(fit_results.items()): buf += ' + regression_arg({:d})'.format(arg_idx) arg_idx += 1 for function_item in combination: |