#!/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])