Source code for sherpa.models.parameter

#
#  Copyright (C) 2007, 2017  Smithsonian Astrophysical Observatory
#
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 3 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License along
#  with this program; if not, write to the Free Software Foundation, Inc.,
#  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#

"""Support for model parameter values.
"""

import logging
import numpy
from sherpa.utils import SherpaFloat, NoNewAttributesAfterInit
from sherpa.utils.err import ParameterErr

warning = logging.getLogger(__name__).warning


__all__ = ('Parameter', 'CompositeParameter', 'ConstantParameter',
           'UnaryOpParameter', 'BinaryOpParameter')


# Default minimum and maximum magnitude for parameters
# tinyval = 1.0e-120
# hugeval = 1.0e+120
# tinyval = 1.0e-38
# hugeval = 1.0e+38
#
# Use FLT_TINY and FLT_MAX
tinyval = numpy.float(numpy.finfo(numpy.float32).tiny)
hugeval = numpy.float(numpy.finfo(numpy.float32).max)


def _make_set_limit(name):
    def _set_limit(self, val):
        val = SherpaFloat(val)
        # Ensure that we don't try to set any value that is outside
        # the hard parameter limits.
        if val < self._hard_min:
            raise ParameterErr('edge', self.fullname,
                               'hard minimum', self._hard_min)
        if val > self._hard_max:
            raise ParameterErr('edge', self.fullname,
                               'hard maximum', self._hard_max)

        # Ensure that we don't try to set a parameter range, such that
        # the minimum will be greater than the current parameter value,
        # or that the maximum will be less than the current parameter value.

        # But we only want to do this check *after* parameter has been
        # created and fully initialized; we are doing this check some time
        # *later*, when the user is trying to reset a parameter range
        # such that the new range will leave the current value
        # *outside* the new range.  We want to warn against and disallow that.

        # Due to complaints about having to rewrite existing user scripts,
        # downgrade the ParameterErr issued here to mere warnings.  Also,
        # set the value to the appropriate soft limit.
        if hasattr(self, "_NoNewAttributesAfterInit__initialized") and \
           self._NoNewAttributesAfterInit__initialized:
            if name == "_min" and (val > self.val):
                self.val = val
                warning(('parameter %s less than new minimum; %s reset to %g') % (self.fullname, self.fullname, self.val))
            if name == "_max" and (val < self.val):
                self.val = val
                warning(('parameter %s greater than new maximum; %s reset to %g') % (self.fullname, self.fullname, self.val))

        setattr(self, name, val)

    return _set_limit


def _make_unop(op, opstr):
    def func(self):
        return UnaryOpParameter(self, op, opstr)
    return func


def _make_binop(op, opstr):
    def func(self, rhs):
        return BinaryOpParameter(self, rhs, op, opstr)

    def rfunc(self, lhs):
        return BinaryOpParameter(lhs, self, op, opstr)

    return (func, rfunc)


[docs]class Parameter(NoNewAttributesAfterInit): """Represent a model parameter. Parameters ---------- modelname : str The name of the model component containing the parameter. name : str The name of the parameter. It should be considered to be matched in a case-insensitive manner. val : number The default value for the parameter. min, max, hard_min, hard_max: number, optional The soft and hard limits for the parameter value. units : str, optional The units for the parameter value. frozen : bool, optional Does the parameter default to being frozen? alwaysfrozen : bool, optional If set then the parameter can never be thawed. hidden : bool, optional Should the parameter be included when displaying the model contents? aliases : None or list of str If not None then alternative names for the parameter (these are expected to be matched in a case-insensitive manner). """ # # Read-only properties # def _get_alwaysfrozen(self): return self._alwaysfrozen alwaysfrozen = property(_get_alwaysfrozen) def _get_hard_min(self): return self._hard_min hard_min = property(_get_hard_min) def _get_hard_max(self): return self._hard_max hard_max = property(_get_hard_max) # # 'val' property # def _get_val(self): if hasattr(self, 'eval'): return self.eval() if self.link is not None: return self.link.val return self._val def _set_val(self, val): if isinstance(val, Parameter): self.link = val else: # Reset link self.link = None # Validate new value val = SherpaFloat(val) if val < self.min: raise ParameterErr('edge', self.fullname, 'minimum', self.min) if val > self.max: raise ParameterErr('edge', self.fullname, 'maximum', self.max) self._val = val self._default_val = val val = property(_get_val, _set_val) # # '_default_val' property # def _get_default_val(self): if hasattr(self, 'eval'): return self.eval() if self.link is not None: return self.link.default_val return self._default_val def _set_default_val(self, default_val): if isinstance(default_val, Parameter): self.link = default_val else: # Reset link self.link = None # Validate new value default_val = SherpaFloat(default_val) if default_val < self.min: raise ParameterErr('edge', self.fullname, 'minimum', self.min) if default_val > self.max: raise ParameterErr('edge', self.fullname, 'maximum', self.max) self._default_val = default_val default_val = property(_get_default_val, _set_default_val) # # 'min' and 'max' properties # def _get_min(self): return self._min min = property(_get_min, _make_set_limit('_min')) def _get_max(self): return self._max max = property(_get_max, _make_set_limit('_max')) # # 'default_min' and 'default_max' properties # def _get_default_min(self): return self._default_min default_min = property(_get_default_min, _make_set_limit('_default_min')) def _get_default_max(self): return self._default_max default_max = property(_get_default_max, _make_set_limit('_default_max')) # # 'frozen' property # def _get_frozen(self): if self.link is not None: return True return self._frozen def _set_frozen(self, val): val = bool(val) if self._alwaysfrozen and (not val): raise ParameterErr('alwaysfrozen', self.fullname) self._frozen = val frozen = property(_get_frozen, _set_frozen) # # 'link' property' # def _get_link(self): return self._link def _set_link(self, link): if link is not None: if self._alwaysfrozen: raise ParameterErr('frozennolink', self.fullname) if not isinstance(link, Parameter): raise ParameterErr('notlink') # Short cycles produce error # e.g. par = 2*par+3 if self in link: raise ParameterErr('linkcycle') # Correctly test for link cycles in long trees. cycle = False ll = link while isinstance(ll, Parameter): if ll == self or self in ll: cycle = True ll = ll.link # Long cycles are overwritten BUG #12287 if cycle and isinstance(link, Parameter): link.link = None self._link = link link = property(_get_link, _set_link) # # Methods # def __init__(self, modelname, name, val, min=-hugeval, max=hugeval, hard_min=-hugeval, hard_max=hugeval, units='', frozen=False, alwaysfrozen=False, hidden=False, aliases=None): self.modelname = modelname self.name = name self.fullname = '%s.%s' % (modelname, name) self._hard_min = SherpaFloat(hard_min) self._hard_max = SherpaFloat(hard_max) self.units = units self._alwaysfrozen = bool(alwaysfrozen) if alwaysfrozen: self._frozen = True else: self._frozen = frozen self.hidden = hidden # Set validated attributes. Access them via their properties so that # validation takes place. self.min = min self.max = max self.val = val self.default_min = min self.default_max = max self.default_val = val self.link = None self._guessed = False self.aliases = [a.lower() for a in aliases] if aliases is not None else [] NoNewAttributesAfterInit.__init__(self) def __iter__(self): return iter([self]) def __repr__(self): r = "<%s '%s'" % (type(self).__name__, self.name) if self.modelname: r += " of model '%s'" % self.modelname r += '>' return r def __str__(self): if self.link is not None: linkstr = self.link.fullname else: linkstr = str(None) return (('val = %s\n' + 'min = %s\n' + 'max = %s\n' + 'units = %s\n' + 'frozen = %s\n' + 'link = %s\n' 'default_val = %s\n' + 'default_min = %s\n' + 'default_max = %s') % (str(self.val), str(self.min), str(self.max), self.units, self.frozen, linkstr, str(self.default_val), str(self.default_min), str(self.default_max))) # Unary operations __neg__ = _make_unop(numpy.negative, '-') __abs__ = _make_unop(numpy.absolute, 'abs') # Binary operations __add__, __radd__ = _make_binop(numpy.add, '+') __sub__, __rsub__ = _make_binop(numpy.subtract, '-') __mul__, __rmul__ = _make_binop(numpy.multiply, '*') __div__, __rdiv__ = _make_binop(numpy.divide, '/') __floordiv__, __rfloordiv__ = _make_binop(numpy.floor_divide, '//') __truediv__, __rtruediv__ = _make_binop(numpy.true_divide, '/') __mod__, __rmod__ = _make_binop(numpy.remainder, '%') __pow__, __rpow__ = _make_binop(numpy.power, '**')
[docs] def freeze(self): """Set the `frozen` attribute for the parameter. See Also -------- thaw """ self.frozen = True
[docs] def thaw(self): """Unset the `frozen` attribute for the parameter. See Also -------- frozen """ self.frozen = False
[docs] def reset(self): # circumvent the attr checks for simplicity, as the defaults have # already passed (defaults either set by user or through self.set). if self._guessed: self._min = self.default_min self._max = self.default_max self._guessed = False self._val = self.default_val
[docs] def set(self, val=None, min=None, max=None, frozen=None, default_val=None, default_min=None, default_max=None): """Change a parameter setting.""" if max is not None and max > self.max: self.max = max if default_max is not None and default_max > self.default_max: self.default_max = default_max if min is not None and min < self.min: self.min = min if default_min is not None and default_min < self.default_min: self.default_min = default_min if val is not None: self.val = val if default_val is not None: self.default_val = default_val if min is not None: self.min = min if max is not None: self.max = max if default_min is not None: self.default_min = default_min if default_max is not None: self.default_max = default_max if frozen is not None: self.frozen = frozen
[docs]class CompositeParameter(Parameter): def __init__(self, name, parts): self.parts = tuple(parts) Parameter.__init__(self, '', name, 0.0) self.fullname = name def __iter__(self): return iter(self._get_parts()) def _get_parts(self): parts = [] for p in self.parts: # A CompositeParameter should not hold a reference to itself assert (p is not self), (("'%s' object holds a reference to " + "itself") % type(self).__name__) parts.append(p) if isinstance(p, CompositeParameter): parts.extend(p._get_parts()) # FIXME: do we want to remove duplicate components from parts? return parts
[docs] def eval(self): raise NotImplementedError
[docs]class ConstantParameter(CompositeParameter): def __init__(self, value): self.value = SherpaFloat(value) CompositeParameter.__init__(self, str(value), ())
[docs] def eval(self): return self.value
[docs]class UnaryOpParameter(CompositeParameter): def __init__(self, arg, op, opstr): self.arg = arg self.op = op CompositeParameter.__init__(self, '%s(%s)' % (opstr, self.arg.fullname), (self.arg,))
[docs] def eval(self): return self.op(self.arg.val)
[docs]class BinaryOpParameter(CompositeParameter):
[docs] @staticmethod def wrapobj(obj): if isinstance(obj, Parameter): return obj return ConstantParameter(obj)
def __init__(self, lhs, rhs, op, opstr): self.lhs = self.wrapobj(lhs) self.rhs = self.wrapobj(rhs) self.op = op CompositeParameter.__init__(self, '(%s %s %s)' % (self.lhs.fullname, opstr, self.rhs.fullname), (self.lhs, self.rhs))
[docs] def eval(self): return self.op(self.lhs.val, self.rhs.val)