Source code for pymodeler.parameter

#!/usr/bin/env python
"""
Classes used to describe aspect of Models.

The base class is `Property` which describes any one property of a model,
such as the name, or some other fixed property.

The `Parameter` class describes variable model parameters.

The `Derived` class describes model properies that are derived
from other model properties.

"""
from __future__ import absolute_import, division, print_function

from copy import deepcopy
from numbers import Number
from collections import OrderedDict as odict

import numpy as np
import yaml

# Python 3 compatibility
try:
    basestring
except NameError:
    basestring = str


def asscalar(a):
    """Convert single-item lists and numpy arrays to scalars. Does
    not care about the type of the elements (i.e., will work fine on
    strings, etc.)

    https://github.com/numpy/numpy/issues/4701
    https://github.com/numpy/numpy/pull/5126
    """
    try:
        return a.item()
    except AttributeError:
        return np.asarray(a).item()


def defaults_docstring(defaults, header=None, indent=None, footer=None):
    """Return a docstring from a list of defaults.
    """
    if indent is None:
        indent = ''
    if header is None:
        header = ''
    if footer is None:
        footer = ''

    width = 60
    #hbar = indent + width * '=' + '\n'  # horizontal bar
    hbar = '\n'

    s = hbar + (header) + hbar
    for key, value, desc in defaults:
        if isinstance(value, basestring):
            value = "'" + value + "'"
        if hasattr(value, '__call__'):
            value = "<" + value.__name__ + ">"

        s += indent +'%-12s\n' % ("%s :" % key)
        s += indent + indent + (indent + 23 * ' ').join(desc.split('\n'))
        s += ' [%s]\n\n' % str(value)
    s += hbar
    s += footer
    return s


def defaults_decorator(defaults):
    """Decorator to append default kwargs to a function.
    """
    def decorator(func):
        """Function that appends default kwargs to a function.
        """
        kwargs = dict(header='Keyword arguments\n-----------------\n', 
                      indent='  ',
                      footer='\n')
        doc = defaults_docstring(defaults, **kwargs)
        if func.__doc__ is None:
            func.__doc__ = ''
        func.__doc__ += doc
        return func

    return decorator


class Meta(type):
    """Meta class for appending docstring with defaults
    """
    def __new__(mcs, name, bases, attrs):
        attrs['_doc'] = attrs.get('__doc__', '')
        return super(Meta, mcs).__new__(mcs, name, bases, attrs)

    @property
    def __doc__(cls):
        kwargs = dict(header='Parameters\n----------\n', 
                      indent='  ',
                      footer='\n')
        return cls._doc + cls.defaults_docstring(**kwargs)


[docs]class Property(object): """Base class for model properties. This class and its sub-classes implement variations on the concept of a 'mutable' value or 'l-value', i.e., an object that can be assigned a value. This class defines some interfaces that help read/write heirachical sets of properties between various formats (python dictionaries, yaml files, astropy tables, etc..) The pymodeler.model.Model class maps from property names to Property instances. """ __metaclass__ = Meta __value__ = None defaults = [ ('value', __value__, 'Property value'), ('help', "", 'Help description'), ('format', '%s', 'Format string for printing'), ('dtype', None, 'Data type'), ('default', None, 'Default value'), ('required', False, 'Is this propery required?'), ('unit', None, 'Units associated to value'), ] @defaults_decorator(defaults) def __init__(self, **kwargs): self._load(**kwargs) if self.__value__ is None and self.__dict__['default'] is not None: self.set_value(self.__dict__['default']) def __str__(self): return self.__value__.__str__() def __repr__(self): return self.__value__.__str__() def _load(self, **kwargs): """Load kwargs key,value pairs into __dict__ """ defaults = dict([(d[0], d[1]) for d in self.defaults]) # Require kwargs are in defaults for k in kwargs: if k not in defaults: msg = "Unrecognized attribute of %s: %s" % ( self.__class__.__name__, k) raise AttributeError(msg) defaults.update(kwargs) # This doesn't overwrite the properties self.__dict__.update(defaults) # This should now be set self.check_type(self.__dict__['default']) # This sets the underlying property values (i.e., __value__) self.set(**defaults)
[docs] @classmethod def defaults_docstring(cls, header=None, indent=None, footer=None): """Add the default values to the class docstring""" return defaults_docstring(cls.defaults, header=header, indent=indent, footer=footer)
@property def value(self): """Return the current value This may be modified by sub-classes to do additional operations (such as caching the results of complicated operations needed to compute the value) """ return self.__value__
[docs] def innertype(self): """Return the type of the current value """ return type(self.__value__)
def __call__(self): """ __call__ will return the current value By default this invokes `self.value` so, any additional functionality that sub-classes implement, (such as caching the results of complicated operations needed to compute the value) will also be invoked """ return self.value
[docs] def set(self, **kwargs): """Set the value to kwargs['value'] The invokes hooks for type-checking and bounds-checking that may be implemented by sub-classes. """ if 'value' in kwargs: self.set_value(kwargs.pop('value', None))
[docs] def set_value(self, value): """Set the value This invokes hooks for type-checking and bounds-checking that may be implemented by sub-classes. """ self.check_bounds(value) self.check_type(value) self.__value__ = value
[docs] def clear_value(self): """Set the value to None This can be useful for sub-classes that use None to indicate an un-initialized value. Note that this invokes hooks for type-checking and bounds-checking that may be implemented by sub-classes, so it should will need to be re-implemented if those checks do note accept None as a valid value. """ self.set_value(None)
[docs] def todict(self): """Convert to a '~collections.OrderedDict' object. By default this only assigns {'value':self.value} """ return odict(value=self.value)
[docs] def dump(self): """Dump this object as a yaml string """ return yaml.dump(self)
[docs] def check_bounds(self, value): """Hook for bounds-checking, invoked during assignment. Sub-classes can raise an exception for out-of-bounds input values. """ pass
[docs] def check_type(self, value): """Hook for type-checking, invoked during assignment. raises TypeError if neither value nor self.dtype are None and they do not match. will not raise an exception if either value or self.dtype is None """ if self.__dict__['dtype'] is None: return elif value is None: return elif isinstance(value, self.__dict__['dtype']): return msg = "Value of type %s, when %s was expected." % ( type(value), self.__dict__['dtype']) raise TypeError(msg)
[docs]class Derived(Property): """Property sub-class for derived model properties (i.e., properties that depend on other properties) This allows specifying the expected data type and formatting string for printing, and specifying a 'loader' function by name that is used to compute the value of the property. """ defaults = deepcopy(Property.defaults) + [ ('loader', None, 'Function to load datum') ] @defaults_decorator(defaults) def __init__(self, **kwargs): super(Derived, self).__init__(**kwargs) @property def value(self): """Return the current value. This first checks if the value is cached (i.e., if `self.__value__` is not None) If it is not cached then it invokes the `loader` function to compute the value, and caches the computed value """ if self.__value__ is None: try: val = self.__dict__['loader']() except TypeError: msg = "Loader is not defined" raise TypeError(msg) try: self.set_value(val) except TypeError: msg = "Loader must return variable of type %s or None" % ( self.__dict__['dtype']) raise TypeError(msg) return self.__value__
[docs]class Parameter(Property): """Property sub-class for defining a numerical Parameter. This includes value, bounds, error estimates and fixed/free status (i.e., for fitting) Adapted from MutableNum: https://gist.github.com/jheiv/6656349 """ __value__ = None __bounds__ = None __free__ = False __errors__ = None # Better to keep the structure consistent with Property defaults = deepcopy(Property.defaults) + [ ('bounds', __bounds__, 'Allowed bounds for value'), ('errors', __errors__, 'Errors on this parameter'), ('free', __free__, 'Is this propery allowed to vary?'), ] # Overwrite the default dtype idx = [d[0] for d in defaults].index('dtype') defaults[idx] = ('dtype', Number, 'Data type') @defaults_decorator(defaults) def __init__(self, **kwargs): super(Parameter, self).__init__(**kwargs)
[docs] def check_type(self, value): """Hook for type-checking, invoked during assignment. Allows size 1 numpy arrays and lists, but raises TypeError if value can not be cast to a scalar. """ try: scalar = asscalar(value) except ValueError as e: raise TypeError(e) super(Parameter, self).check_type(scalar)
# Comparison Methods def __eq__(self, x): return self.__value__ == x def __ne__(self, x): return self.__value__ != x def __lt__(self, x): return self.__value__ < x def __gt__(self, x): return self.__value__ > x def __le__(self, x): return self.__value__ <= x def __ge__(self, x): return self.__value__ >= x def __cmp__(self, x): return 0 if self.__value__ == x else 1 if self.__value__ > 0 else -1 # Unary Ops def __pos__(self): return +self.__value__ def __neg__(self): return -self.__value__ def __abs__(self): return abs(self.__value__) # Bitwise Unary Ops def __invert__(self): return ~self.__value__ # Arithmetic Binary Ops def __add__(self, x): return self.__value__ + x def __sub__(self, x): return self.__value__ - x def __mul__(self, x): return self.__value__ * x def __div__(self, x): return self.__value__ / x def __mod__(self, x): return self.__value__ % x def __pow__(self, x): return self.__value__ ** x def __floordiv__(self, x): return self.__value__ // x def __divmod__(self, x): return divmod(self.__value__, x) def __truediv__(self, x): return self.__value__.__truediv__(x) # Reflected Arithmetic Binary Ops def __radd__(self, x): return x + self.__value__ def __rsub__(self, x): return x - self.__value__ def __rmul__(self, x): return x * self.__value__ def __rdiv__(self, x): return x / self.__value__ def __rmod__(self, x): return x % self.__value__ def __rpow__(self, x): return x ** self.__value__ def __rfloordiv__(self, x): return x // self.__value__ def __rdivmod__(self, x): return divmod(x, self.__value__) def __rtruediv__(self, x): return x.__truediv__(self.__value__) # Bitwise Binary Ops def __and__(self, x): return self.__value__ & x def __or__(self, x): return self.__value__ | x def __xor__(self, x): return self.__value__ ^ x def __lshift__(self, x): return self.__value__ << x def __rshift__(self, x): return self.__value__ >> x # Reflected Bitwise Binary Ops def __rand__(self, x): return x & self.__value__ def __ror__(self, x): return x | self.__value__ def __rxor__(self, x): return x ^ self.__value__ def __rlshift__(self, x): return x << self.__value__ def __rrshift__(self, x): return x >> self.__value__ # ADW: Don't allow compound assignments # Compound Assignment #def __iadd__(self, x): self.set(self + x); return self #def __isub__(self, x): self.set(self - x); return self #def __imul__(self, x): self.set(self * x); return self #def __idiv__(self, x): self.set(self / x); return self #def __imod__(self, x): self.set(self % x); return self #def __ipow__(self, x): self.set(self **x); return self # Casts def __nonzero__(self): return self.__value__ != 0 def __bool__(self): return self.__value__.__bool__() def __int__(self): return self.__value__.__int__() def __float__(self): return self.__value__.__float__() def __long__(self): return self.__value__.__long__() # Conversions def __oct__(self): return self.__value__.__oct__() def __hex__(self): return self.__value__.__hex__() # Random Ops def __index__(self): return self.__value__.__index__() def __trunc__(self): return self.__value__.__trunc__() def __coerce__(self, x): return self.__value__.__coerce__(x) # Represenation # ADW: This should probably be __str__ not __repr__ def __repr__(self): if self.bounds is None: bounds = '[None, None]' else: bounds = '[%s, %s]' % (self.bounds[0], self.bounds[1]) if self.errors is None: errors = '[None, None]' else: errors = '[%s, %s]' % (self.errors[0], self.errors[1]) return "%s(%s, %s, %s, %s)" % (self.__class__.__name__, self.value, bounds, errors, self.free) @property def bounds(self): """Return the parameter bounds. None implies unbounded. """ return self.__bounds__ @property def free(self): """Return the fixd/free status """ return self.__free__ @property def errors(self): """Return the parameter uncertainties. None implies no error estimate. Single value implies symmetric errors. Two values implies low,high asymmetric errors. """ return self.__errors__ @property def symmetric_error(self): """Return the symmertic error Similar to above, but zero implies no error estimate, and otherwise this will either be the symmetric error, or the average of the low,high asymmetric errors. """ # ADW: Should this be `np.nan`? if self.__errors__ is None: return 0. if np.isscalar(self.__errors__): return self.__errors__ return 0.5 * (self.__errors__[0] + self.__errors__[1])
[docs] def item(self): """For asscalar """ return self.value
[docs] def check_bounds(self, value): """Hook for bounds-checking, invoked during assignment. raises ValueError if value is outside of bounds. does nothing if bounds is set to None. """ if self.__bounds__ is None: return if not self.__bounds__[0] <= value <= self.__bounds__[1]: msg = "Value outside bounds: %.2g [%.2g,%.2g]" msg = msg % (value, self.__bounds__[0], self.__bounds__[1]) raise ValueError(msg)
[docs] def set_bounds(self, bounds): """Set bounds """ if bounds is None: self.__bounds__ = None return self.__bounds__ = [asscalar(b) for b in bounds]
[docs] def set_free(self, free): """Set free/fixed status """ if free is None: self.__free__ = False return self.__free__ = bool(free)
[docs] def set_errors(self, errors): """Set parameter error estimate """ if errors is None: self.__errors__ = None return self.__errors__ = [asscalar(e) for e in errors]
[docs] def set(self, **kwargs): """Set the value,bounds,free,errors based on corresponding kwargs The invokes hooks for type-checking and bounds-checking that may be implemented by sub-classes. """ # Probably want to reset bounds if set fails if 'bounds' in kwargs: self.set_bounds(kwargs.pop('bounds')) if 'free' in kwargs: self.set_free(kwargs.pop('free')) if 'errors' in kwargs: self.set_errors(kwargs.pop('errors')) if 'value' in kwargs: self.set_value(kwargs.pop('value'))
[docs] def todict(self): """Convert to a '~collections.OrderedDict' object. This assigns {'value':self.value,'bounds'=self.bounds, 'free'=self.free,'errors'=self.errors} """ return odict(value=self.value, bounds=self.bounds, free=self.free, errors=self.errors)
[docs] def dump(self): """Dump this object as a yaml string """ return yaml.dump(self)
[docs] @staticmethod def representer(dumper, data): """ http://stackoverflow.com/a/14001707/4075339 http://stackoverflow.com/a/21912744/4075339 """ tag = yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG return dumper.represent_mapping( tag, data.todict().items(), flow_style=True)
Param = Parameter def odict_representer(dumper, data): """ http://stackoverflow.com/a/21912744/4075339 """ # Probably belongs in a util return dumper.represent_mapping( yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, data.items()) yaml.add_representer(odict, odict_representer) yaml.add_representer(Parameter, Parameter.representer)