Source code for diffpy.srfit.fitbase.fitrecipe

#!/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.
#
##############################################################################
"""FitRecipe class.

FitRecipes organize FitContributions, variables, Restraints and
Constraints to create a recipe of the system you wish to optimize. From
the client's perspective, the FitRecipe is a residual calculator. The
residual method does the work of updating variable values, which get
propagated to the Parameters of the underlying FitContributions via the
variables and Constraints.  This class needs no special knowledge of the
type of FitContribution or data being used. Thus, it is suitable for
combining residual equations from various types of refinements into a
single residual.

Variables added to a FitRecipe can be tagged with string identifiers.
Variables can be later retrieved or manipulated by tag. The tag name
``__fixed`` is reserved.

See the examples in the documentation for how to create an optimization
problem using FitRecipe.
"""

__all__ = ["FitRecipe"]

from collections import OrderedDict
from pathlib import Path

import matplotlib.pyplot as plt
from bg_mpl_stylesheets.styles import all_styles
from numpy import array, concatenate, dot, sqrt

import diffpy.srfit.util.inpututils as utils
from diffpy.srfit.fitbase.fithook import PrintFitHook
from diffpy.srfit.fitbase.parameter import ParameterProxy
from diffpy.srfit.fitbase.recipeorganizer import RecipeOrganizer
from diffpy.srfit.interface import _fitrecipe_interface
from diffpy.srfit.util.tagmanager import TagManager
from diffpy.utils._deprecator import build_deprecation_message, deprecated

plt.style.use(all_styles["bg-style"])

base = "diffpy.srfit.fitbase.FitRecipe"
removal_version = "4.0.0"
addcontrib_dep_msg = build_deprecation_message(
    base, "addContribution", "add_contribution", removal_version
)

pushfithook_dep_msg = build_deprecation_message(
    base, "pushFitHook", "push_fit_hook", removal_version
)

popfithook_dep_msg = build_deprecation_message(
    base, "popFitHook", "pop_fit_hook", removal_version
)

getfithooks_dep_msg = build_deprecation_message(
    base, "getFitHooks", "get_fit_hooks", removal_version
)

clearfithooks_dep_msg = build_deprecation_message(
    base, "clearFitHooks", "clear_fit_hooks", removal_version
)

setweight_dep_msg = build_deprecation_message(
    base, "setWeight", "set_weight", removal_version
)

addparset_dep_msg = build_deprecation_message(
    base, "addParameterSet", "add_parameter_set", removal_version
)

removeParameterSet_dep_msg = build_deprecation_message(
    base, "removeParameterSet", "remove_parameter_set", removal_version
)

scalarResidual_dep_msg = build_deprecation_message(
    base, "scalarResidual", "scalar_residual", removal_version
)

addVar_dep_msg = build_deprecation_message(
    base, "addVar", "add_variable", removal_version
)

delVar_dep_msg = build_deprecation_message(
    base, "delVar", "delete_variable", removal_version
)

newVar_dep_msg = build_deprecation_message(
    base, "newVar", "create_new_variable", removal_version
)

isFree_dep_msg = build_deprecation_message(
    base, "isFree", "is_free", removal_version
)

getValues_dep_msg = build_deprecation_message(
    base, "getValues", "get_values", removal_version
)

getNames_dep_msg = build_deprecation_message(
    base, "getNames", "get_names", removal_version
)

getBounds_dep_msg = build_deprecation_message(
    base, "getBounds", "get_bounds_pairs", removal_version
)

getBounds2_dep_msg = build_deprecation_message(
    base, "getBounds2", "get_bounds_array", removal_version
)

boundsToRestraints_dep_msg = build_deprecation_message(
    base, "boundsToRestraints", "convert_bounds_to_restraints", removal_version
)

constrain_dep_msg = build_deprecation_message(
    base, "constrain", "add_constraint", removal_version
)

unconstrain_dep_msg = build_deprecation_message(
    base, "unconstrain", "remove_constraint", removal_version
)


[docs] class FitRecipe(_fitrecipe_interface, RecipeOrganizer): """Organize FitContributions, variables, restraints, and constraints into a refinable recipe. Attributes ---------- name : str A name for this FitRecipe. fithooks : list The list of FitHook instances that can pass information out of the system during a refinement. By default, this is populated by a PrintFitHook instance. _constraints : dict The dictionary of Constraints, indexed by the constrained Parameter. Constraints can be added using the `add_constraint` method. _oconstraints : list The ordered list of the constraints from this and all sub-components. _calculators : dict The managed dictionary of Calculators. _contributions : OrderedDict The managed OrderedDict of FitContributions. _parameters : OrderedDict The managed OrderedDict of parameters (in this case the parameters are varied). _parsets : dict The managed dictionary of ParameterSets. _eqfactory : diffpy.srfit.equation.builder.EquationFactory The diffpy.srfit.equation.builder.EquationFactory instance that is used to create constraints and restraints from strings. _restraintlist : list The list of restraints from this and all sub-components. _restraints : set The set of Restraints. Restraints can be added using the 'restrain' or 'confine' methods. _ready : bool The flag indicating if all attributes are ready for the calculation. _tagmanager : TagManager The TagManager instance for managing tags on Parameters. _weights : list The list of weighing factors for each FitContribution. The weights are multiplied by the residual of the FitContribution when determining the overall residual. _fixedtag : str ``__fixed``, used for tagging variables as fixed. Don't use this tag unless you want issues. Properties ---------- names : list The variable names (read only). See get_names. values : numpy.ndarray The variable values (read only). See get_values. fixednames : list The names of the fixed refinable variables (read only). fixedvalues : numpy.ndarray The values of the fixed refinable variables (read only). bounds : list of tuple The bounds on parameters (read only). See get_bounds_pairs. bounds2 : tuple of numpy.ndarray The bounds on parameters (read only). See get_bounds_array. """ fixednames = property( lambda self: [ v.name for v in self._parameters.values() if not (self.is_free(v) or self.is_constrained(v)) ], doc="names of the fixed refinable variables", ) fixedvalues = property( lambda self: array( [ v.value for v in self._parameters.values() if not (self.is_free(v) or self.is_constrained(v)) ] ), doc="values of the fixed refinable variables", ) bounds = property(lambda self: self.get_bounds_pairs()) bounds2 = property(lambda self: self.get_bounds_array()) def __init__(self, name="fit"): """Initialize the FitRecipe. Parameters ---------- name : str, optional The name for this FitRecipe. Default is "fit". """ RecipeOrganizer.__init__(self, name) self.fithooks = [] self.pushFitHook(PrintFitHook()) self._restraintlist = [] self._oconstraints = [] self._ready = False self._fixedtag = "__fixed" self._weights = [] self._tagmanager = TagManager() self._parsets = {} self._manage(self._parsets) self._contributions = OrderedDict() self._manage(self._contributions) self.plot_options = { "show_observed": True, "show_fit": True, "show_diff": True, "offset_scale": 1.0, "xmin": None, "xmax": None, "figsize": (8, 6), "data_style": "o", "fit_style": "-", "diff_style": "-", "data_color": None, "fit_color": None, "diff_color": None, "data_label": "Observed", "fit_label": "Calculated", "diff_label": "Difference", "xlabel": None, "ylabel": None, "title": None, "legend": True, "legend_loc": "best", "grid": False, "markersize": None, "linewidth": None, "alpha": 1.0, "show": True, } return
[docs] def push_fit_hook(self, fithook, index=None): """Add a FitHook to be called within the residual method. The hook is an object for reporting updates, or more fundamentally, passing information out of the system during a refinement. See the diffpy.srfit.fitbase.fithook.FitHook class for the required interface. Added FitHooks will be called sequentially during refinement. Parameters ---------- fithook : diffpy.srfit.fitbase.fithook.FitHook The FitHook instance to add to the sequence. index : int or None, optional The index for inserting fithook into the list of fit hooks. If this is None (default), the fithook is added to the end. """ if index is None: index = len(self.fithooks) self.fithooks.insert(index, fithook) # Make sure the added FitHook gets its reset method called. self._update_configuration() return
[docs] @deprecated(pushfithook_dep_msg) def pushFitHook(self, fithook, index=None): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.FitRecipe.push_fit_hook instead. """ self.push_fit_hook(fithook, index) return
[docs] def pop_fit_hook(self, fithook=None, index=-1): """Remove a FitHook by index or reference. Parameters ---------- fithook : diffpy.srfit.fitbase.fithook.FitHook or None, optional The FitHook instance to remove from the sequence. If this is None (default), default to index. index : int, optional The index of FitHook instance to remove (default -1). Raises ------ ValueError If fithook is not None, but is not present in the sequence. IndexError If the sequence is empty or index is out of range. """ if fithook is not None: self.fithooks.remove(fithook) return self.fithook.remove(index) return
[docs] @deprecated(popfithook_dep_msg) def popFitHook(self, fithook=None, index=-1): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.FitRecipe.pop_fit_hook instead. """ self.pop_fit_hook(fithook, index) return
[docs] def get_fit_hooks(self): """Get the sequence of FitHook instances. Returns ------- list The list of FitHook instances registered with this FitRecipe. """ return self.fithooks[:]
[docs] @deprecated(getfithooks_dep_msg) def getFitHooks(self): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.FitRecipe.get_fit_hooks instead.""" return self.get_fit_hooks()
[docs] def clear_fit_hooks(self): """Clear the FitHook sequence.""" del self.fithooks[:] return
[docs] @deprecated(clearfithooks_dep_msg) def clearFitHooks(self): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.FitRecipe.clear_fit_hooks instead.""" self.clear_fit_hooks() return
[docs] def add_contribution(self, con, weight=1.0): """Add a FitContribution to the FitRecipe. Parameters ---------- con : FitContribution The FitContribution to be stored. weight : float, optional The weight of the FitContribution. Default is 1.0. Raises ------ ValueError If the FitContribution has no name or if the FitContribution has the same name as some other managed object. """ self._add_object(con, self._contributions, True) self._weights.append(weight) return
[docs] @deprecated(addcontrib_dep_msg) def addContribution(self, con, weight=1.0): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.FitRecipe.add_contribution instead. """ self.add_contribution(con, weight) return
[docs] def set_weight(self, con, weight): """Set the weight of a FitContribution. Parameters ---------- con : FitContribution The FitContribution object whose weight is to be set. weight : float The weight value to assign to the specified FitContribution. Returns ------- None """ idx = list(self._contributions.values()).index(con) self._weights[idx] = weight return
[docs] @deprecated(setweight_dep_msg) def setWeight(self, con, weight): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.FitRecipe.set_weight instead.""" self.set_weight(con, weight) return
[docs] def add_parameter_set(self, parset): """Add a ParameterSet to the hierarchy. Parameters ---------- parset : ParameterSet The ParameterSet to be stored. Raises ------ ValueError If the ParameterSet has no name or if the ParameterSet has the same name as some other managed object. """ self._add_object(parset, self._parsets, True) return
[docs] @deprecated(addparset_dep_msg) def addParameterSet(self, parset): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.FitRecipe.add_parameter_set instead. """ self.add_parameter_set(parset) return
[docs] def remove_parameter_set(self, parset): """Remove a ParameterSet from the hierarchy. This method removes the specified ParameterSet object from the internal hierarchy of managed ParameterSets. If the provided ParameterSet is not currently managed by this object, a ValueError will be raised. Parameters ---------- parset : ParameterSet The ParameterSet instance to be removed from the hierarchy. Raises ------ ValueError If the provided ParameterSet is not managed by this object. """ self._remove_object(parset, self._parsets) return
[docs] @deprecated(removeParameterSet_dep_msg) def removeParameterSet(self, parset): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.FitRecipe.remove_parameter_set instead. """ self.remove_parameter_set(parset) return
[docs] def residual(self, p=[]): """Calculate the vector residual to be optimized. The residual is by default the weighted concatenation of each FitContribution's residual, plus the value of each restraint. The array returned, denoted ``chiv``, is such that ``dot(chiv, chiv) = chi^2 + restraints``. Parameters ---------- p : list or numpy.ndarray The list of current variable values, provided in the same order as the ``_parameters`` list. If ``p`` is an empty iterable (default), then it is assumed that the parameters have already been updated in some other way, and the explicit update within this function is skipped. Returns ------- chiv : numpy.ndarray The array of residuals to be optimized. The array is such that ``dot(chiv, chiv) = chi^2 + restraints``. """ # Prepare, if necessary self._prepare() for fithook in self.fithooks: fithook.precall(self) # Update the variable parameters. self._apply_values(p) # Update the constraints. These are ordered such that the list only # needs to be cycled once. for con in self._oconstraints: con.update() # Calculate the bare chiv chiv = concatenate( [ wi * ci.residual().flatten() for wi, ci in zip(self._weights, self._contributions.values()) ] ) # Calculate the point-average chi^2 w = dot(chiv, chiv) / len(chiv) # Now we must append the restraints penalties = [sqrt(res.penalty(w)) for res in self._restraintlist] chiv = concatenate([chiv, penalties]) for fithook in self.fithooks: fithook.postcall(self, chiv) return chiv
[docs] def scalar_residual(self, p=[]): """Calculate the scalar residual to be optimized. Parameters ---------- p : list or numpy.ndarray The list of current variable values, provided in the same order as the ``_parameters`` list. If ``p`` is an empty iterable (default), then it is assumed that the parameters have already been updated in some other way, and the explicit update within this function is skipped. Returns ------- float The scalar residual, ``dot(chiv, chiv)``, where ``chiv`` is the vector residual returned by `residual`. Notes ----- The residual is by default the weighted concatenation of each :class:`FitContribution` residual, plus the value of each restraint. The returned array, denoted ``chiv``, is such that ``dot(chiv, chiv) = chi^2 + restraints``. """ chiv = self.residual(p) return dot(chiv, chiv)
[docs] @deprecated(scalarResidual_dep_msg) def scalarResidual(self, p=[]): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.FitRecipe.scalar_residual instead. """ return self.scalar_residual(p)
def __call__(self, p=[]): """Compute the scalar residual, same as `scalar_residual`. Parameters ---------- p : list or numpy.ndarray, optional The list of current variable values, provided in the same order as the ``_parameters`` list. Default is an empty list. Returns ------- float The scalar residual, ``dot(chiv, chiv)``. """ return self.scalar_residual(p) def _prepare(self): """Prepare for the residual calculation, if necessary. This will prepare the data attributes to be used in the residual calculation. This updates the local restraints with those of the contributions. Raises ------ AttributeError If there are variables without a value. """ # Only prepare if the configuration has changed within the recipe # hierarchy. if self._ready: return # Inform the fit hooks that we're updating things for fithook in self.fithooks: fithook.reset(self) # Check Profiles self.__verify_profiles() # Check parameters self.__verify_parameters() # Update constraints and restraints. self.__collect_constraints_and_restraints() # We do this here so that the calculations that take place during the # validation use the most current values of the parameters. In most # cases, this will save us from recalculating them later. for con in self._oconstraints: con.update() # Validate! self._validate() self._ready = True return def __verify_profiles(self): """Verify that each FitContribution has a Profile.""" # Check for profile values for con in self._contributions.values(): if con.profile is None: m = "FitContribution '%s' does not have a Profile" % con.name raise AttributeError(m) if ( con.profile.x is None or con.profile.y is None or con.profile.dy is None ): m = "Profile for '%s' is missing data" % con.name raise AttributeError(m) return def __verify_parameters(self): """Verify that all Parameters have values.""" # Get all parameters with a value of None badpars = [] for par in self.iterate_over_parameters(): try: par.getValue() except ValueError: badpars.append(par) # Get the bad names badnames = [] for par in badpars: objlist = self._locate_managed_object(par) names = [obj.name for obj in objlist] badnames.append(".".join(names)) # Construct an error message, if necessary m = "" if len(badnames) == 1: m = "%s is not defined or needs an initial value" % badnames[0] elif len(badnames) > 0: s1 = ",".join(badnames[:-1]) s2 = badnames[-1] m = "%s and %s are not defined or need initial values" % (s1, s2) if m: raise AttributeError(m) return def __collect_constraints_and_restraints(self): """Collect the Constraints and Restraints from subobjects.""" from functools import cmp_to_key from itertools import chain rset = set(self._restraints) cdict = {} for org in chain(self._contributions.values(), self._parsets.values()): rset.update(org._get_restraints()) cdict.update(org._get_constraints()) cdict.update(self._constraints) # The order of the restraint list does not matter self._restraintlist = list(rset) # Reorder the constraints. Constraints are ordered such that a given # constraint is placed before its dependencies. self._oconstraints = list(cdict.values()) # Create a depth-1 map of the constraint dependencies depmap = {} for con in self._oconstraints: depmap[con] = set() # Now check the constraint's equation for constrained arguments for arg in con.eq.args: if arg in cdict: depmap[con].add(cdict[arg]) # Turn the dependency map into multi-level map. def _extendDeps(con): deps = set(depmap[con]) for dep in depmap[con]: deps.update(_extendDeps(dep)) return deps for con in depmap: depmap[con] = _extendDeps(con) # Now sort the constraints based on the dependency map. def cmp(x, y): # x == y if neither of them have dependencies if not depmap[x] and not depmap[y]: return 0 # x > y if y is a dependency of x # x > y if y has no dependencies if y in depmap[x] or not depmap[y]: return 1 # x < y if x is a dependency of y # x < y if x has no dependencies if x in depmap[y] or not depmap[x]: return -1 # If there are dependencies, but there is no relationship, the # constraints are equivalent return 0 self._oconstraints.sort(key=cmp_to_key(cmp)) return # Variable manipulation
[docs] def add_variable( self, par, value=None, name=None, fixed=False, tag=None, tags=[] ): """Add a variable to be refined. Parameters ---------- par : diffpy.srfit.fitbase.Parameter The Parameter that will be varied during a fit. value : float or None, optional The initial value for the variable. If this is None (default), then the current value of par will be used. name : str or None, optional The name for this variable. If name is None (default), then the name of the parameter will be used. fixed : bool, optional Fix the variable so that it does not vary (default False). tag : str or None, optional The tag for the variable. This can be used to retrieve, fix or free variables by tag (default None). Note that a variable is automatically tagged with its name and "all". tags : list of str, optional The list of tags (default []). Both tag and tags can be applied. Returns ------- ParameterProxy The ParameterProxy (variable) for the passed Parameter. Raises ------ ValueError If the name of the variable is already taken by another managed object. ValueError If par is constant. ValueError If par is constrained. """ name = name or par.name if par.const: raise ValueError("The parameter '%s' is constant" % par) if par.constrained: raise ValueError("The parameter '%s' is constrained" % par) var = ParameterProxy(name, par) if value is not None: var.set_value(value) self._add_parameter(var) if fixed: self.fix(var) # Tag with passed tags and by name self._tagmanager.tag(var, var.name) self._tagmanager.tag(var, "all") self._tagmanager.tag(var, *tags) if tag is not None: self._tagmanager.tag(var, tag) return var
[docs] @deprecated(addVar_dep_msg) def addVar( self, par, value=None, name=None, fixed=False, tag=None, tags=[] ): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.FitRecipe.add_variable instead. """ return self.add_variable(par, value, name, fixed, tag, tags)
[docs] def delete_variable(self, var): """Remove a variable. Note that constraints and restraints involving the variable are not modified. Parameters ---------- var : ParameterProxy A variable of the FitRecipe. Raises ------ ValueError If var is not part of the FitRecipe. """ self._remove_parameter(var) self._tagmanager.untag(var) return
[docs] @deprecated(delVar_dep_msg) def delVar(self, var): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.FitRecipe.delete_variable instead. """ self.delete_variable(var) return
def __delattr__(self, name): """Delete a variable if name refers to one, otherwise defer to the base class.""" if name in self._parameters: self.delete_variable(self._parameters[name]) return super(FitRecipe, self).__delattr__(name) return
[docs] def create_new_variable( self, name, value=None, fixed=False, tag=None, tags=[] ): """Create a new variable of the fit. This method lets new variables be created that are not tied to a Parameter. Orphan variables may cause a fit to fail, depending on the optimization routine, and therefore should only be created to be used in constraint or restraint equations. Parameters ---------- name : str The name of the variable. The variable will be able to be used by this name in restraint and constraint equations. value : float or None, optional The initial value for the variable. If this is None (default), then the variable will be given the value of the first non-None-valued Parameter constrained to it. If this fails, an error will be thrown when 'residual' is called. fixed : bool, optional Fix the variable so that it does not vary (default False). The variable will still be managed by the FitRecipe. tag : str or None, optional The tag for the variable. This can be used to fix and free variables by tag (default None). Note that a variable is automatically tagged with its name and "all". tags : list of str, optional The list of tags (default []). Both tag and tags can be applied. Returns ------- Parameter The new variable (Parameter instance). """ # This will fix the Parameter var = self._new_parameter(name, value) # We may explicitly free it if not fixed: self.free(var) # Tag with passed tags self._tagmanager.tag(var, *tags) if tag is not None: self._tagmanager.tag(var, tag) return var
[docs] @deprecated(newVar_dep_msg) def newVar(self, name, value=None, fixed=False, tag=None, tags=[]): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.FitRecipe.create_new_variable instead. """ return self.create_new_variable(name, value, fixed, tag, tags)
def _new_parameter(self, name, value, check=True): """Overloaded to tag variables. See RecipeOrganizer._new_parameter """ par = RecipeOrganizer._new_parameter(self, name, value, check) # tag this self._tagmanager.tag(par, par.name) self._tagmanager.tag(par, "all") self.fix(par.name) return par def __get_var_and_check(self, var): """Get the actual variable from var. Parameters ---------- var A variable of the FitRecipe, or the name of a variable. Returns ------- object The variable. Raises ------ ValueError If the variable is not part of the FitRecipe. """ if isinstance(var, str): var = self._parameters.get(var) if var not in self._parameters.values(): raise ValueError("Passed variable is not part of the FitRecipe") return var def __get_vars_from_args(self, *args, **kw): """Get a list of variables from passed arguments. This method accepts string or variable arguments. An argument of "all" selects all variables. Keyword arguments must be parameter names, followed by a value to assign to the fixed variable. This method is used by the fix and free methods. Raises ValueError if an unknown variable, name or tag is passed, or if a tag is passed in a keyword. """ # Process args. Each variable is tagged with its name, so this is easy. strargs = set([arg for arg in args if isinstance(arg, str)]) varargs = set(args) - strargs # Check that the tags are valid alltags = set(self._tagmanager.alltags()) badtags = strargs - alltags if badtags: names = ",".join(badtags) raise ValueError("Variables or tags cannot be found (%s)" % names) # Check that variables are valid allvars = set(self._parameters.values()) badvars = varargs - allvars if badvars: names = ",".join(v.name for v in badvars) raise ValueError("Variables cannot be found (%s)" % names) # Make sure that we only have parameters in kw kwnames = set(kw.keys()) allnames = set(self._parameters.keys()) badkw = kwnames - allnames if badkw: names = ",".join(badkw) raise ValueError("Tags cannot be passed as keywords (%s)" % names) # Now get all the objects referred to in the arguments. varargs |= self._tagmanager.union(*strargs) varargs |= self._tagmanager.union(*kw.keys()) return varargs
[docs] def fix(self, *args, **kw): """Fix one or more parameters by reference, name, or tag. This method marks specified parameters as fixed, meaning they will not be refined during the fitting process. By default, all parameters are free (not fixed). Parameters can be specified using their references, names, or tags. Additionally, keyword arguments can be used to assign specific values to the fixed parameters. Parameters ---------- *args : str or Parameter The positional arguments specifying the parameters to fix. These can be parameter objects, their names as strings, or tags. The special string "all" can be used to select all parameters. **kw : dict The keyword arguments where the keys are parameter names and the values are the values to assign to the corresponding fixed parameters. Raises ------ ValueError If an unknown parameter, name, or tag is passed, or if a tag is passed as a keyword argument. Examples -------- :: # Fix a parameter by reference recipe.fix(param1) # Fix a parameter by name recipe.fix("param2") # Fix all parameters recipe.fix("all") # Fix parameters by tag recipe.fix(tag="group1") # Fix a parameter and assign it a value recipe.fix(param3=10.0) """ # Check the inputs and get the variables from them varargs = self.__get_vars_from_args(*args, **kw) # Fix all of these for var in varargs: self._tagmanager.tag(var, self._fixedtag) # Set the kw values for name, val in kw.items(): self.get(name).value = val return
[docs] def free(self, *args, **kw): """Free one or more parameters by reference, name, or tag. This method marks specified parameters as free, allowing them to be refined during the fitting process. By default, variables are free unless they are constrained. Constrained variables cannot be freed. Parameters ---------- *args : str or Parameter The positional arguments specifying the parameters to free. These can be: - Parameter objects - Names of parameters (as strings) - Tags associated with parameters (as strings) - The string "all" to select all parameters. **kw : dict The keyword arguments specifying parameter names as keys and their values to assign after freeing. This is useful for setting the value of a parameter while marking it as free. Returns ------- None Raises ------ ValueError If an unknown parameter, name, or tag is passed, or if a tag is passed as a keyword argument. Notes ----- - Parameters that are already free will remain free. - Tags associated with fixed parameters will be removed when they are freed. - If keyword arguments are provided, the corresponding parameter values will be updated after freeing. """ # Check the inputs and get the variables from them varargs = self.__get_vars_from_args(*args, **kw) # Free all of these for var in varargs: if not var.constrained: self._tagmanager.untag(var, self._fixedtag) # Set the kw values for name, val in kw.items(): self.get(name).value = val return
[docs] def is_free(self, var): """Determine if a variable is free (not fixed) in the fit recipe. This method checks whether the specified variable does not have the fixed tag associated with it, indicating that it is free to vary during the fitting process. Parameters ---------- var : object The variable to check. This is typically an instance of a parameter or variable object used in the fit recipe. Returns ------- bool True if the variable is free (not fixed), False otherwise. """ return not self._tagmanager.hasTags(var, self._fixedtag)
[docs] @deprecated(isFree_dep_msg) def isFree(self, var): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.FitRecipe.is_free instead. """ return self.is_free(var)
[docs] def remove_constraint(self, *pars): """Unconstrain a Parameter. This removes any constraints on a Parameter. If the Parameter is also a variable of the recipe, it will be freed as well. Parameters ---------- *pars : str or Parameter The names of Parameters or Parameter objects to unconstrain. Raises ------ ValueError If the Parameter is not constrained. """ update = False for par in pars: if isinstance(par, str): name = par par = self.get(name) if par is None: raise ValueError("The parameter cannot be found") if par in self._constraints: self._constraints[par].remove_constraint() del self._constraints[par] update = True if par in self._parameters.values(): self._tagmanager.untag(par, self._fixedtag) if update: # Our configuration changed self._update_configuration() return
[docs] @deprecated(unconstrain_dep_msg) def unconstrain(self, *pars): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.FitRecipe.remove_constraint instead. """ self.remove_constraint(*pars) return
[docs] def add_constraint(self, par, con, ns={}): """Constrain a parameter to an equation. Note that only one constraint can exist on a Parameter at a time. This is overloaded to set the value of con if it represents a variable and its current value is None. A constrained variable will be set as fixed. Parameters ---------- par : Parameter The Parameter to constrain. con : str or Parameter The string representation of the constraint equation or a Parameter to constrain to. A constraint equation must consist of numpy operators and "known" Parameters. Parameters are known if they are in the ns argument, or if they are managed by this object. ns : dict, optional The dictionary of Parameters, indexed by name, that are used in the eqstr, but not part of this object (default {}). Raises ------ ValueError If ns uses a name that is already used for a variable. ValueError If eqstr depends on a Parameter that is not part of the FitRecipe and that is not defined in ns. ValueError If par is marked as constant. """ if isinstance(par, str): name = par par = self.get(name) if par is None: par = ns.get(name) if par is None: raise ValueError("The parameter '%s' cannot be found" % name) if con in self._parameters.keys(): con = self._parameters[con] if par.const: raise ValueError("The parameter '%s' is constant" % par) # This will pass the value of a constrained parameter to the initial # value of a parameter constraint. if con in self._parameters.values(): val = con.getValue() if val is None: val = par.getValue() con.set_value(val) if par in self._parameters.values(): self.fix(par) RecipeOrganizer.add_constraint(self, par, con, ns) return
[docs] @deprecated(constrain_dep_msg) def constrain(self, par, con, ns={}): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.FitRecipe.add_constraint instead. """ self.add_constraint(par, con, ns) return
[docs] def get_values(self): """Retrieve the current values of all free variables in the fit recipe. This method collects the values of all parameters that are marked as free (i.e., adjustable during the fitting process) and returns them as a NumPy array. Returns ------- values_array : numpy.ndarray The array containing the current values of all free variables in the fit recipe. """ values_array = array( [v.value for v in self._parameters.values() if self.is_free(v)] ) return values_array
[docs] @deprecated(getValues_dep_msg) def getValues(self): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.FitRecipe.get_values instead.""" return self.get_values()
[docs] def get_names(self): """Retrieve the names of all free variables in the fit recipe. This method iterates through the parameters in the fit recipe and returns a list of names for those variables that are marked as free. Returns ------- parameter_names : list of str The list containing the names of free variables. """ parameter_names = [ v.name for v in self._parameters.values() if self.is_free(v) ] return parameter_names
[docs] @deprecated(getNames_dep_msg) def getNames(self): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.FitRecipe.get_names instead.""" return self.get_names()
[docs] def get_bounds_pairs(self): """Get the bounds on variables in a list. Returns ------- bounds_pair_list : list of tuple of float The list of ``(lower, upper)`` bounds on the variables, in the same order as ``get_names`` and ``get_values``. """ return [v.bounds for v in self._parameters.values() if self.is_free(v)]
[docs] @deprecated(getBounds_dep_msg) def getBounds(self): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.FitRecipe.get_bounds_pairs instead. """ return self.get_bounds_pairs()
[docs] def get_bounds_array(self): """Get the bounds on variables in two numpy arrays. Returns ------- lower_bounds : numpy.ndarray The numpy array of lower bounds on the variables, in the same order as ``get_names`` and ``get_values``. upper_bounds : numpy.ndarray The numpy array of upper bounds on the variables, in the same order as ``get_names`` and ``get_values``. """ bounds = self.get_bounds_pairs() lower_bounds = array([b[0] for b in bounds]) upper_bounds = array([b[1] for b in bounds]) return lower_bounds, upper_bounds
[docs] @deprecated(getBounds2_dep_msg) def getBounds2(self): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.FitRecipe.get_bounds_array instead. """ return self.get_bounds_array()
[docs] def initialize_recipe_with_recipe(self, recipe_object): """Initialize a FitRecipe with another FitRecipe. This is used to initialize a FitRecipe with the contribution(s), parameters, constraints and restraints of another FitRecipe. If a duplicate contribution, parameter, constraint, or restraint is added to the FitRecipe you are initializing, the value from the added object will be used. Parameters ---------- recipe_object : FitRecipe The FitRecipe to initialize with. Raises ------ ValueError If the object passed is not a FitRecipe. """ if not isinstance(recipe_object, FitRecipe): raise ValueError( "The input recipe_object must be a FitRecipe, " f"but got {type(recipe_object)}." ) for contrib_object in recipe_object._contributions.values(): if contrib_object not in self._contributions.values(): self.add_contribution(contrib_object) for param_name, param_object in recipe_object._parameters.items(): if param_name not in self._parameters: self._parameters.update({param_name: param_object}) for ( parameter_object, constraint_object, ) in recipe_object._constraints.items(): if parameter_object not in self._constraints: self._constraints.update({parameter_object: constraint_object}) for restraint in recipe_object._restraints: if restraint not in self._restraints: self._restraints.add(restraint)
def _pretty_print_results_dict(self, params_dict): """Pretty print a dictionary of parameter names and values.""" sorted_params = sorted(params_dict.items()) width = max(len(name) for name, _ in sorted_params) for name, value in sorted_params: if isinstance(value, float): value_str = f"{value:.6g}" else: value_str = str(value) print(f" {name:<{width}} = {value_str}") def _set_parameters_from_dict(self, params_dict): """Set the parameters of the FitRecipe from a dictionary of parameter names and values.""" for param_name, param_value in params_dict.items(): if param_name in self._parameters: self._parameters[param_name].set_value(param_value) else: print( f"Warning: Parameter '{param_name}' from results " "not found in FitRecipe and will be ignored." )
[docs] def initialize_recipe_with_results(self, results, verbose=True): """Initialize a FitRecipe with a FitResults object or a results file. Note that at least one FitContribution must already exist in the FitRecipe. Parameters ---------- results : FitResults, pathlib.Path, or str The FitResults object or path to results file to initialize with. verbose : bool, optional If True, print warnings for any parameters in the results that are not in the FitRecipe. Default is True. Raises ------ ValueError If the input results is not a FitResults object or a path to a results file. """ if hasattr(results, "get_results_dictionary"): params_dict = results.get_results_dictionary() metrics_in_dict = [ "Residual", "Contributions", "Restraints", "Chi2", "Reduced Chi2", "Rw", ] for metric in metrics_in_dict: params_dict.pop(metric, None) elif isinstance(results, (str, Path)): params_dict = utils.get_dict_from_results_file(results) else: raise ValueError( "The input results must be a FitResults object or a path to a " f"results file, but got {type(results)}." ) self._set_parameters_from_dict(params_dict) if verbose: print() print("Parameters found in Results:") print("=" * 30) self._pretty_print_results_dict(params_dict) print() print("Parameters set in FitRecipe:") print("=" * 30) set_parameters_dict = { param.name: param.getValue() for param in self._parameters.values() } self._pretty_print_results_dict(set_parameters_dict)
[docs] def set_plot_defaults(self, **kwargs): """Set default plotting options for all future plots. Any keyword argument accepted by plot_recipe() can be set here. Parameters ---------- show_observed : bool, optional The observed data is plotted if True. Default is True. show_fit : bool, optional The fit to the data is plotted if True. Default is True. show_diff : bool, optional The difference curve (observed - calculated) is plotted if True. Default is True. offset_scale : float, optional The scaling factor for the difference curve offset. The difference curve is offset below the data by (min_y - 0.1*range) * offset_scale. Default is 1.0. xmin : float or None, optional The minimum x value to plot. If None, uses the minimum x value of the data. Default is None. xmax : float or None, optional The maximum x value to plot. If None, uses the maximum x value of the data. Default is None. figsize : tuple, optional The figure size as (width, height). Default is (8, 6). data_style : str, optional The matplotlib line/marker style for data points. Default is "o". fit_style : str, optional The matplotlib line/marker style for the calculated fit. Default is "-". diff_style : str, optional The matplotlib line/marker style for the difference curve. Default is "-". data_color : str or None, optional The color for data plot. If None, uses default matplotlib colors. fit_color : str or None, optional The color for the fit plot. If None, uses default matplotlib colors. diff_color : str or None, optional The color for the difference plot. If None, uses default matplotlib colors. data_label : str, optional The legend label for observed data. Default is "Observed". fit_label : str, optional The legend label for the calculated fit. Default is "Calculated". diff_label : str, optional The legend label for the difference curve. Default is "Difference". xlabel : str, optional The label for the x-axis. ylabel : str, optional The label for the y-axis. title : str or None, optional The plot title. If None (default), each figure created by `plot_recipe` is titled with the name of the contribution it shows. A title is not added to a user-supplied axes. legend : bool, optional The legend is shown if True. Default is True. legend_loc : str, optional The legend location. Default is "best". grid : bool, optional The grid is shown if True. Default is False. markersize : float, optional The size of data point markers. linewidth : float, optional The width of fit and difference lines. alpha : float, optional The transparency of all plot elements (0=transparent, 1=opaque). Default is 1.0. show : bool, optional The plot is displayed using `plt.show()` if True. Default is True. Notes ----- The `data_label`, `fit_label`, `diff_label` and `title` options accept a ``{contribution}`` placeholder that is replaced by the name of the FitContribution being plotted, e.g. ``fit_label="{contribution} calculated"``. When several contributions are drawn on a shared axes, labels without the placeholder are prefixed with the contribution name so the legend entries stay distinguishable. Examples -------- >>> recipe.set_plot_defaults( xlabel='r (Å)', ylabel='G(r) (Å⁻²)', data_color='black', fit_color='red' ) """ for key in kwargs: if key not in self.plot_options: print( f"Warning: '{key}' is not a valid " "plot_recipe option and will be ignored." ) self.plot_options.update(kwargs)
def _format_plot_label(self, label, contribution_name, add_prefix): """Insert the contribution name into a legend label.""" if "{contribution}" in label: return label.format(contribution=contribution_name) if add_prefix: return f"{contribution_name}: {label}" return label def _set_axes_labels_from_metadata(self, meta, plot_params): """Set axes labels based on filename suffix in profile metadata if not already set.""" if isinstance(meta, dict): filename = meta.get("filename") if filename: suffix = filename.rsplit(".", 1)[-1].lower() if "gr" in suffix: if plot_params.get("xlabel") is None: plot_params["xlabel"] = r"r ($\mathrm{\AA}$)" if plot_params.get("ylabel") is None: plot_params["ylabel"] = r"G ($\mathrm{\AA}^{-2}$)" return
[docs] def plot_recipe(self, ax=None, return_fig=False, **kwargs): """Plot the observed, fit, and difference curves for each contribution of the fit recipe. If the recipe has multiple contributions, a separate plot is created for each contribution. Parameters ---------- ax : matplotlib.axes.Axes or None, optional The axes object to plot on. If None, creates a new figure. Default is None. return_fig : bool, optional The figure and axes objects are returned if True. Default is False. **kwargs : dict Any plotting option can be passed to override the defaults in `FitRecipe().plot_options`. See the `FitRecipe().set_plot_defaults()` method for available keyword arguments. Returns ------- fig, axes : tuple The figure and axes objects, returned only if ``return_fig=True``. If the recipe has a single contribution, a single ``mpl.figure.Figure`` and ``mpl.axes.Axes`` are returned. If it has multiple contributions, a list of figures and a list of axes (one per contribution) are returned instead. Examples -------- Plot with default settings: >>> recipe.plot_recipe() Override defaults for one plot: >>> recipe.plot_recipe(show_diff=False, title='My Custom Title') Set defaults once, use everywhere: >>> recipe.set_plot_defaults(xlabel='r (Å)', ylabel='G(r)') >>> recipe.plot_recipe() # Uses xlabel and ylabel >>> recipe.plot_recipe() # Still uses them Override a default for one plot: >>> recipe.set_plot_defaults(figsize=(10, 7)) >>> recipe.plot_recipe() # Uses (10, 7) >>> recipe.plot_recipe(figsize=(12, 8)) # Temporarily uses (12, 8) >>> recipe.plot_recipe() # Back to (10, 7) Notes ----- The default values are taken from recipe.plot_options. You can modify these defaults in three ways: 1. Using set_plot_defaults(): recipe.set_plot_defaults(xlabel='r (Å)') 2. Direct attribute access: recipe.plot_options['xlabel'] = 'r (Å)' 3. Using update(): recipe.plot_options.update({'xlabel': 'r (Å)', 'ylabel': 'G(r)'}) """ plot_params = self.plot_options.copy() plot_params.update(kwargs) if not any( [ plot_params["show_observed"], plot_params["show_fit"], plot_params["show_diff"], ] ): raise ValueError( "At least one of show_observed, show_fit, " "or show_diff must be True" ) if not self._contributions: raise ValueError( "No contributions found in recipe. " "Add contributions before plotting." ) figures = [] axes_list = [] shared_axes = ax is not None and len(self._contributions) > 1 for name, contrib in self._contributions.items(): profile = contrib.profile x = profile.x yobs = profile.y ycalc = profile.ycalc show_fit = plot_params["show_fit"] show_diff = plot_params["show_diff"] if ycalc is None: if show_fit or show_diff: print( f"Contribution '{name}' has no calculated values " "(ycalc is None). " "Only observed data will be plotted." ) show_fit = False show_diff = False else: diff = yobs - ycalc y_min = min(yobs.min(), ycalc.min()) y_max = max(yobs.max(), ycalc.max()) y_range = y_max - y_min base_offset = y_min - 0.1 * y_range offset = base_offset * plot_params["offset_scale"] if ax is None: fig = plt.figure(figsize=plot_params["figsize"]) current_ax = fig.add_subplot(111) else: current_ax = ax fig = current_ax.figure if plot_params["show_observed"]: current_ax.plot( x, yobs, plot_params["data_style"], label=self._format_plot_label( plot_params["data_label"], name, shared_axes ), color=plot_params["data_color"], markersize=plot_params["markersize"], alpha=plot_params["alpha"], ) if show_fit: current_ax.plot( x, ycalc, plot_params["fit_style"], label=self._format_plot_label( plot_params["fit_label"], name, shared_axes ), color=plot_params["fit_color"], linewidth=plot_params["linewidth"], alpha=plot_params["alpha"], ) if show_diff: current_ax.plot( x, diff + offset, plot_params["diff_style"], label=self._format_plot_label( plot_params["diff_label"], name, shared_axes ), color=plot_params["diff_color"], linewidth=plot_params["linewidth"], alpha=plot_params["alpha"], ) current_ax.axhline( offset, color="black", ) meta = getattr(profile, "meta", None) if meta: self._set_axes_labels_from_metadata(meta, plot_params) if plot_params["xlabel"] is not None: current_ax.set_xlabel(plot_params["xlabel"]) if plot_params["ylabel"] is not None: current_ax.set_ylabel(plot_params["ylabel"]) if plot_params["title"] is not None: current_ax.set_title( self._format_plot_label(plot_params["title"], name, False) ) elif ax is None: current_ax.set_title(name) if plot_params["legend"]: current_ax.legend(loc=plot_params["legend_loc"], frameon=True) if plot_params["grid"]: current_ax.grid(True) if ( plot_params["xmin"] is not None or plot_params["xmax"] is not None ): current_ax.set_xlim( left=plot_params["xmin"], right=plot_params["xmax"] ) fig.tight_layout() figures.append(fig) axes_list.append(current_ax) if plot_params["show"] and ax is None: plt.show() if return_fig: if len(figures) == 1: return figures[0], axes_list[0] else: return figures, axes_list
[docs] def convert_bounds_to_restraints(self, sig=1, scaled=False): """Turn all bounded parameters into restraints. The bounds become limits on the restraint. Parameters ---------- sig : float or iterable of float, optional The number of standard deviations associated with each bound. Smaller values produce stronger restraints. If a scalar is given, the same value is applied to all parameters. If an iterable is provided, it must match the number of parameters. Default is 1. scaled : bool, optional If True, scale each restraint by the magnitude of the corresponding parameter, consistent with the behavior of :meth:`restrain`. Default is False. """ pars = self._parameters.values() if not hasattr(sig, "__iter__"): sig = [sig] * len(pars) for par, x in zip(pars, sig): self.add_soft_bounds( par, par.bounds[0], par.bounds[1], sig=x, scaled=scaled ) return
[docs] @deprecated(boundsToRestraints_dep_msg) def boundsToRestraints(self, sig=1, scaled=False): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.FitRecipe.convert_bounds_to_restraints instead. """ self.convert_bounds_to_restraints(sig, scaled) return
def _apply_values(self, p): """Apply variable values to the variables.""" if len(p) == 0: return vargen = (v for v in self._parameters.values() if self.is_free(v)) for var, pval in zip(vargen, p): var.set_value(pval) return def _update_configuration(self): """Notify RecipeContainers in hierarchy of configuration change.""" self._ready = False return
# End of file