#!/usr/bin/env python
##############################################################################
#
# diffpy.srfit by DANSE Diffraction group
# Simon J. L. Billinge
# (c) 2008 The Trustees of Columbia University
# in the City of New York. All rights reserved.
#
# File coded by: Chris Farrow
#
# See AUTHORS.txt for a list of people who contributed.
# See LICENSE_DANSE.txt for license information.
#
##############################################################################
"""FitContribution class.
FitContributions generate a residual function for a FitRecipe. A
FitContribution associates an Equation for generating a signal,
optionally one or more ProfileGenerators or Calculators that help in
this, and a Profile that holds the observed and calculated signals.
See the examples in the documentation for how to use a FitContribution.
"""
__all__ = ["FitContribution"]
from diffpy.srfit.exceptions import SrFitError
from diffpy.srfit.fitbase.parameter import ParameterProxy
from diffpy.srfit.fitbase.parameterset import ParameterSet
from diffpy.srfit.fitbase.profile import Profile
from diffpy.srfit.fitbase.recipeorganizer import get_equation_from_string
from diffpy.utils._deprecator import build_deprecation_message, deprecated
base = "diffpy.srfit.fitbase.FitContribution"
removal_version = "4.0.0"
setequation_dep_msg = build_deprecation_message(
base,
"setEquation",
"set_equation",
removal_version,
)
setprofile_dep_msg = build_deprecation_message(
base,
"setProfile",
"set_profile",
removal_version,
)
addprofilegenerator_dep_msg = build_deprecation_message(
base,
"addProfileGenerator",
"add_profile_generator",
removal_version,
)
getequation_dep_msg = build_deprecation_message(
base,
"getEquation",
"get_equation",
removal_version,
)
setresidualequation_dep_msg = build_deprecation_message(
base,
"setResidualEquation",
"set_residual_equation",
removal_version,
)
getresidualequation_dep_msg = build_deprecation_message(
base,
"getResidualEquation",
"get_residual_equation",
removal_version,
)
[docs]
class FitContribution(ParameterSet):
"""Organize an Equation, a Profile, and their supporting objects.
FitContributions organize an Equation that calculates the signal, and a
Profile that holds the signal. ProfileGenerators and Calculators can be
used as well. Constraints and Restraints can be created as part of a
FitContribution.
Attributes
----------
name : str
The name of this FitContribution.
profile : Profile or None
The Profile that holds the measured (and calculated)
signal. It is None until ``set_profile`` is called.
names : list of str
The names of the top-level Parameters (read only). See
``get_names``.
values : list
The values of the top-level Parameters (read only). See
``get_values``.
_calculators : dict
The managed dictionary of Calculators, indexed by name.
_constraints : dict
The dictionary of Constraints, indexed by the constrained
Parameter. Constraints can be added using the
``add_constraint`` method.
_generators : dict
The managed dictionary of ProfileGenerators, indexed by name.
_parameters : OrderedDict
The managed OrderedDict of parameters.
_restraints : set
The set of Restraints. Restraints can be added using the
``add_soft_bounds`` method.
_parsets : OrderedDict
The managed OrderedDict of ParameterSets.
_eqfactory : diffpy.srfit.equation.builder.EquationFactory
The factory that is used to create constraints and
restraints from strings.
_eq : Equation or None
The profile equation that calculates the signal.
_reseq : Equation or None
The residual equation that will be optimized.
_xname : str or None
The name of the x-variable.
_yname : str or None
The name of the y-variable.
_dyname : str or None
The name of the dy-variable.
"""
def __init__(self, name):
"""Initialize the FitContribution.
Parameters
----------
name : str
The name of this FitContribution.
"""
ParameterSet.__init__(self, name)
self._eq = None
self._reseq = None
self.profile = None
self._xname = None
self._yname = None
self._dyname = None
self._generators = {}
self._manage(self._generators)
return
[docs]
def set_profile(self, profile, xname=None, yname=None, dyname=None):
"""Assign the Profile for this FitContribution.
The x, y and dy Parameters of the Profile are added to this
FitContribution under the names ``xname``, ``yname`` and
``dyname``. The Profile is also assigned to every
ProfileGenerator of this FitContribution, and the residual
equation is set to ``"chiv"`` if a profile equation exists but no
residual equation has been set.
Parameters
----------
profile : Profile
The Profile that specifies the calculation points and that
will store the calculated signal.
xname : str, optional
The name of the independent variable from the Profile. If
this is None (default), then the name specified by the
Profile for this parameter will be used. This variable is
usable within string equations with the specified name.
yname : str, optional
The name of the observed Profile. If this is None
(default), then the name specified by the Profile for this
parameter will be used. This variable is usable within
string equations with the specified name.
dyname : str, optional
The name of the uncertainty in the observed Profile. If
this is None (default), then the name specified by the
Profile for this parameter will be used. This variable is
usable within string equations with the specified name.
Raises
------
TypeError
If ``profile`` is not a Profile instance.
"""
# Enforce type of the profile argument
if not isinstance(profile, Profile):
emsg = "Argument must be an instance of the Profile class."
raise TypeError(emsg)
# Set the Profile and add its parameters to this organizer.
self.profile = profile
if xname is None:
xname = self.profile.xpar.name
if yname is None:
yname = self.profile.ypar.name
if dyname is None:
dyname = self.profile.dypar.name
self._xname = xname
self._yname = yname
self._dyname = dyname
xpar = ParameterProxy(xname, self.profile.xpar)
ypar = ParameterProxy(yname, self.profile.ypar)
dypar = ParameterProxy(dyname, self.profile.dypar)
self.add_parameter(xpar, check=False)
self.add_parameter(ypar, check=False)
self.add_parameter(dypar, check=False)
# If we have ProfileGenerators, set their Profiles.
for gen in self._generators.values():
gen.set_profile(profile)
# If we have _eq, but not _reseq, set the residual
if self._eq is not None and self._reseq is None:
self.set_residual_equation("chiv")
return
@deprecated(setprofile_dep_msg)
def setProfile(self, profile, xname=None, yname=None, dyname=None):
"""This function has been deprecated and will be removed in version
4.0.0.
Please use diffpy.srfit.fitbase.FitContribution.set_profile instead.
"""
return self.set_profile(
profile, xname=xname, yname=yname, dyname=dyname
)
[docs]
def add_profile_generator(self, gen, name=None):
"""Add a ProfileGenerator to be used by this FitContribution.
The ProfileGenerator is given a name so that it can be used as part of
the profile equation (see ``set_equation``). This can be different
from the name of the ProfileGenerator used for attribute access.
FitContributions should not share ProfileGenerator instances. Different
ProfileGenerators can share Parameters and ParameterSets, however.
Calling ``add_profile_generator`` sets the profile equation to call
the calculator if there is not a profile equation already.
Parameters
----------
gen : ProfileGenerator
The ProfileGenerator instance to add.
name : str, optional
The name used for the ProfileGenerator in the profile
equation. If name is None (default), then the
ProfileGenerator's name attribute will be used.
Raises
------
ValueError
If the ProfileGenerator has no name, or if the
ProfileGenerator has the same name as some other managed
object.
"""
if name is None:
name = gen.name
# Register the generator with the equation factory and add it as a
# managed object.
self._eqfactory.register_operator(name, gen)
self._add_object(gen, self._generators, True)
# If we have a profile, set the profile of the generator.
if self.profile is not None:
gen.set_profile(self.profile)
# Make this our equation if we don't have one. This will set the
# residual equation if necessary.
if self._eq is None:
self.set_equation(name)
return
@deprecated(addprofilegenerator_dep_msg)
def addProfileGenerator(self, gen, name=None):
"""This function has been deprecated and will be removed in version
4.0.0.
Please use
diffpy.srfit.fitbase.FitContribution.add_profile_generator
instead.
"""
self.add_profile_generator(gen, name=name)
return
[docs]
def set_equation(self, eqstr, ns={}):
"""Set the profile equation for the FitContribution.
This sets the equation that will be used when generating the residual
for this FitContribution. The equation will be usable within
``set_residual_equation`` as ``"eq"``, and it takes no arguments.
Parameters
----------
eqstr : str
The string representation of the equation. Any Parameter
registered by ``add_parameter`` or ``set_profile``, or function
registered by ``register_calculator``, ``register_function`` or
``register_string_function`` can be used in the equation
by name. Other names will be turned into Parameters of this
FitContribution.
ns : dict, optional
The dictionary of Parameters, indexed by name, that are used
in the eqstr, but not registered (default {}).
Raises
------
ValueError
If ns uses a name that is already used for a variable.
"""
# Build the equation instance.
eq = get_equation_from_string(
eqstr, self._eqfactory, buildargs=True, ns=ns
)
eq.name = "eq"
# Register any new Parameters.
for par in self._eqfactory.newargs:
self._add_parameter(par)
# Register eq as an operator
self._eqfactory.register_operator("eq", eq)
self._eqfactory.wipeout(self._eq)
self._eq = eq
# Set the residual if we need to
if self.profile is not None and self._reseq is None:
self.set_residual_equation("chiv")
return
@deprecated(setequation_dep_msg)
def setEquation(self, eqstr, ns={}):
"""This function has been deprecated and will be removed in version
4.0.0.
Please use diffpy.srfit.fitbase.FitContribution.set_equation
instead.
"""
self.set_equation(eqstr, ns=ns)
return
[docs]
def get_equation(self):
"""Get the math expression string for the active profile
equation.
Returns
-------
str
The normalized math expression, or an empty string if the
profile equation has not been set yet.
"""
from diffpy.srfit.equation.visitors import get_expression
rv = ""
if self._eq is not None:
rv = get_expression(self._eq)
return rv
@deprecated(getequation_dep_msg)
def getEquation(self):
"""This function has been deprecated and will be removed in version
4.0.0.
Please use diffpy.srfit.fitbase.FitContribution.get_equation
instead.
"""
return self.get_equation()
[docs]
def set_residual_equation(self, eqstr):
"""Set the residual equation for the FitContribution.
Two residuals are preset for convenience, ``"chiv"`` and ``"resv"``.
``chiv`` is defined such that ``dot(chiv, chiv) = chi^2``.
``resv`` is defined such that ``dot(resv, resv) = Rw^2``.
You can call on these in your residual equation. Note that the quantity
that will be optimized is the summed square of the residual equation.
Keep that in mind when defining a new residual or using the built-in
ones.
Parameters
----------
eqstr : str
The string representation of the residual, or one of the
preset names ``"chiv"`` and ``"resv"``.
Raises
------
SrFitError
If the Profile or the profile equation is not yet defined.
ValueError
If eqstr depends on a Parameter that is not part of the
FitContribution.
"""
if self.profile is None:
raise SrFitError("Assign the Profile first")
if self._eq is None:
raise SrFitError("Assign the Equation first")
chivstr = "(eq - %s)/%s" % (self._yname, self._dyname)
resvstr = "(eq - %s)/sum(%s**2)**0.5" % (self._yname, self._yname)
# Get the equation string if it is not defined
if eqstr == "chiv":
eqstr = chivstr
elif eqstr == "resv":
eqstr = resvstr
reseq = get_equation_from_string(eqstr, self._eqfactory)
self._eqfactory.wipeout(self._reseq)
self._reseq = reseq
return
@deprecated(setresidualequation_dep_msg)
def setResidualEquation(self, eqstr):
"""This function has been deprecated and will be removed in version
4.0.0.
Please use
diffpy.srfit.fitbase.FitContribution.set_residual_equation
instead.
"""
self.set_residual_equation(eqstr)
return
[docs]
def get_residual_equation(self):
"""Get the math expression string for the active residual
equation.
Returns
-------
str
The normalized math formula, or an empty string if the
residual equation has not been configured yet.
"""
from diffpy.srfit.equation.visitors import get_expression
rv = ""
if self._reseq is not None:
rv = get_expression(self._reseq, eqskip="eq$")
return rv
@deprecated(getresidualequation_dep_msg)
def getResidualEquation(self):
"""This function has been deprecated and will be removed in version
4.0.0.
Please use
diffpy.srfit.fitbase.FitContribution.get_residual_equation
instead.
"""
return self.get_residual_equation()
[docs]
def residual(self):
"""Calculate the residual for this FitContribution.
When this method is called, it is assumed that all parameters have been
assigned their most current values by the FitRecipe. This will be the
case when being called as part of a FitRecipe refinement.
The residual is by default an array ``chiv``:
``chiv = (eq() - self.profile.y) / self.profile.dy``.
The value that is optimized is ``dot(chiv, chiv)``.
The residual equation can be changed with the
``set_residual_equation`` method.
Returns
-------
numpy.ndarray
The array of residual values.
"""
# Assign the calculated profile
self.profile.ycalc = self._eq()
# Note that equations only recompute when their inputs are modified, so
# the following will not recompute the equation.
return self._reseq()
[docs]
def evaluate(self):
"""Evaluate the contribution equation and update profile.ycalc.
The calculated signal is stored in ``profile.ycalc`` only if a
Profile has been assigned.
Returns
-------
numpy.ndarray
The calculated signal.
"""
yc = self._eq()
if self.profile is not None:
self.profile.ycalc = yc
return yc
def _validate(self):
"""Validate my state.
This performs profile validations. This performs
ProfileGenerator validations. This validates _eq. This validates
_reseq and residual.
Raises
------
SrFitError
If validation fails.
"""
self.profile._validate()
ParameterSet._validate(self)
# Try to get the value of eq.
from diffpy.srfit.equation.visitors import validate
try:
validate(self._eq)
except ValueError as e:
raise SrFitError(e)
if self._eq is None:
raise SrFitError("_eq is None")
val = None
try:
val = self._eq()
except TypeError as e:
raise SrFitError("_eq cannot be evaluated: %s" % e)
if val is None:
raise SrFitError("_eq evaluates to None")
# Try to get the value for residual
try:
validate(self._reseq)
except ValueError as e:
raise SrFitError(e)
try:
val = self.residual()
except TypeError:
raise SrFitError("residual cannot be evaluated")
if val is None:
raise SrFitError("residual evaluates to None")
return
# End of file