Source code for diffpy.srfit.fitbase.recipeorganizer

#!/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.
#
##############################################################################
"""Base classes and tools for constructing a FitRecipe.

RecipeContainer is the base class for organizing Parameters, and other
RecipeContainers.  RecipeOrganizer is an extended RecipeContainer that
incorporates equation building, constraints and Restraints.
get_equation_from_string creates an Equation instance from a string.
"""

__all__ = ["RecipeContainer", "RecipeOrganizer", "get_equation_from_string"]

import re
from collections import OrderedDict
from functools import partial
from itertools import chain, groupby

from numpy import inf

from diffpy.srfit.equation import Equation
from diffpy.srfit.equation.builder import EquationFactory
from diffpy.srfit.fitbase.configurable import Configurable
from diffpy.srfit.fitbase.constraint import Constraint
from diffpy.srfit.fitbase.parameter import Parameter
from diffpy.srfit.fitbase.restraint import Restraint
from diffpy.srfit.fitbase.validatable import Validatable
from diffpy.srfit.interface import _recipeorganizer_interface
from diffpy.srfit.util import _DASHEDLINE
from diffpy.srfit.util import sortKeyForNumericString as numstr
from diffpy.srfit.util.nameutils import validateName
from diffpy.srfit.util.observable import Observable
from diffpy.utils._deprecator import build_deprecation_message, deprecated

recipecontainer_base = "diffpy.srfit.fitbase.recipeorganizer.RecipeContainer"
removal_version = "4.0.0"

getValues_deprecation_msg = build_deprecation_message(
    recipecontainer_base,
    "getValues",
    "get_values",
    removal_version,
)

getNames_deprecation_msg = build_deprecation_message(
    recipecontainer_base,
    "getNames",
    "get_names",
    removal_version,
)

iterPars_deprecation_msg = build_deprecation_message(
    recipecontainer_base,
    "iterPars",
    "iterate_over_parameters",
    removal_version,
)

recipeorganizer_base = "diffpy.srfit.fitbase.recipeorganizer.RecipeOrganizer"

registerCalculator_deprecation_msg = build_deprecation_message(
    recipeorganizer_base,
    "registerCalculator",
    "register_calculator",
    removal_version,
)

registerFunction_deprecation_msg = build_deprecation_message(
    recipeorganizer_base,
    "registerFunction",
    "register_function",
    removal_version,
)

registerStringFunction_deprecation_msg = build_deprecation_message(
    recipeorganizer_base,
    "registerStringFunction",
    "register_string_function",
    removal_version,
)

evaluateEquation_deprecation_msg = build_deprecation_message(
    recipeorganizer_base,
    "evaluateEquation",
    "evaluate_equation",
    removal_version,
)

isConstrained_deprecation_msg = build_deprecation_message(
    recipeorganizer_base,
    "isConstrained",
    "is_constrained",
    removal_version,
)

getConstrainedPars_deprecation_msg = build_deprecation_message(
    recipeorganizer_base,
    "getConstrainedPars",
    "get_constrained_parmeters",
    removal_version,
)

clearConstraints_deprecation_msg = build_deprecation_message(
    recipeorganizer_base,
    "clearConstraints",
    "clear_all_constraints",
    removal_version,
)

addRestraint_deprecation_msg = build_deprecation_message(
    recipeorganizer_base,
    "addRestraint",
    "register_soft_bounds",
    removal_version,
)

constrain_deprecation_msg = build_deprecation_message(
    recipeorganizer_base,
    "constrain",
    "add_constraint",
    removal_version,
)

unconstrain_deprecation_msg = build_deprecation_message(
    recipeorganizer_base,
    "unconstrain",
    "remove_constraint",
    removal_version,
)

restrain_deprecation_msg = build_deprecation_message(
    recipeorganizer_base,
    "restrain",
    "add_soft_bounds",
    removal_version,
)

unrestrain_deprecation_msg = build_deprecation_message(
    recipeorganizer_base,
    "unrestrain",
    "remove_soft_bounds",
    removal_version,
)

clearRestraints_deprecation_msg = build_deprecation_message(
    recipeorganizer_base,
    "clearRestraints",
    "clear_all_soft_bounds",
    removal_version,
)

equationFromString_deprecation_msg = build_deprecation_message(
    recipeorganizer_base,
    "equationFromString",
    "get_equation_from_string",
    removal_version,
)


[docs] class RecipeContainer(Observable, Configurable, Validatable): """Base class for organizing pieces of a FitRecipe. RecipeContainers are hierarchical organizations of Parameters and other RecipeContainers. This class provides attribute-access to these contained objects. Parameters and other RecipeContainers can be found within the hierarchy with the `_locate_managed_object` method. A RecipeContainer can manage dictionaries for that store various objects. These dictionaries can be added to the RecipeContainer using the `_manage` method. RecipeContainer methods that add, remove or retrieve objects will work with any managed dictionary. This makes it easy to add new types of objects to be contained by a RecipeContainer. By default, the RecipeContainer is configured to manage an OrderedDict of Parameter objects. RecipeContainer is an Observable, and observes its managed objects and Parameters. This allows hierarchical calculation elements, such as ProfileGenerator, to detect changes in Parameters and Restraints on which it may depend. Attributes ---------- name A name for this RecipeContainer. Names should be unique within a RecipeContainer and should be valid attribute names. _parameters A managed OrderedDict of contained Parameters. __managed A list of managed dictionaries. This is used for attribute access, addition and removal. _configobjs A set of configurable objects that must know of configuration changes within this object. Properties ---------- names Variable names (read only). See get_names. values Variable values (read only). See get_values. """ names = property(lambda self: self.get_names()) values = property(lambda self: self.get_values()) def __init__(self, name): Observable.__init__(self) Configurable.__init__(self) validateName(name) self.name = name self._parameters = OrderedDict() self.__managed = [] self._manage(self._parameters) return def _manage(self, d): """Manage a dictionary of objects. This adds the dictionary to the __managed list. Dictionaries in __managed are used for attribute access, addition, and removal. """ self.__managed.append(d) return def _iter_managed(self): """Get iterator over managed objects.""" return chain(*(d.values() for d in self.__managed))
[docs] def iterate_over_parameters( self, pattern="", recurse=True, fullnames=False ): """Iterate over the Parameters contained in this object. Parameters ---------- pattern : str, optional The regular expression pattern to match parameter names against. Only parameters with names matching this pattern will be returned. Default is an empty string, which matches all parameter names. recurse : bool, optional The flag indicating whether to recurse into managed objects when iterating over parameters. If True (default), the method will also iterate over parameters in managed sub-objects. If False, only top-level parameters will be iterated over. fullnames : bool, optional The flag indicating whether to match against hierarchical dotted names relative to this object. If False (default), match only leaf parameter names. Yields ------ Parameter The next Parameter whose name matches `pattern`. Examples -------- .. for param in recipe.iterate_over_parameters(pattern="scale_"): # print the name and value of parameters containing "scale_" print(f"{param.name}={param.value}") """ regexp = re.compile(pattern) if not fullnames: yield from self._iterpars_leafnames(regexp, pattern, recurse) else: yield from self._iterpars_fullnames( regexp, recurse=recurse, prefix="" )
def _iter_local_parameters(self, regexp, prefix=""): """Iterate over local Parameters with matching names.""" for parameter in list(self._parameters.values()): name = f"{prefix}{parameter.name}" if regexp.search(name): yield parameter def _iter_managed_parameter_containers(self): """Iterate over managed objects that can iterate over Parameters.""" managed = self.__managed[:] managed.remove(self._parameters) for managed_dict in managed: for obj in managed_dict.values(): if hasattr(obj, "iterate_over_parameters"): yield obj def _iterpars_leafnames(self, regexp, pattern, recurse=True): """Iterate over Parameters matched by leaf parameter names.""" yield from self._iter_local_parameters(regexp) if recurse: for obj in self._iter_managed_parameter_containers(): yield from obj.iterate_over_parameters( pattern=pattern, recurse=True, ) def _iter_child_fullname_parameters(self, obj, regexp, prefix): """Iterate over one child's Parameters with hierarchical names.""" if hasattr(obj, "_iterpars_fullnames"): childprefix = f"{prefix}{obj.name}." yield from obj._iterpars_fullnames( regexp, recurse=True, prefix=childprefix, ) else: yield from obj.iterate_over_parameters( pattern=regexp.pattern, recurse=True, ) def _iterpars_fullnames(self, regexp, recurse=True, prefix=""): """Iterate over Parameters matched by hierarchical dotted names.""" yield from self._iter_local_parameters(regexp, prefix=prefix) if recurse: for obj in self._iter_managed_parameter_containers(): yield from self._iter_child_fullname_parameters( obj, regexp, prefix, )
[docs] @deprecated(iterPars_deprecation_msg) def iterPars(self, pattern="", recurse=True): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.recipeorganizer.RecipeContainer.iterate_over_parameters instead. """ return self.iterate_over_parameters(pattern=pattern, recurse=recurse)
def __iter__(self): """Iterate over top-level parameters. Returns ------- iterator The iterator over the top-level Parameters. """ return iter(self._parameters.values()) def __len__(self): """Get number of top-level parameters. Returns ------- int The number of top-level Parameters. """ return len(self._parameters) def __getitem__(self, idx): """Get top-level parameters by index. Parameters ---------- idx : int or slice The index, or slice, of the top-level Parameters to get. Returns ------- Parameter or list of Parameter The Parameter, or list of Parameters, at `idx`. """ # need to wrap this in a list for python 3 compatibility. return list(self._parameters.values())[idx] def __getattr__(self, name): """Give access to the contained objects as attributes. Parameters ---------- name : str The name of the managed object to retrieve. Returns ------- object The managed object registered under `name`. Raises ------ AttributeError If no managed object is registered under `name`. """ arg = self.get(name) if arg is None: raise AttributeError(name) return arg # Ensure there is no __dir__ override in the base class. assert ( getattr(Observable, "__dir__", None) is getattr(Configurable, "__dir__", None) is getattr(Validatable, "__dir__", None) is getattr(object, "__dir__", None) ) def __dir__(self): """Return sorted list of attributes for this object. Returns ------- list of str The sorted list of attribute names, including managed objects. """ rv = set(dir(type(self))) rv.update(self.__dict__) # self.get fetches looks up for items in all managed dictionaries. # Add keys from each dictionary in self.__managed. rv.update(*self.__managed) rv = sorted(rv) return rv # Needed by __setattr__ _parameters = OrderedDict() __managed = [] def __setattr__(self, name, value): """Set an attribute, routing Parameter names to Parameter values. If `name` matches a managed Parameter, the Parameter's value is set rather than replacing the Parameter itself. Otherwise this behaves like normal attribute assignment, except that a managed non-Parameter object of that name may not be overwritten. Parameters ---------- name : str The name of the attribute to set. value The value to assign. If `name` refers to a managed Parameter, this may be a plain value or a Parameter, whose value will be copied. Raises ------ AttributeError If `name` refers to a managed, non-Parameter object. """ if name in self._parameters: parameter = self._parameters[name] if isinstance(value, Parameter): parameter.value = value.value else: parameter.value = value return m = self.get(name) if m is not None: raise AttributeError("Cannot set '%s'" % name) super(RecipeContainer, self).__setattr__(name, value) return def __delattr__(self, name): """Delete parameters with ``del``. This does not allow deletion of non-parameters, as this may require configuration changes that are not yet handled in a general way. Parameters ---------- name : str The name of the Parameter to delete. Raises ------ AttributeError If `name` refers to a managed, non-Parameter object. """ if name in self._parameters: self._remove_parameter(self._parameters[name]) return m = self.get(name) if m is not None: raise AttributeError("Cannot delete '%s'" % name) super(RecipeContainer, self).__delattr__(name) return
[docs] def get(self, name, default=None): """Get a managed object. Parameters ---------- name : str The name of the managed object to retrieve. default : optional The value to return if no managed object is found under `name` (default None). Returns ------- object The managed object registered under `name`, or `default` if no such object exists. """ for d in self.__managed: arg = d.get(name) if arg is not None: return arg return default
[docs] def get_names(self): """Get the names of managed parameters. Returns ------- list of str The names of the managed Parameters. """ return [p.name for p in self._parameters.values()]
[docs] @deprecated(getNames_deprecation_msg) def getNames(self): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.recipeorganizer.RecipeContainer.get_names instead. """ return self.get_names()
[docs] def get_values(self): """Get the values of managed parameters. Returns ------- list The values of the managed Parameters. """ return [p.value for p in self._parameters.values()]
[docs] @deprecated(getValues_deprecation_msg) def getValues(self): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.recipeorganizer.RecipeContainer.get_values instead. """ return self.get_values()
def _add_object(self, obj, d, check=True): """Add an object to a managed dictionary. Parameters ---------- obj The object to be stored. d The managed dictionary to store the object in. check If True (default), a ValueError is raised if an object of the given name already exists. Raises ------ ValueError If the object has no name. ValueError If the object has the same name as some other managed object. """ # Check name if not obj.name: message = "%s has no name" % obj.__class__.__name__ raise ValueError(message) # Check for extant object in d with same name oldobj = d.get(obj.name) if check and oldobj is not None: message = "%s with name '%s' already exists" % ( obj.__class__.__name__, obj.name, ) raise ValueError(message) # Check for object with same name in other dictionary. if oldobj is None and self.get(obj.name) is not None: message = "Non-%s with name '%s' already exists" % ( obj.__class__.__name__, obj.name, ) raise ValueError(message) # Detach the old object, if there is one if oldobj is not None: oldobj.removeObserver(self._flush) # Add the object d[obj.name] = obj # Observe the object obj.addObserver(self._flush) # Store this as a configurable object self._store_configurable(obj) return def _remove_object(self, obj, d): """Remove an object from a managed dictionary. Raises ------ ValueError If `obj` is not part of the dictionary. """ if obj not in d.values(): m = "'%s' is not part of the %s" % (obj, self.__class__.__name__) raise ValueError(m) del d[obj.name] obj.removeObserver(self._flush) return def _locate_managed_object(self, obj): """Find the location a managed object within the hierarchy. Parameters ---------- obj The object to find. Returns ------- list The list of objects. The first member of the list is this object, and each subsequent member is a sub-object of the previous one. The last entry in the list is `obj`. If `obj` cannot be found, the list is empty. """ loc = [self] # This handles the case that an object is asked to locate itself. if obj is self: return loc for m in self._iter_managed(): # Check locally for the object if m is obj: loc.append(obj) return loc # Check within managed objects if hasattr(m, "_locate_managed_object"): subloc = m._locate_managed_object(obj) if subloc: return loc + subloc return [] def _flush(self, other): """Invalidate cached state. This will force any observer to invalidate its state. By default this does nothing. """ self.notify(other) return def _validate(self): """Validate my state. This validates that contained Parameters and managed objects are valid. Raises ------ AttributeError If validation fails. """ iterable = chain(self.__iter__(), self._iter_managed()) self._validate_others(iterable) return
# End class RecipeContainer
[docs] class RecipeOrganizer(_recipeorganizer_interface, RecipeContainer): """Extended base class for organizing pieces of a FitRecipe. This class extends RecipeContainer by organizing constraints and Restraints, as well as Equations that can be used in Constraint and Restraint equations. These constraints and Restraints can be placed at any level and a flattened list of them can be retrieved with the `_get_constraints` and `_get_restraints` methods. Attributes ---------- name A name for this organizer. Names should be unique within a RecipeOrganizer and should be valid attribute names. _calculators A managed dictionary of Calculators, indexed by name. _parameters A managed OrderedDict of contained Parameters. _constraints A dictionary of Constraints, indexed by the constrained Parameter. Constraints can be added using the 'constrain' method. _restraints A set of Restraints. Restraints can be added using the 'restrain' method. _eqfactory A diffpy.srfit.equation.builder.EquationFactory instance that is used create Equations from string. Properties ---------- names Variable names (read only). See get_names. values Variable values (read only). See get_values. Raises ------ ValueError If the name is not a valid attribute identifier. """ def __init__(self, name): RecipeContainer.__init__(self, name) self._restraints = set() self._constraints = {} self._eqfactory = EquationFactory() self._calculators = {} self._manage(self._calculators) return # Parameter management def _new_parameter(self, name, value, check=True): """Add a new Parameter to the container. This creates a new Parameter and adds it to the container using the `_add_parameter` method. Returns ------- Parameter The newly created Parameter. """ p = Parameter(name, value) self._add_parameter(p, check) return p def _add_parameter(self, parameter, check=True): """Store a Parameter. Parameters added in this way are registered with the _eqfactory. Parameters ---------- parameter The Parameter to be stored. check If True (default), a ValueError is raised if a Parameter of the specified name has already been inserted. Raises ------ ValueError If the Parameter has no name. ValueError If the Parameter has the same name as a contained RecipeContainer. """ # Store the Parameter RecipeContainer._add_object(self, parameter, self._parameters, check) # Register the Parameter self._eqfactory.registerArgument(parameter.name, parameter) return def _remove_parameter(self, parameter): """Remove a parameter. This de-registers the Parameter with the `_eqfactory`. The Parameter will remain part of built equations. Note that constraints and restraints involving the Parameter are not modified. Raises ------ ValueError If `parameter` is not part of the RecipeOrganizer. """ self._remove_object(parameter, self._parameters) self._eqfactory.deRegisterBuilder(parameter.name) return
[docs] def register_calculator(self, calculator, argnames=None): """Register a Calculator so it can be used within equation strings. A Calculator is an elaborate function that can organize Parameters. This creates a function with this class that can be used within string equations. The resulting equation can be used in a string with arguments like a function or without, in which case the values of the Parameters created from argnames will be be used to compute the value. Parameters ---------- calculator : Calculator object The Calculator to register. argnames : list or None, optional The names of the arguments to `calculator` (list or None). If this is None, then the argument names will be extracted from the function. Returns ------- Equation The callable Equation object wrapping `calculator`. """ self._eqfactory.registerOperator(calculator.name, calculator) self._add_object(calculator, self._calculators) # Register arguments of the calculator if argnames is None: fncode = calculator.__call__.__func__.__code__ argnames = list(fncode.co_varnames) argnames = argnames[1 : fncode.co_argcount] for pname in argnames: if pname not in self._eqfactory.builders: parameter = self._new_parameter(pname, 0) else: parameter = self.get(pname) calculator.addLiteral(parameter) # Now return an equation object eq = self._eqfactory.makeEquation(calculator.name) return eq
[docs] @deprecated(registerCalculator_deprecation_msg) def registerCalculator(self, f, argnames=None): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.recipeorganizer.RecipeOrganizer.register_calculator instead. """ return self.register_calculator(f, argnames=argnames)
[docs] def register_function(self, function, name=None, argnames=None): """Register a function so it can be used within equation strings. This creates a function with this class that can be used within string equations. The resulting equation does not require the arguments to be passed in the equation string, as this will be handled automatically. Parameters ---------- function : callable The callable to register. If this is an Equation instance, then all that needs to be provided is a name. name : str or None, optional The name of the function to be used in equations. If this is None (default), the method will try to determine the name of the function automatically. argnames : list or None, optional The names of the arguments to `function` (list or None). If this is None (default), then the argument names will be extracted from the function. Returns ------- equation_object : Equation The callable Equation object. Raises ------ TypeError If name or argnames cannot be automatically extracted. TypeError If an automatically extracted name is '<lambda>'. ValueError If function is an Equation object and name is None. Notes ----- The `name` and `argnames` args can be extracted from regular Python functions (of type <function>), bound class methods, and callable classes. """ # If the function is an equation, we treat it specially. This is # required so that the objects observed by the root get observed if the # Equation is used within another equation. It is assumed that a plain # function is not observable. if isinstance(function, Equation): if name is None: m = ( "The equation must be given a name. " "Specify a name with the 'name' argument." ) raise ValueError(m) self._eqfactory.registerOperator(name, function) return function # Introspection code if name is None or argnames is None: import inspect # A decorator such as `deprecated` replaces the code object with # that of its (*args, **kwargs) wrapper, so introspect the # function it wraps while still registering the decorated one. wrapped_function = inspect.unwrap(function) fncode = None # This will let us offset the argument list to eliminate 'self' offset = 0 # check regular functions if inspect.isfunction(wrapped_function): fncode = wrapped_function.__code__ # check class method elif inspect.ismethod(function): fncode = function.__func__.__code__ offset = 1 # check functor elif hasattr(wrapped_function, "__call__") and hasattr( wrapped_function.__call__, "__func__" ): fncode = wrapped_function.__call__.__func__.__code__ offset = 1 else: m = "Cannot extract name or argnames" raise ValueError(m) # Extract the name if name is None: name = fncode.co_name if name == "<lambda>": m = "You must supply a name name for a lambda function" raise ValueError(m) # Extract the arguments if argnames is None: argnames = list(fncode.co_varnames) argnames = argnames[offset : fncode.co_argcount] # End introspection code # Make missing Parameters for pname in argnames: if pname not in self._eqfactory.builders: self._new_parameter(pname, 0) # Initialize and register from diffpy.srfit.fitbase.calculator import Calculator if isinstance(function, Calculator): for pname in argnames: parameter = self.get(pname) function.addLiteral(parameter) self._eqfactory.registerOperator(name, function) else: self._eqfactory.register_function(name, function, argnames) # Now we can create the Equation and return it to the user. equation_object = self._eqfactory.makeEquation(name) return equation_object
[docs] @deprecated(registerFunction_deprecation_msg) def registerFunction(self, f, name=None, argnames=None): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.recipeorganizer.RecipeOrganizer.register_function instead. """ return self.register_function(f, name=name, argnames=argnames)
[docs] def register_string_function(self, function_str, name, func_params={}): """Register a string function. This creates a function with this class that can be used within string equations. The resulting equation does not require the arguments to be passed in the function string, as this will be handled automatically. Parameters ---------- function_str : str A string equation to register. name : str The name of the function to be used in equations. func_params : dict, optional A dictionary of Parameters, indexed by name, that are used in `function_str`, but not part of the FitRecipe (default {}). Returns ------- equation_object : Equation The callable Equation object. Raises ------ ValueError If `func_params` uses a name that is already used for another managed object. ValueError If the function name is the name of another managed object. """ # Build the equation instance. eq = get_equation_from_string( function_str, self._eqfactory, ns=func_params, buildargs=True ) eq.name = name # Register any new Parameters. for parameter in self._eqfactory.newargs: self._add_parameter(parameter) # Register the equation as a callable function. argnames = eq.argdict.keys() equation_object = self.register_function( eq, name=name, argnames=argnames ) return equation_object
[docs] @deprecated(registerStringFunction_deprecation_msg) def registerStringFunction(self, fstr, name, ns={}): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.recipeorganizer.RecipeOrganizer.register_string_function instead. """ return self.register_string_function(fstr, name, func_params=ns)
[docs] def evaluate_equation(self, equation_str, func_params={}): """Evaluate a string equation. This method takes a string representation of a mathematical equation and evaluates it using the current values of the registered Parameters in the FitRecipe. Additional parameters not part of the FitRecipe can also be provided via the `func_params` dictionary. Parameters ---------- equation_str The string equation to evaluate. The equation is evaluated at the current value of the registered Parameters. func_params : dict, optional The dictionary of Parameters, indexed by name, that are used in `equation_str`, but not part of the FitRecipe (default `{}`). Returns ------- returned_value : float The value of the evaluated equation. Raises ------ ValueError If `func_params` uses a name that is already used for a variable. """ eq = get_equation_from_string( equation_str, self._eqfactory, func_params ) try: returned_value = eq() finally: self._eqfactory.wipeout(eq) return returned_value
[docs] @deprecated(evaluateEquation_deprecation_msg) def evaluateEquation(self, eqstr, ns={}): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.recipeorganizer.RecipeOrganizer.evaluate_equation instead. """ return self.evaluate_equation(eqstr, func_params=ns)
[docs] def add_constraint(self, parameter, constraint_eq, params={}): """Constrain a parameter to an equation. Note that only one constraint can exist on a Parameter at a time. Parameters ---------- parameter : str or Parameter The name of a Parameter or a Parameter to constrain. constraint_eq : str or Equation A 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 `params` argument, or if they are managed by this object. params : dict, optional A dictionary of Parameters, indexed by name, that are used in `parameter`, but not part of this object (default {}). Raises ------ ValueError If `params` uses a name that is already used for a variable. ValueError If `parameter` is a string but not part of this object or in `params`. ValueError If `parameter` is marked as constant. """ if isinstance(parameter, str): name = parameter parameter = self.get(name) if parameter is None: parameter = params.get(name) if parameter is None: raise ValueError("The parameter cannot be found") if parameter.const: raise ValueError("The parameter '%s' is constant" % parameter) if isinstance(constraint_eq, str): eqstr = constraint_eq eq = get_equation_from_string( constraint_eq, self._eqfactory, params ) else: eq = Equation(root=constraint_eq) eqstr = constraint_eq.name eq.name = "_constraint_%s" % parameter.name # Make and store the constraint constraint_eq = Constraint() constraint_eq.add_constraint(parameter, eq) # Store the equation string so it can be shown later. constraint_eq.eqstr = eqstr self._constraints[parameter] = constraint_eq # Our configuration changed self._update_configuration() return
[docs] @deprecated(constrain_deprecation_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.recipeorganizer.RecipeOrganizer.add_constraint instead. """ self.add_constraint(par, con, params=ns) return
[docs] def is_constrained(self, parameter): """Determine if a Parameter is constrained in this object. Parameters ---------- parameter : str or Parameter The name of a Parameter or a Parameter to check. Returns ------- bool True if the Parameter is constrained in this object, False otherwise. """ if isinstance(parameter, str): name = parameter parameter = self.get(name) return parameter in self._constraints
[docs] @deprecated(isConstrained_deprecation_msg) def isConstrained(self, par): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.recipeorganizer.RecipeOrganizer.is_constrained instead. """ return self.is_constrained(par)
[docs] def remove_constraint(self, *pars): """Unconstrain a Parameter. This removes any constraints on a Parameter. Parameters ---------- *pars : str or Parameter The names of Parameters or Parameters to unconstrain. Raises ------ ValueError If the Parameter is not constrained. """ update = False for parameter in pars: if isinstance(parameter, str): name = parameter parameter = self.get(name) if parameter is None: raise ValueError("The parameter cannot be found") if parameter in self._constraints: self._constraints[parameter].remove_constraint() del self._constraints[parameter] update = True if update: # Our configuration changed self._update_configuration() else: raise ValueError("The parameter is not constrained") return
[docs] @deprecated(unconstrain_deprecation_msg) def unconstrain(self, *pars): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.recipeorganizer.RecipeOrganizer.remove_constraint instead. """ self.remove_constraint(*pars) return
[docs] def get_constrained_parmeters(self, recurse=False): """Get a list of constrained managed Parameters in this object. Parameters ---------- recurse : bool, optional If False (default), only constrained Parameters in this object are returned. If True, constrained Parameters in managed sub-objects are also included. Returns ------- constrained_params : list of Parameter The list of constrained managed Parameters in this object. """ const = self._get_constraints(recurse) constrained_params = const.keys() return constrained_params
[docs] @deprecated(getConstrainedPars_deprecation_msg) def getConstrainedPars(self, recurse=False): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.recipeorganizer.RecipeOrganizer.get_constrained_parmeters instead. """ return self.get_constrained_parmeters(recurse=recurse)
[docs] def clear_all_constraints(self, recurse=False): """Clear all constraints managed by this organizer. This removes constraints that are held in this organizer, no matter where the constrained parameters are from. Parameters ---------- recurse : bool, optional If False (default), only constraints in this object are cleared. If True, constraints in managed sub-objects are also cleared. """ if self._constraints: self.remove_constraint(*self._constraints) if recurse: for m in filter(_has_clear_constraints, self._iter_managed()): m.clear_all_constraints(recurse) return
[docs] @deprecated(clearConstraints_deprecation_msg) def clearConstraints(self, recurse=False): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.recipeorganizer.RecipeOrganizer.clear_all_constraints instead. """ return self.clear_all_constraints(recurse=recurse)
[docs] def add_soft_bounds( self, param_or_eq, lower_bound=-inf, upper_bound=inf, sig=1, scaled=False, params={}, ): """Restrain an expression to specified bounds. See Notes for how the penalty is calculated. Parameters ---------- param_or_eq : str The equation or parameter to restrain. lower_bound : float, optional The lower bound for the restraint evaluation (default is -inf). upper_bound : float, optional The upper bound for the restraint evaluation (default is inf). sig : float, optional The uncertainty associated with the bounds (default is 1). Please see Notes for how this is used in the penalty calculation. scaled : bool, optional If True, the restraint penalty is scaled by the unrestrained point-average chi^2 (chi^2/numpoints) (default is False). params : dict, optional The dictionary of Parameters, indexed by name, that are used in `param_or_eq` (if an equation string is used) but are not part of the RecipeOrganizer (default is {}). Returns ------- Restraint The created Restraint object, which can be used with the 'unrestrain' method. Notes ----- The penalty is calculated as: .. (max(0, lower_bound - val, val - upper_bound) / sig) ** 2 where `val` is the value of the evaluated `param_or_eq`. If `scaled` is True, this penalty is multiplied by the average chi^2. Examples -------- Restraining the lattice parameters of an Ni lattice to be approximately 7.4Å (2x the original lattice param) can be done with the following code: .. recipe.add_soft_bounds( "a_ni + b_ni", lower_bound=7.0, upper_bound=7.5, sig=0.1, scaled=True, params={"b_ni": Parameter("b_ni", 3.473)} ) Raises ------ ValueError If `params` contains a name that is already used for a Parameter. ValueError If `param_or_eq` depends on a Parameter that is not part of the RecipeOrganizer and is not defined in `params`. """ if isinstance(param_or_eq, str): eqstr = param_or_eq eq = get_equation_from_string(param_or_eq, self._eqfactory, params) else: eq = Equation(root=param_or_eq) eqstr = param_or_eq.name # Make and store the restraint param_or_eq = Restraint(eq, lower_bound, upper_bound, sig, scaled) param_or_eq.eqstr = eqstr self.register_soft_bounds(param_or_eq) return param_or_eq
[docs] @deprecated(restrain_deprecation_msg) def restrain(self, res, lb=-inf, ub=inf, sig=1, scaled=False, ns={}): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.recipeorganizer.RecipeOrganizer.add_soft_bounds instead. """ return self.add_soft_bounds( res, lower_bound=lb, upper_bound=ub, sig=sig, scaled=scaled, params=ns, )
[docs] def register_soft_bounds(self, res): """Add a Restraint instance to the RecipeOrganizer. Parameters ---------- res : Restraint A Restraint instance. """ self._restraints.add(res) # Our configuration changed. Notify observers. self._update_configuration() return
[docs] @deprecated(addRestraint_deprecation_msg) def addRestraint(self, res): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.recipeorganizer.RecipeOrganizer.register_soft_bounds instead. """ self.register_soft_bounds(res) return
[docs] def remove_soft_bounds(self, *ress): """Remove a Restraint from the RecipeOrganizer. Parameters ---------- *ress : Restraint The Restraints returned from the 'add_soft_bounds' method or added with the 'register_soft_bounds' method. """ update = False restuple = tuple(self._restraints) for res in ress: if res in restuple: self._restraints.remove(res) update = True if update: # Our configuration changed self._update_configuration() return
[docs] @deprecated(unrestrain_deprecation_msg) def unrestrain(self, *ress): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.recipeorganizer.RecipeOrganizer.remove_soft_bounds instead. """ self.remove_soft_bounds(*ress) return
[docs] def clear_all_soft_bounds(self, recurse=False): """Clear all restraints. Parameters ---------- recurse : bool, optional If False (default), only restraints in this object are cleared. If True, restraints in managed sub-objects are also cleared. """ self.remove_soft_bounds(*self._restraints) if recurse: for msg in filter(_has_clear_restraints, self._iter_managed()): msg.clear_all_soft_bounds(recurse) return
[docs] @deprecated(clearRestraints_deprecation_msg) def clearRestraints(self, recurse=False): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.recipeorganizer.RecipeOrganizer.clear_all_soft_bounds instead. """ self.clear_all_soft_bounds(recurse=recurse) return
def _get_constraints(self, recurse=True): """Get the constrained Parameters for this and managed sub- objects.""" constraints = {} if recurse: for m in filter(_has_get_constraints, self._iter_managed()): constraints.update(m._get_constraints(recurse)) constraints.update(self._constraints) return constraints def _get_restraints(self, recurse=True): """Get the Restraints for this and embedded ParameterSets. This returns a set of Restraint objects. """ restraints = set(self._restraints) if recurse: for m in filter(_has_get_restraints, self._iter_managed()): restraints.update(m._get_restraints(recurse)) return restraints def _validate(self): """Validate my state. This performs RecipeContainer validations. This validates contained Restraints and Constraints. Raises ------ AttributeError If validation fails. """ RecipeContainer._validate(self) iterable = chain(self._restraints, self._constraints.values()) self._validate_others(iterable) return # For printing the configured recipe to screen def _format_managed(self, prefix=""): """Format hierarchy of managed parameters for showing. Parameters ---------- prefix : str The leading string to be prefixed to each parameter name. Returns ------- list The list of formatted lines, one per each Parameter. """ lines = [] formatstr = "{:<W}{}" # Format own parameters. if self._parameters: w0 = max(len(n) for n in self._parameters) w1 = ((w0 + len(prefix) + 1) // 4 + 1) * 4 fmt = formatstr.replace("W", str(w1)) lines.extend( fmt.format(prefix + n, p.value) for n, p in self._parameters.items() ) # Recurse into managed objects. for obj in self._iter_managed(): if hasattr(obj, "_format_managed"): oprefix = prefix + obj.name + "." tlines = obj._format_managed(prefix=oprefix) lines.extend([""] if lines and tlines else []) lines.extend(tlines) return lines def _format_constraints(self): """Format constraints for showing. This collects constraints on all levels of the hierarchy and displays them with respect to this level. Returns ------- list The list of formatted lines displaying the defined constraints. Empty list when no constraints were defined. """ cdict = self._get_constraints() # Find each constraint and format the equation clines = [] for parameter, con in cdict.items(): loc = self._locate_managed_object(parameter) if loc: locstr = ".".join(o.name for o in loc[1:]) clines.append("%s <-- %s" % (locstr, con.eqstr)) else: clines.append("%s <-- %s" % (parameter.name, con.eqstr)) clines.sort(key=numstr) return clines def _format_restraints(self): """Format restraints for showing. This collects restraints on all levels of the hierarchy and displays them with respect to this level. Returns ------- list The list of formatted lines displaying the defined restraints. Empty list when no restraints were defined. """ rset = self._get_restraints() rlines = [] for res in rset: line = ( "%s: lower_bound = %f, upper_bound = %f, sig = %f, scaled = %s" % ( res.eqstr, res.lower_bound, res.upper_bound, res.sig, res.scaled, ) ) rlines.append(line) rlines.sort(key=numstr) return rlines
[docs] def show(self, pattern="", textwidth=78): """Show the configuration hierarchy on the screen. This will print out a summary of all contained objects. Parameters ---------- pattern : str, optional Limit output to only those parameters that match this regular expression (match all by default). textwidth : int, optional Trim formatted lines at this text width to avoid folding at the screen width. Do not trim when negative or 0. """ regexp = re.compile(pattern) _pmatch_with_re = partial(_pmatch, regexp=regexp) # Show sub objects and their parameters lines = [] tlines = self._format_managed() if tlines: lines.extend(["Parameters", _DASHEDLINE]) linesok = filter(_pmatch_with_re, tlines) lastnotblank = False # squeeze repeated blank lines for lastnotblank, g in groupby(linesok, bool): lines.extend(g if lastnotblank else [""]) # remove trailing blank line if not lastnotblank: lines.pop(-1) # FIXME - parameter names in equations not particularly informative # Show constraints cmatch = regexp.search tlines = self._format_constraints() if tlines: if lines: lines.append("") lines.extend(["Constraints", _DASHEDLINE]) lines.extend(filter(cmatch, tlines)) # FIXME - parameter names in equations not particularly informative # Show restraints tlines = self._format_restraints() if tlines: if lines: lines.append("") lines.extend(["Restraints", _DASHEDLINE]) lines.extend(filter(_pmatch_with_re, tlines)) # Determine effective text width tw. tw = textwidth if (textwidth is not None and textwidth > 0) else None # Avoid outputting "\n" when there is no output. if lines: print("\n".join(s[:tw] for s in lines)) return
# End RecipeOrganizer
[docs] def get_equation_from_string( eqstr, factory, ns={}, buildargs=False, argclass=Parameter, argkw={} ): """Make an Equation object from a string. Parameters ---------- eqstr : str A string representation of the equation. The equation must consist of numpy operators and "known" Parameters. Parameters are known if they are in ns, or already defined in the factory. factory : EquationFactory An EquationFactory instance. ns : dict, optional The dictionary of Parameters indexed by name that are used in the eqstr but not already defined in the factory (default {}). buildargs : bool, optional A flag indicating whether missing Parameters can be created by the Factory (default False). If False, then the a ValueError will be raised if there are undefined arguments in the eqstr. argclass : Parameter class, optional Class to use when creating new Arguments (default Parameter). The class constructor must accept the 'name' key word. argkw : dict, optional Key word dictionary to pass to the argclass constructor (default {}). Returns ------- eq : Equation An Equation instance representing the equation in eqstr. Raises ------ ValueError If buildargs is False and there are undefined parameters in eqstr or if ns uses a name that is already defined in the factory. """ defined = set(factory.builders.keys()) # Check if ns overloads any parameters. if defined.intersection(ns.keys()): raise ValueError("ns contains defined names") # Register the ns parameters in the equation factory for name, arg in ns.items(): factory.registerArgument(name, arg) eq = factory.makeEquation(eqstr, buildargs, argclass, argkw) # Clean the ns parameters for name in ns: factory.deRegisterBuilder(name) return eq
@deprecated(equationFromString_deprecation_msg) def equationFromString( eqstr, factory, ns={}, buildargs=False, argclass=Parameter, argkw={} ): """This function has been deprecated and will be removed in version 4.0.0. Please use diffpy.srfit.fitbase.recipeorganizer.get_equation_from_string instead. """ return get_equation_from_string( eqstr, factory, ns=ns, buildargs=buildargs, argclass=argclass, argkw=argkw, ) def _has_clear_constraints(msg): """Check whether `msg` has a `clear_all_constraints` method.""" return hasattr(msg, "clear_all_constraints") def _has_clear_restraints(msg): """Check whether `msg` has a `clear_all_soft_bounds` method.""" return hasattr(msg, "clear_all_soft_bounds") def _has_get_restraints(msg): """Check whether `msg` has a `_get_restraints` method.""" return hasattr(msg, "_get_restraints") def _has_get_constraints(msg): """Check whether `msg` has a `_get_constraints` method.""" return hasattr(msg, "_get_constraints") def _pmatch(inp_str, regexp): """Check whether the leading name in `inp_str` matches `regexp`.""" parts = inp_str.split(None, 1) return len(parts) < 2 or regexp.search(parts[0])