from __future__ import absolute_import, division, print_function, unicode_literals
import numpy as np
import six
from ..utils.logger import setup_module_logger
[docs]class Features(object):
"""
Class for calculating features of a model.
Parameters
----------
new_features : {None, callable, list of callables}
The new features to add. The feature functions have the requirements
stated in ``reference_feature``. If None, no features are added.
Default is None.
features_to_run : {"all", None, str, list of feature names}, optional
Which features to calculate uncertainties for.
If ``"all"``, the uncertainties are calculated for all
implemented and assigned features.
If None, or an empty list ``[]``, no features are
calculated.
If str, only that feature is calculated.
If list of feature names, all the listed features are
calculated. Default is ``"all"``.
new_utility_methods : {None, list}, optional
A list of new utility methods. All methods in this class that is not in
the list of utility methods, is considered to be a feature.
Default is None.
interpolate : {None, "all", str, list of feature names}, optional
Which features are irregular, meaning they have a varying number of
time points between evaluations. An interpolation is performed on
each irregular feature to create regular results.
If ``"all"``, all features are interpolated.
If None, or an empty list, no features are interpolated.
If str, only that feature is interpolated.
If list of feature names, all listed features are interpolated.
Default is None.
labels : dictionary, optional
A dictionary with key as the feature name and the value as a list of
labels for each axis. The number of elements in the list corresponds
to the dimension of the feature. Example:
.. code-block:: Python
new_labels = {"0d_feature": ["x-axis"],
"1d_feature": ["x-axis", "y-axis"],
"2d_feature": ["x-axis", "y-axis", "z-axis"]
}
logger_level : {"info", "debug", "warning", "error", "critical", None}, optional
Set the threshold for the logging level. Logging messages less severe
than this level is ignored. If None, no logging is performed.
Default logger level is "info".
Attributes
----------
features_to_run : list
Which features to calculate uncertainties for.
interpolate : list
A list of irregular features to be interpolated.
utility_methods : list
A list of all utility methods implemented. All methods in this class
that is not in the list of utility methods is considered to be a feature.
labels : dictionary
Labels for the axes of each feature, used when plotting.
See also
--------
uncertainpy.features.Features.reference_feature : reference_feature showing the requirements of a feature function.
"""
def __init__(self,
new_features=None,
features_to_run="all",
new_utility_methods=None,
interpolate=None,
labels={},
preprocess=None,
logger_level="info"):
self.utility_methods = ["calculate_feature",
"calculate_features",
"calculate_all_features",
"__init__",
"implemented_features",
"preprocess",
"add_features",
"reference_feature",
"_preprocess",
"validate"]
if new_utility_methods is None:
new_utility_methods = []
self._features_to_run = []
self._interpolate = None
self._labels = {}
self.utility_methods += new_utility_methods
self.interpolate = interpolate
if new_features is not None:
self.add_features(new_features, labels=labels)
if preprocess is not None:
self.preprocess = preprocess
self.labels = labels
self.features_to_run = features_to_run
setup_module_logger(class_instance=self, level=logger_level)
@property
def preprocess(self):
"""
Preprossesing of the time `time` and results `values` from the model, before the
features are calculated.
No preprocessing is performed, and the direct model results are
currently returned. If preprocessing is needed it should follow the
below format.
Parameters
----------
*model_results
Variable length argument list. Is the values that ``model.run()``
returns. By default it contains `time` and `values`, and then any number of
optional `info` values.
Returns
-------
preprocess_results
Returns any number of values that are sent to each feature.
The values returned must compatible with the input arguments of
all features.
Notes
-----
Perform a preprossesing of the model results before the results are sent
to the calculation of each feature. It is used to perform common
calculations that each feature needs to perform, to reduce the number of
necessary calculations. The values returned must therefore be compatible
with the input arguments to each features.
See also
--------
uncertainpy.models.Model.run : The model run method
"""
return self._preprocess
def _preprocess(self, *model_result):
return model_result
@preprocess.setter
def preprocess(self, new_preprocess_function):
if not callable(new_preprocess_function):
raise TypeError("preprocess function must be callable")
self._preprocess = new_preprocess_function
@property
def labels(self):
"""
Labels for the axes of each feature, used when plotting.
Parameters
----------
new_labels : dictionary
A dictionary with key as the feature name and the value as a list of
labels for each axis. The number of elements in the list corresponds
to the dimension of the feature. Example:
.. code-block:: Python
new_labels = {"0d_feature": ["x-axis"],
"1d_feature": ["x-axis", "y-axis"],
"2d_feature": ["x-axis", "y-axis", "z-axis"]
}
"""
return self._labels
@labels.setter
def labels(self, new_labels):
self.labels.update(new_labels)
@property
def features_to_run(self):
"""
Which features to calculate uncertainties for.
Parameters
----------
new_features_to_run : {"all", None, str, list of feature names}
Which features to calculate uncertainties for.
If ``"all"``, the uncertainties are calculated for all
implemented and assigned features.
If None, or an empty list , no features are
calculated.
If str, only that feature is calculated.
If list of feature names, all listed features are
calculated. Default is ``"all"``.
Returns
-------
list
A list of features to calculate uncertainties for.
"""
return self._features_to_run
@features_to_run.setter
def features_to_run(self, new_features_to_run):
if new_features_to_run == "all":
self._features_to_run = self.implemented_features()
elif new_features_to_run is None:
self._features_to_run = []
elif isinstance(new_features_to_run, six.string_types):
self._features_to_run = [new_features_to_run]
else:
self._features_to_run = new_features_to_run
@property
def interpolate(self):
"""
Features that require an interpolation.
Which features are interpolated, meaning they have a varying number of
time points between evaluations. An interpolation is performed on
each interpolated feature to create regular results.
Parameters
----------
new_interpolate : {None, "all", str, list of feature names}
If ``"all"``, all features are interpolated.
If None, or an empty list, no features are interpolated.
If str, only that feature is interpolated.
If list of feature names, all listed features are interpolated.
Default is None.
Returns
-------
list
A list of irregular features to be interpolated.
"""
return self._interpolate
@interpolate.setter
def interpolate(self, new_interpolate):
if new_interpolate == "all":
self._interpolate = self.implemented_features()
elif new_interpolate is None:
self._interpolate = []
elif isinstance(new_interpolate, six.string_types):
self._interpolate = [new_interpolate]
else:
self._interpolate = new_interpolate
[docs] def add_features(self, new_features, labels={}):
"""
Add new features.
Parameters
----------
new_features : {callable, list of callables}
The new features to add. The feature functions have the requirements
stated in ``reference_feature``.
labels : dictionary, optional
A dictionary with the labels for the new features. The keys are the
feature function names and the values are a list of labels for each
axis. The number of elements in the list corresponds
to the dimension of the feature. Example:
.. code-block:: Python
new_labels = {"0d_feature": ["x-axis"],
"1d_feature": ["x-axis", "y-axis"],
"2d_feature": ["x-axis", "y-axis", "z-axis"]
}
Raises
------
TypeError
Raises a TypeError if `new_features` is not callable or list of
callables.
Notes
-----
The features added are not added to ``features_to_run``.
``features_to_run`` must be set manually afterwards.
See also
--------
uncertainpy.features.Features.reference_feature : reference_feature showing the requirements of a feature function.
"""
if callable(new_features):
setattr(self, new_features.__name__, new_features)
# self.features_to_run.append(new_features.__name__)
tmp_label = labels.get(new_features.__name__)
if tmp_label is not None:
self.labels[new_features.__name__] = tmp_label
else:
try:
for feature in new_features:
if callable(feature):
setattr(self, feature.__name__, feature)
# self.features_to_run.append(feature.__name__)
tmp_lables = labels.get(feature.__name__)
if tmp_lables is not None:
self.labels[feature.__name__] = tmp_lables
else:
raise TypeError("Feature in iterable is not callable")
except TypeError as error:
msg = "Added features must be a callable or list of callables"
if not error.args:
error.args = ("",)
error.args = error.args + (msg,)
raise
[docs] def calculate_feature(self, feature_name, *preprocess_results):
"""
Calculate feature with `feature_name`.
Parameters
----------
feature_name : str
Name of feature to calculate.
*preprocess_results
The values returned by ``preprocess``. These values are sent
as input arguments to each feature. By default preprocess returns
the values that ``model.run()`` returns, which contains `time` and
`values`, and then any number of optional `info` values.
The implemented features require that `info` is a single
dictionary with the information stored as key-value pairs.
Certain features require specific keys to be present.
Returns
-------
time : {None, numpy.nan, array_like}
Time values, or equivalent, of the feature, if no time values
returns None or numpy.nan.
values : array_like
The feature results, `values` must either be regular (have the same
number of points for different paramaters) or be able to be
interpolated.
Raises
------
TypeError
If `feature_name` is a utility method.
See also
--------
uncertainpy.models.Model.run : The model run method
"""
if feature_name in self.utility_methods:
raise TypeError("{} is a utility method".format(feature_name))
try:
feature_result = getattr(self, feature_name)(*preprocess_results)
except Exception as error:
msg = "Error when calculating: {}".format(feature_name)
if not error.args:
error.args = ("",)
error.args = error.args + (msg,)
raise
self.validate(feature_name, *feature_result)
return feature_result
[docs] def validate(self, feature_name, *feature_result):
"""
Validate the results from ``calculate_feature``.
This method ensures each returns `time`, `values`.
Parameters
----------
model_results
Any type of model results returned by ``run``.
feature_name : str
Name of the feature, to create better error messages.
Raises
------
ValueError
If the model result does not fit the requirements.
TypeError
If the model result does not fit the requirements.
Notes
-----
Tries to verify that at least, `time` and `values` are returned from ``run``.
``model_result`` should follow the format: ``return time, values, info_1, info_2, ...``.
Where:
* ``time_feature`` : ``{None, numpy.nan, array_like}``
Time values, or equivalent, of the feature, if no time values
return None or numpy.nan.
* ``values`` : ``{None, numpy.nan, array_like}``
The feature results, `values` must either be regular (have the same
number of points for different paramaters) or be able to be
interpolated. If there are no feature results return
None or ``numpy.nan`` instead of `values` and that evaluation are
disregarded.
"""
if isinstance(feature_result, np.ndarray):
raise ValueError("{} returns an numpy array. ".format(feature_name) +
"This indicates only time or values is returned. " +
"{} must return time and values".format(feature_name) +
"(return time, values | return None, values)")
if isinstance(feature_result, six.string_types):
raise ValueError("{} returns a string. ".format(feature_name) +
"This indicates only time or values is returned. " +
"{} must return time and values".format(feature_name) +
"(return time, values | return None, values)")
# Check that time, and values is returned
try:
time_feature, values_feature = feature_result
except (ValueError, TypeError) as error:
msg = "feature {} must return time and values (return time, values | return None, values)".format(feature_name)
if not error.args:
error.args = ("",)
error.args = error.args + (msg,)
raise
[docs] def calculate_features(self, *model_results):
"""
Calculate all features in ``features_to_run``.
Parameters
----------
*model_results
Variable length argument list. Is the values that ``model.run()``
returns. By default it contains `time` and `values`, and then any number of
optional `info` values.
Returns
-------
results : dictionary
A dictionary where the keys are the feature names
and the values are a dictionary with the time values `time` and feature
results on `values`, on the form ``{"time": time, "values": values}``.
Raises
------
TypeError
If `feature_name` is a utility method.
Notes
-----
Checks that the feature returns two values.
See also
--------
uncertainpy.features.Features.calculate_feature : Method for calculating a single feature.
"""
preprocess_results = self.preprocess(*model_results)
results = {}
for feature in self.features_to_run:
time_feature, values_feature = self.calculate_feature(feature, *preprocess_results)
results[feature] = {"time": time_feature, "values": values_feature}
return results
[docs] def calculate_all_features(self, *model_results):
"""
Calculate all implemented features.
Parameters
----------
*model_results
Variable length argument list. Is the values that ``model.run()``
returns. By default it contains `time` and `values`, and then any number of
optional `info` values.
Returns
-------
results : dictionary
A dictionary where the keys are the feature names
and the values are a dictionary with the time values `time` and feature
results on `values`, on the form ``{"time": t, "values": U}``.
Raises
------
TypeError
If `feature_name` is a utility method.
Notes
-----
Checks that the feature returns two values.
See also
--------
uncertainpy.features.Features.calculate_feature : Method for calculating a single feature.
"""
preprocess_results = self.preprocess(*model_results)
results = {}
for feature in self.implemented_features():
time_feature, values_feature = self.calculate_feature(feature, *preprocess_results)
results[feature] = {"time": time_feature, "values": values_feature}
return results
[docs] def implemented_features(self):
"""
Return a list of all callable methods in feature, that are not utility
methods, does not starts with "_" and not a method of a general python object.
Returns
-------
list
A list of all callable methods in feature, that are not utility
methods.
"""
return [method for method in dir(self) if callable(getattr(self, method)) and method not in self.utility_methods and method not in dir(object) and not method.startswith("_")]
[docs] def reference_feature(self, *preprocess_results):
"""
An example feature. Feature function have the following requirements.
Parameters
----------
*preprocess_results
Variable length argument list. Is the values that
``Features.preprocess`` returns. By default ``Features.preprocess``
returns the same values as ``Model.run`` returns.
Returns
-------
time : {None, numpy.nan, array_like}
Time values, or equivalent, of the feature, if no time values
return None or numpy.nan.
values : array_like
The feature results, `values` must either be regular (have the same
number of points for different paramaters) or be able to be
interpolated. If there are no feature results return
None or numpy.nan instead of `values` and that evaluation are
disregarded.
See also
--------
uncertainpy.features.Features.preprocess : The features preprocess method.
uncertainpy.models.Model.run : The model run method
uncertainpy.models.Model.postprocess : The postprocessing method.
"""
# Perform feature calculations here
time = None
values = None
return time, values