Source code for simkit.core.formulas

# -*- coding: utf-8 -*-

"""
This module provides the framework for formulas. All formulas should inherit
from the Formula class in this module. Formula sources must include a
formula importer, or can subclass one of the formula importers here.
"""

from simkit.core import logging, CommonBase, Registry, UREG, Parameter
import imp
import importlib
import os
import sys
import numexpr as ne
import inspect
from uncertainty_wrapper import unc_wrapper_args

LOGGER = logging.getLogger(__name__)


[docs]class FormulaParameter(Parameter): """ Field for data parameters. """ _attrs = ['islinear', 'args', 'units', 'isconstant']
[docs]class FormulaRegistry(Registry): """ A registry for formulas. The meta names are ``islinear``, ``args``, ``units`` and ``isconstant``. """ meta_names = ['islinear', 'args', 'units', 'isconstant']
[docs] def register(self, new_formulas, *args, **kwargs): """ Register formula and meta data. * ``islinear`` - ``True`` if formula is linear, ``False`` if non-linear. * ``args`` - position of arguments * ``units`` - units of returns and arguments as pair of tuples * ``isconstant`` - constant arguments not included in covariance :param new_formulas: new formulas to add to registry. """ kwargs.update(zip(self.meta_names, args)) # call super method, meta must be passed as kwargs! super(FormulaRegistry, self).register(new_formulas, **kwargs)
[docs]class FormulaImporter(object): """ A class that imports formulas. :param parameters: Parameters used to import formulas. :type parameters: dict :param meta: Options for formulas and formula inporters :type meta: Meta """ def __init__(self, parameters, meta=None): #: parameters to be read by reader self.parameters = parameters #: options for importer self.meta = meta
[docs] def import_formulas(self): """ This method must be implemented by each formula importer. :returns: formulas :rtype: dict :raises: :exc:`~exceptions.NotImplementedError` """ raise NotImplementedError(' '.join(['Function "import_formulas" is', 'not implemented.']))
[docs]class PyModuleImporter(FormulaImporter): """ Import formulas from a Python module. """
[docs] def import_formulas(self): """ Import formulas specified in :attr:`parameters`. :returns: formulas :rtype: dict """ # TODO: unit tests! # TODO: move this to somewhere else and call it "importy", maybe # core.__init__.py since a lot of modules might use it. module = self.meta.module # module read from parameters package = getattr(self.meta, 'package', None) # package read from meta name = package + module if package else module # concat pkg + name path = getattr(self.meta, 'path', None) # path read from parameters # import module using module and package mod = None # SEE ALSO: http://docs.python.org/2/library/imp.html#examples try: # fast path: see if module was already imported mod = sys.modules[name] except KeyError: try: # import module specified in parameters mod = importlib.import_module(module, package) except ImportError as err: if not path: msg = ('%s could not be imported either because it was not ' 'on the PYTHONPATH or path was not given.') LOGGER.exception(msg, name) raise err else: # import module using path # expand ~, environmental variables and make path absolute if not os.path.isabs(path): path = os.path.expanduser(os.path.expandvars(path)) path = os.path.abspath(path) # paths must be a list paths = [path] # imp does not find hierarchical module names, find and load # packages recursively, then load module, see last paragraph # https://docs.python.org/2/library/imp.html#imp.find_module pname = '' # full dotted name of package to load # traverse namespace while name: # if dot in name get first package if '.' in name: pkg, name = name.split('.', 1) else: pkg, name = name, None # pkg is the module # Find package or module by name and path fp, filename, desc = imp.find_module(pkg, paths) # full dotted name of package to load pname = pkg if not pname else '%s.%s' % (pname, pkg) LOGGER.debug('package name: %s', pname) # try to load the package or module try: mod = imp.load_module(pname, fp, filename, desc) finally: if fp: fp.close() # append package paths for imp.find_module if name: paths = mod.__path__ formulas = {} # an empty list of formulas formula_param = self.parameters # formulas key # FYI: iterating over dictionary is equivalent to iterkeys() if isinstance(formula_param, (list, tuple, dict)): # iterate through formulas for f in formula_param: formulas[f] = getattr(mod, f) elif isinstance(formula_param, basestring): # only one formula # FYI: use basestring to test for str and unicode # SEE: http://docs.python.org/2/library/functions.html#basestring formulas[formula_param] = getattr(mod, formula_param) else: # autodetect formulas assuming first letter is f formulas = {f: getattr(mod, f) for f in dir(mod) if f[:2] == 'f_'} if not len(formulas): for f in dir(mod): mod_attr = getattr(mod, f) if inspect.isfunction(mod_attr): formulas[f] = mod_attr return formulas
[docs]class NumericalExpressionImporter(FormulaImporter): """ Import formulas from numerical expressions using Python Numexpr. """
[docs] def import_formulas(self): formulas = {} # an empty list of formulas formula_param = self.parameters # formulas key for f, p in formula_param.iteritems(): formulas[f] = lambda *args: ne.evaluate( p['extras']['expression'], {k: a for k, a in zip(p['args'], args)}, {} ).reshape(1, -1) LOGGER.debug('formulas %s = %r', f, formulas[f]) return formulas
[docs]class FormulaBase(CommonBase): """ Metaclass for formulas. """ _path_attr = 'formulas_path' _file_attr = 'formulas_file' _param_cls = FormulaParameter def __new__(mcs, name, bases, attr): # use only with Formula subclasses if not CommonBase.get_parents(bases, FormulaBase): return super(FormulaBase, mcs).__new__(mcs, name, bases, attr) # set _meta combined from bases attr = mcs.set_meta(bases, attr) # set param file full path if formulas path and file specified or # try to set parameters from class attributes except private/magic attr = mcs.set_param_file_or_parameters(attr) return super(FormulaBase, mcs).__new__(mcs, name, bases, attr)
[docs]class Formula(object): """ A class for formulas. Specify ``formula_importer`` which must subclass :class:`FormulaImporter` to import formula source files as class. If no ``formula_importer`` is specified, the default is :class:`~simkit.core.formulas.PyModuleImporter`. Specify ``formula_path`` and ``formula_file`` that contains formulas in string form or parameters used to import the formula source file. This is the required interface for all source files containing formulas used in SimKit. """ __metaclass__ = FormulaBase def __init__(self): # check for path listed in param file path = getattr(self._meta, 'path', None) if path is None: proxy_file = self.param_file if self.param_file else __file__ # use the same path as the param file or this file if no param file self._meta.path = os.path.dirname(proxy_file) # check for path listed in param file formula_importer = getattr(self._meta, 'formula_importer', None) if formula_importer is None: #: formula importer class, default is ``PyModuleImporter`` self._meta.formula_importer = PyModuleImporter meta = getattr(self, '_meta', None) # options for formulas importer_instance = self._meta.formula_importer(self.parameters, meta) #: formulas loaded by the importer using specified parameters self.formulas = importer_instance.import_formulas() #: linearity determined by each data source? self.islinear = {} #: positional arguments self.args = {} #: expected units of returns and arguments as pair of tuples self.units = {} #: constant arguments that are not included in covariance calculation self.isconstant = {} # sequence of formulas, don't propagate uncertainty or units for f in self.formulas: self.islinear[f] = True self.args[f] = inspect.getargspec(self.formulas[f]).args formula_param = self.parameters # formulas key # if formulas is a list or if it can't be iterated as a dictionary # then log warning and return try: formula_param_generator = formula_param.iteritems() except AttributeError as err: LOGGER.warning('Attribute Error: %s', err.message) return # formula dictionary for k, v in formula_param_generator: if not v: # skip formula if attributes are null or empty continue # get islinear formula attribute is_linear = v.get('islinear') if is_linear is not None: self.islinear[k] = is_linear # get positional arguments f_args = v.get('args') if f_args is not None: self.args[k] = f_args # get constant arguments to exclude from covariance self.isconstant[k] = v.get('isconstant') if self.isconstant[k] is not None: argn = [n for n, a in enumerate(self.args[k]) if a not in self.isconstant[k]] LOGGER.debug('%s arg nums: %r', k, argn) self.formulas[k] = unc_wrapper_args(*argn)(self.formulas[k]) # get units of returns and arguments self.units[k] = v.get('units') if self.units[k] is not None: # append units for covariance and Jacobian if all args # constant and more than one return output if self.isconstant[k] is not None: # check if retval units is a string or None before adding # extra units for Jacobian and covariance ret_units = self.units[k][0] if isinstance(ret_units, basestring) or ret_units is None: self.units[k][0] = [ret_units] try: self.units[k][0] += [None, None] except TypeError: self.units[k][0] += (None, None) # wrap function with Pint's unit wrapper self.formulas[k] = UREG.wraps(*self.units[k])( self.formulas[k] ) def __getitem__(self, item): return self.formulas[item]