diff --git a/news/init-w-results.rst b/news/init-w-results.rst new file mode 100644 index 00000000..ae63a94a --- /dev/null +++ b/news/init-w-results.rst @@ -0,0 +1,23 @@ +**Added:** + +* Added ``initialize_recipe_with_results`` to ``FitRecipe``. + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/srfit/fitbase/fitrecipe.py b/src/diffpy/srfit/fitbase/fitrecipe.py index c44eb6d9..0aff6ecd 100644 --- a/src/diffpy/srfit/fitbase/fitrecipe.py +++ b/src/diffpy/srfit/fitbase/fitrecipe.py @@ -35,11 +35,13 @@ __all__ = ["FitRecipe"] from collections import OrderedDict +from pathlib import Path import matplotlib.pyplot as plt from bg_mpl_stylesheets.styles import all_styles from numpy import array, concatenate, dot, sqrt +import diffpy.srfit.util.inpututils as utils from diffpy.srfit.fitbase.fithook import PrintFitHook from diffpy.srfit.fitbase.parameter import ParameterProxy from diffpy.srfit.fitbase.recipeorganizer import RecipeOrganizer @@ -129,64 +131,64 @@ class FitRecipe(_fitrecipe_interface, RecipeOrganizer): Attributes ---------- - name + name : str A name for this FitRecipe. - fithooks - List of FitHook instances that can pass information out - of the system during a refinement. By default, the is + fithooks : list + The list of FitHook instances that can pass information out + of the system during a refinement. By default, this is populated by a PrintFitHook instance. - _constraints - A dictionary of Constraints, indexed by the constrained + _constraints : dict + The dictionary of Constraints, indexed by the constrained Parameter. Constraints can be added using the 'constrain' method. - _oconstraints - An ordered list of the constraints from this and all + _oconstraints : list + The ordered list of the constraints from this and all sub-components. - _calculators - A managed dictionary of Calculators. - _contributions - A managed OrderedDict of FitContributions. - _parameters - A managed OrderedDict of parameters (in this case the + _calculators : dict + The managed dictionary of Calculators. + _contributions : OrderedDict + The managed OrderedDict of FitContributions. + _parameters : OrderedDict + The managed OrderedDict of parameters (in this case the parameters are varied). - _parsets - A managed dictionary of ParameterSets. - _eqfactory - A diffpy.srfit.equation.builder.EquationFactory + _parsets : dict + The managed dictionary of ParameterSets. + _eqfactory : diffpy.srfit.equation.builder.EquationFactory + The diffpy.srfit.equation.builder.EquationFactory instance that is used to create constraints and - restraints from string - _restraintlist - A list of restraints from this and all sub-components. - _restraints - A set of Restraints. Restraints can be added using the + restraints from strings. + _restraintlist : list + The list of restraints from this and all sub-components. + _restraints : set + The set of Restraints. Restraints can be added using the 'restrain' or 'confine' methods. - _ready - A flag indicating if all attributes are ready for the + _ready : bool + The flag indicating if all attributes are ready for the calculation. - _tagmanager - A TagManager instance for managing tags on Parameters. - _weights - List of weighing factors for each FitContribution. The + _tagmanager : TagManager + The TagManager instance for managing tags on Parameters. + _weights : list + The list of weighing factors for each FitContribution. The weights are multiplied by the residual of the FitContribution when determining the overall residual. - _fixedtag + _fixedtag : str "__fixed", used for tagging variables as fixed. Don't use this tag unless you want issues. Properties ---------- - names - Variable names (read only). See get_names. - values - Variable values (read only). See get_values. - fixednames - Names of the fixed refinable variables (read only). - fixedvalues - Values of the fixed refinable variables (read only). - bounds - Bounds on parameters (read only). See get_bounds_pairs. - bounds2 - Bounds on parameters (read only). See get_bounds_array. + names : list + The variable names (read only). See get_names. + values : numpy.ndarray + The variable values (read only). See get_values. + fixednames : list + The names of the fixed refinable variables (read only). + fixedvalues : numpy.ndarray + The values of the fixed refinable variables (read only). + bounds : list of tuple + The bounds on parameters (read only). See get_bounds_pairs. + bounds2 : tuple of numpy.ndarray + The bounds on parameters (read only). See get_bounds_array. """ fixednames = property( @@ -267,12 +269,12 @@ def push_fit_hook(self, fithook, index=None): diffpy.srfit.fitbase.fithook.FitHook class for the required interface. Added FitHooks will be called sequentially during refinement. - Attributes + Parameters ---------- - fithook - FitHook instance to add to the sequence - index - Index for inserting fithook into the list of fit hooks. If + fithook : diffpy.srfit.fitbase.fithook.FitHook + The FitHook instance to add to the sequence. + index : int or None, optional + The index for inserting fithook into the list of fit hooks. If this is None (default), the fithook is added to the end. """ if index is None: @@ -295,18 +297,20 @@ def pushFitHook(self, fithook, index=None): def pop_fit_hook(self, fithook=None, index=-1): """Remove a FitHook by index or reference. - Attributes + Parameters ---------- - fithook - FitHook instance to remove from the sequence. If this is + fithook : diffpy.srfit.fitbase.fithook.FitHook or None, optional + The FitHook instance to remove from the sequence. If this is None (default), default to index. - index - Index of FitHook instance to remove (default -1). - + index : int, optional + The index of FitHook instance to remove (default -1). - Raises ValueError if fithook is not None, but is not present in the - sequence. - Raises IndexError if the sequence is empty or index is out of range. + Raises + ------ + ValueError + If fithook is not None, but is not present in the sequence. + IndexError + If the sequence is empty or index is out of range. """ if fithook is not None: self.fithooks.remove(fithook) @@ -353,15 +357,18 @@ def clearFitHooks(self): def add_contribution(self, con, weight=1.0): """Add a FitContribution to the FitRecipe. - Attributes + Parameters ---------- - con + con : FitContribution The FitContribution to be stored. + weight : float, optional + The weight of the FitContribution. Default is 1.0. - - Raises ValueError if the FitContribution has no name - Raises ValueError if the FitContribution has the same name as some - other managed object. + Raises + ------ + ValueError + If the FitContribution has no name or if the FitContribution has + the same name as some other managed object. """ self._add_object(con, self._contributions, True) self._weights.append(weight) @@ -379,7 +386,19 @@ def addContribution(self, con, weight=1.0): return def set_weight(self, con, weight): - """Set the weight of a FitContribution.""" + """Set the weight of a FitContribution. + + Parameters + ---------- + con : FitContribution + The FitContribution object whose weight is to be set. + weight : float + The weight value to assign to the specified FitContribution. + + Returns + ------- + None + """ idx = list(self._contributions.values()).index(con) self._weights[idx] = weight return @@ -396,15 +415,16 @@ def setWeight(self, con, weight): def add_parameter_set(self, parset): """Add a ParameterSet to the hierarchy. - Attributes + Parameters ---------- - parset + parset : ParameterSet The ParameterSet to be stored. - - Raises ValueError if the ParameterSet has no name. - Raises ValueError if the ParameterSet has the same name as some other - managed object. + Raises + ------ + ValueError + If the ParameterSet has no name or if the ParameterSet has the same + name as some other managed object. """ self._add_object(parset, self._parsets, True) return @@ -422,7 +442,19 @@ def addParameterSet(self, parset): def remove_parameter_set(self, parset): """Remove a ParameterSet from the hierarchy. - Raises ValueError if parset is not managed by this object. + This method removes the specified ParameterSet object from the internal + hierarchy of managed ParameterSets. If the provided ParameterSet is not + currently managed by this object, a ValueError will be raised. + + Parameters: + ----------- + parset : ParameterSet + The ParameterSet instance to be removed from the hierarchy. + + Raises: + ------- + ValueError + If the provided ParameterSet is not managed by this object. """ self._remove_object(parset, self._parsets) return @@ -442,7 +474,7 @@ def residual(self, p=[]): Parameters ---------- - p + p : list or numpy.ndarray The list of current variable values, provided in the same order as the '_parameters' list. If p is an empty iterable (default), then it is assumed that the parameters have already been @@ -493,7 +525,7 @@ def scalar_residual(self, p=[]): Parameters ---------- - p + p : list or numpy.ndarray The list of current variable values, provided in the same order as the '_parameters' list. If p is an empty iterable (default), then it is assumed that the parameters have already been @@ -531,7 +563,10 @@ def _prepare(self): This updates the local restraints with those of the contributions. - Raises AttributeError if there are variables without a value. + Raises + ------ + AttributeError + If there are variables without a value. """ # Only prepare if the configuration has changed within the recipe @@ -682,37 +717,40 @@ def add_variable( ): """Add a variable to be refined. - Attributes + Parameters ---------- - par - A Parameter that will be varied during a fit. - value - An initial value for the variable. If this is None + par : diffpy.srfit.fitbase.Parameter + The Parameter that will be varied during a fit. + value : float or None, optional + The initial value for the variable. If this is None (default), then the current value of par will be used. - name - A name for this variable. If name is None (default), then + name : str or None, optional + The name for this variable. If name is None (default), then the name of the parameter will be used. - fixed + fixed : bool, optional Fix the variable so that it does not vary (default False). - tag - A tag for the variable. This can be used to retrieve, fix + tag : str or None, optional + The tag for the variable. This can be used to retrieve, fix or free variables by tag (default None). Note that a variable is automatically tagged with its name and "all". - tags - A list of tags (default []). Both tag and tags can be + tags : list of str, optional + The list of tags (default []). Both tag and tags can be applied. - Returns ------- - vars + ParameterProxy ParameterProxy (variable) for the passed Parameter. - - Raises ValueError if the name of the variable is already taken by - another managed object. - Raises ValueError if par is constant. - Raises ValueError if par is constrained. + Raises + ------ + ValueError + If the name of the variable is already taken by + another managed object. + ValueError + If par is constant. + ValueError + If par is constrained. """ name = name or par.name if par.const: @@ -750,13 +788,15 @@ def delete_variable(self, var): Note that constraints and restraints involving the variable are not modified. - Attributes + Parameters ---------- - var + var : ParameterProxy A variable of the FitRecipe. - - Raises ValueError if var is not part of the FitRecipe. + Raises + ------ + ValueError + If var is not part of the FitRecipe. """ self._remove_parameter(var) @@ -790,29 +830,31 @@ def create_new_variable( optimization routine, and therefore should only be created to be used in constraint or restraint equations. - Attributes + Parameters ---------- - name + name : str The name of the variable. The variable will be able to be used by this name in restraint and constraint equations. - value - An initial value for the variable. If this is None + value : float or None, optional + The initial value for the variable. If this is None (default), then the variable will be given the value of the first non-None-valued Parameter constrained to it. If this fails, an error will be thrown when 'residual' is called. - fixed + fixed : bool, optional Fix the variable so that it does not vary (default False). The variable will still be managed by the FitRecipe. - tag - A tag for the variable. This can be used to fix and free + tag : str or None, optional + The tag for the variable. This can be used to fix and free variables by tag (default None). Note that a variable is automatically tagged with its name and "all". - tags - A list of tags (default []). Both tag and tags can be + tags : list of str, optional + The list of tags (default []). Both tag and tags can be applied. - - Returns the new variable (Parameter instance). + Returns + ------- + Parameter + The new variable (Parameter instance). """ # This will fix the Parameter var = self._new_parameter(name, value) @@ -908,16 +950,51 @@ def __get_vars_from_args(self, *args, **kw): return varargs def fix(self, *args, **kw): - """Fix a parameter by reference, name or tag. + """Fix one or more parameters by reference, name, or tag. - A fixed variable is not refined. Variables are free by default. + This method marks specified parameters as fixed, meaning they will not + be refined during the fitting process. By default, all parameters are + free (not fixed). Parameters can be specified using their references, + names, or tags. Additionally, keyword arguments can be used to assign + specific values to the fixed parameters. - This method accepts string or variable arguments. An argument of - "all" selects all variables. Keyword arguments must be parameter - names, followed by a value to assign to the fixed variable. + Parameters + ---------- + *args : str or Parameter + The positional arguments specifying the parameters to fix. + These can be parameter objects, their names as strings, or + tags. The special string "all" can be used to select all + parameters. + **kw : dict + The keyword arguments where the keys are parameter names and + the values are the values to assign to the corresponding + fixed parameters. + + Raises + ------ + ValueError: + If an unknown parameter, name, or tag is passed, or if a + tag is passed as a keyword argument. + + Example + ------- + + :: + + # Fix a parameter by reference + recipe.fix(param1) + + # Fix a parameter by name + recipe.fix("param2") - Raises ValueError if an unknown Parameter, name or tag is - passed, or if a tag is passed in a keyword. + # Fix all parameters + recipe.fix("all") + + # Fix parameters by tag + recipe.fix(tag="group1") + + # Fix a parameter and assign it a value + recipe.fix(param3=10.0) """ # Check the inputs and get the variables from them varargs = self.__get_vars_from_args(*args, **kw) @@ -933,17 +1010,44 @@ def fix(self, *args, **kw): return def free(self, *args, **kw): - """Free a parameter by reference, name or tag. + """Free one or more parameters by reference, name, or tag. - A free variable is refined. Variables are free by default. - Constrained variables are not free. + This method marks specified parameters as free, allowing them to be + refined during the fitting process. By default, variables + are free unless they are constrained. Constrained variables + cannot be freed. - This method accepts string or variable arguments. An argument of - "all" selects all variables. Keyword arguments must be parameter - names, followed by a value to assign to the fixed variable. + Parameters + ---------- + *args : str or Parameter + The positional arguments specifying the parameters to free. + These can be: + - Parameter objects + - Names of parameters (as strings) + - Tags associated with parameters (as strings) + - The string "all" to select all parameters. + **kw : dict + The keyword arguments specifying parameter names as keys and + their values to assign after freeing. This is useful + for setting the value of a parameter while marking it as free. - Raises ValueError if an unknown Parameter, name or tag is - passed, or if a tag is passed in a keyword. + Raises + ------ + ValueError + If an unknown parameter, name, or tag is passed, or if a + tag is passed as a keyword argument. + + Notes + ----- + - Parameters that are already free will remain free. + - Tags associated with fixed parameters will be removed when they + are freed. + - If keyword arguments are provided, the corresponding parameter values + will be updated after freeing. + + Returns + ------- + None """ # Check the inputs and get the variables from them varargs = self.__get_vars_from_args(*args, **kw) @@ -960,7 +1064,24 @@ def free(self, *args, **kw): return def is_free(self, var): - """Check if a variable is fixed.""" + """Determine if a variable is free (not fixed) in the fit + recipe. + + This method checks whether the specified variable does not have the + fixed tag associated with it, indicating that it is free to vary + during the fitting process. + + Parameters + ---------- + var : object + The variable to check. This is typically an instance of a parameter + or variable object used in the fit recipe. + + Returns + ------- + bool + True if the variable is free (not fixed), False otherwise. + """ return not self._tagmanager.hasTags(var, self._fixedtag) @deprecated(isFree_dep_msg) @@ -978,13 +1099,15 @@ def unconstrain(self, *pars): This removes any constraints on a Parameter. If the Parameter is also a variable of the recipe, it will be freed as well. - Attributes + Parameters ---------- - *pars - The names of Parameters or Parameters to unconstrain. + *pars : str or Parameter + The names of Parameters or Parameter objects to unconstrain. - - Raises ValueError if the Parameter is not constrained. + Raises + ------ + ValueError + If the Parameter is not constrained. """ update = False for par in pars: @@ -1018,26 +1141,29 @@ def constrain(self, par, con, ns={}): and its current value is None. A constrained variable will be set as fixed. - Attributes + Parameters ---------- - par + par : Parameter The Parameter to constrain. - con - A string representation of the constraint equation or a - Parameter to constrain to. A constraint equation must + con : str or Parameter + The string representation of the constraint equation or a + Parameter to constrain to. A constraint equation must consist of numpy operators and "known" Parameters. Parameters are known if they are in the ns argument, or if they are managed by this object. - ns - A dictionary of Parameters, indexed by name, that are used + ns : dict, optional + The dictionary of Parameters, indexed by name, that are used in the eqstr, but not part of this object (default {}). - - Raises ValueError if ns uses a name that is already used for a - variable. - Raises ValueError if eqstr depends on a Parameter that is not part of - the FitRecipe and that is not defined in ns. - Raises ValueError if par is marked as constant. + Raises + ------ + ValueError + If ns uses a name that is already used for a variable. + ValueError + If eqstr depends on a Parameter that is not part of the FitRecipe + and that is not defined in ns. + ValueError + If par is marked as constant. """ if isinstance(par, str): name = par @@ -1068,10 +1194,24 @@ def constrain(self, par, con, ns={}): return def get_values(self): - """Get the current values of the variables in a list.""" - return array( + """Retrieve the current values of all free variables in the fit + recipe. + + This method collects the values of all parameters that are marked as + free (i.e., adjustable during the fitting process) and returns them + as a NumPy array. + + Returns + ------- + + values_array : numpy.ndarray + The array containing the current values of all free + variables in the fit recipe. + """ + values_array = array( [v.value for v in self._parameters.values() if self.is_free(v)] ) + return values_array @deprecated(getValues_dep_msg) def getValues(self): @@ -1082,8 +1222,20 @@ def getValues(self): return self.get_values() def get_names(self): - """Get the names of the variables in a list.""" - return [v.name for v in self._parameters.values() if self.is_free(v)] + """Retrieve the names of all free variables in the fit recipe. + + This method iterates through the parameters in the fit recipe and + returns a list of names for those variables that are marked as free. + + Returns + ------- + parameter_names :list of str + The list containing the names of free variables. + """ + parameter_names = [ + v.name for v in self._parameters.values() if self.is_free(v) + ] + return parameter_names @deprecated(getNames_dep_msg) def getNames(self): @@ -1099,7 +1251,7 @@ def get_bounds_pairs(self): Returns ------- bounds_pair_list : list of tuple of float - A list of ``(lower, upper)`` bounds on the variables, in the same + The list of ``(lower, upper)`` bounds on the variables, in the same order as ``get_names`` and ``get_values``. """ return [v.bounds for v in self._parameters.values() if self.is_free(v)] @@ -1120,10 +1272,10 @@ def get_bounds_array(self): Returns ------- lower_bounds : numpy.ndarray - A numpy array of lower bounds on the variables, in the same order + The numpy array of lower bounds on the variables, in the same order as ``get_names`` and ``get_values``. upper_bounds : numpy.ndarray - A numpy array of upper bounds on the variables, in the same order + The numpy array of upper bounds on the variables, in the same order as ``get_names`` and ``get_values``. """ bounds = self.get_bounds_pairs() @@ -1184,6 +1336,84 @@ def initialize_recipe_with_recipe(self, recipe_object): if restraint not in self._restraints: self._restraints.add(restraint) + def _pretty_print_results_dict(self, params_dict): + """Pretty print a dictionary of parameter names and values.""" + sorted_params = sorted(params_dict.items()) + width = max(len(name) for name, _ in sorted_params) + for name, value in sorted_params: + if isinstance(value, float): + value_str = f"{value:.6g}" + else: + value_str = str(value) + print(f" {name:<{width}} = {value_str}") + + def _set_parameters_from_dict(self, params_dict): + """Set the parameters of the FitRecipe from a dictionary of + parameter names and values.""" + for param_name, param_value in params_dict.items(): + if param_name in self._parameters: + self._parameters[param_name].setValue(param_value) + else: + print( + f"Warning: Parameter '{param_name}' from results " + "not found in FitRecipe and will be ignored." + ) + + def initialize_recipe_with_results(self, results, verbose=True): + """Initialize a FitRecipe with a FitResults object or a results + file. + + Note that at least one FitContribution must already exist in + the FitRecipe. + + Parameters + ---------- + results : FitResults, pathlib.Path, or str + The FitResults object or path to results file to initialize with. + verbose : bool, optional + If True, print warnings for any parameters in the results that are + not in the FitRecipe. Default is True. + + Raises + ------ + ValueError + If the input results is not a FitResults object or a path to a + results file. + """ + if hasattr(results, "get_results_dictionary"): + params_dict = results.get_results_dictionary() + metrics_in_dict = [ + "Residual", + "Contributions", + "Restraints", + "Chi2", + "Reduced Chi2", + "Rw", + ] + for metric in metrics_in_dict: + params_dict.pop(metric, None) + elif isinstance(results, (str, Path)): + params_dict = utils.get_dict_from_results_file(results) + else: + raise ValueError( + "The input results must be a FitResults object or a path to a " + f"results file, but got {type(results)}." + ) + self._set_parameters_from_dict(params_dict) + if verbose: + print() + print("Parameters found in Results:") + print("=" * 30) + self._pretty_print_results_dict(params_dict) + print() + print("Parameters set in FitRecipe:") + print("=" * 30) + set_parameters_dict = { + param.name: param.getValue() + for param in self._parameters.values() + } + self._pretty_print_results_dict(set_parameters_dict) + def set_plot_defaults(self, **kwargs): """Set default plotting options for all future plots. diff --git a/src/diffpy/srfit/fitbase/fitresults.py b/src/diffpy/srfit/fitbase/fitresults.py index c154d018..d8310109 100644 --- a/src/diffpy/srfit/fitbase/fitresults.py +++ b/src/diffpy/srfit/fitbase/fitresults.py @@ -66,64 +66,94 @@ new_base="diffpy.srfit.fitbase.FitResults", ) +initializeRecipe_dep_msg = build_deprecation_message( + "diffpy.srfit.fitbase", + "initializeRecipe", + "initialize_recipe_with_results", + removal_version, + new_base="diffpy.srfit.fitbase.FitRecipe", +) + class FitResults(object): """Class for processing, presenting and storing results of a fit. Attributes ---------- - recipe - The recipe containing the results. - cov - The covariance matrix from the recipe. - conresults - An ordered dictionary of ContributionResults for each - FitContribution, indexed by the FitContribution name. - derivstep - The fractional step size for calculating numeric - derivatives. Default 1e-8. - varnames - Names of the variables in the recipe. - varvals - Values of the variables in the recipe. - varunc - Uncertainties in the variable values. - showfixed - Show fixed variables (default True). - fixednames - Names of the fixed variables of the recipe. - fixedvals - Values of the fixed variables of the recipe. - showcon - Show constraint values in the output (default False). - connames - Names of the constrained parameters. - convals - Values of the constrained parameters. - conunc - Uncertainties in the constraint values. - residual - The scalar residual of the recipe. - penalty - The penalty to residual from the restraints. - chi2 - The chi2 of the recipe. - cumchi2 - The cumulative chi2 of the recipe. - rchi2 - The reduced chi2 of the recipe. - rw - The Rw of the recipe. - cumrw - The cumulative Rw of the recipe. - messages - A list of messages about the results. - precision - The precision of numeric output (default 8). - _dcon - The derivatives of the constraint equations with respect to - the variables. This is used internally. + recipe : FitRecipe + The recipe from which the results were generated. + + cov : numpy.ndarray or None + The covariance matrix of the refined variables. None if unavailable. + + conresults : collections.OrderedDict[str, ContributionResults] + The ordered mapping of FitContribution name → ContributionResults. + + derivstep : float + The fractional step size used for numerical derivatives (default 1e-8). + + varnames : list[str] + The names of refined variables in the recipe. + + varvals : numpy.ndarray + The optimized values of the refined variables. + + varunc : numpy.ndarray or None + The estimated standard uncertainties of the variables. None if invalid. + + showfixed : bool + Show the fixed variables in the formatted output + (default True). + + fixednames : list[str] + The names of variables held fixed during refinement. + + fixedvals : numpy.ndarray + The values of the fixed variables. + + showcon : bool + show the constrained parameters in the formatted output + (default False). + + connames : list[str] + The names of constrained parameters. + + convals : numpy.ndarray + The values of constrained parameters. + + conunc : numpy.ndarray or None + The uncertainties of constrained parameters. None if unavailable. + + residual : float + The scalar residual value of the recipe. + penalty : float + The penalty contribution to the residual from restraints. + + chi2 : float + The chi-squared value of the fit. + + cumchi2 : numpy.ndarray + The cumulative chi-squared as a function of data index. + + rchi2 : float + The reduced chi-squared of the fit. + + rw : float + The weighted R-factor of the fit. + + cumrw : numpy.ndarray + The cumulative weighted R-factor as a function of data index. + + messages : list[str] + The informational or warning messages associated with the results. + + precision : int + The number of digits used when formatting numeric output (default 8). + + _dcon : numpy.ndarray + The jacobian of constraint equations with respect to variables. + Used internally for uncertainty propagation. Each of these attributes, except the recipe, are created or updated when the update method is called. @@ -134,14 +164,14 @@ def __init__(self, recipe, update=True, showfixed=True, showcon=False): Attributes ---------- - recipe - The recipe containing the results - update - Flag indicating whether to do an immediate update (default - True). - showcon + recipe : FitRecipe + The recipe containing the results. + update : bool + The flag indicating whether to do an immediate update + (default True). + showfixed : bool Show fixed variables in the output (default True). - showcon + showcon : bool Show constraint values in the output (default False). """ self.recipe = recipe @@ -568,11 +598,11 @@ def print_results(self, header="", footer="", update=False): Parameters ---------- header - A header to add to the output (default "") + The header to add to the output (default "") footer - A footer to add to the output (default "") + The footer to add to the output (default "") update - Flag indicating whether to call update() (default False). + The flag indicating whether to call update() (default False). """ print(self.get_results_string(header, footer, update).rstrip()) return @@ -597,13 +627,13 @@ def save_results(self, filename, header="", footer="", update=False): Parameters ---------------------------------- filename - Name of the save file. + The name of the save file. header - A header to add to the output (default "") + The header to add to the output (default "") footer - A footer to add to the output (default "") + The footer to add to the output (default "") update - Flag indicating whether to call update() (default False). + The flag indicating whether to call update() (default False). """ # Save the time and user from getpass import getuser @@ -637,8 +667,8 @@ def get_results_dictionary(self): Returns ------- results_dict : dict - A dictionary containing the variable names and values, and overall - metrics, from the FitResults. + The dictionary containing the variable names and values, + and overall metrics, from the FitResults. """ parameter_names = self.varnames parameter_values = self.varvals @@ -666,38 +696,38 @@ class ContributionResults(object): Attributes ---------- - y + y : numpy.ndarray or None The FitContribution's profile over the calculation range (default None). - dy + dy : numpy.ndarray or None The uncertainty in the FitContribution's profile over the calculation range (default None). - x - A numpy array of the calculated independent variable for the + x : numpy.ndarray or None + The numpy array of the calculated independent variable for the FitContribution (default None). - ycalc - A numpy array of the calculated signal for the FitContribution + ycalc : numpy.ndarray or None + The numpy array of the calculated signal for the FitContribution (default None). - residual + residual : float The scalar residual of the FitContribution. - chi2 + chi2 : float The chi2 of the FitContribution. - cumchi2 + cumchi2 : numpy.ndarray The cumulative chi2 of the FitContribution. - rw + rw : float The Rw of the FitContribution. - cumrw + cumrw : numpy.ndarray The cumulative Rw of the FitContribution. - weight + weight : float The weight of the FitContribution in the recipe. - conlocs + conlocs : list The location of the constrained parameters in the FitContribution (see the RecipeContainer._locate_managed_object method). - convals - Values of the constrained parameters. - conunc - Uncertainties in the constraint values. + convals : list + The values of the constrained parameters. + conunc : list + The uncertainties in the constraint values. """ def __init__(self, con, weight, fitres): @@ -820,8 +850,16 @@ def resultsDictionary(results): return mpairs +@deprecated(initializeRecipe_dep_msg) def initializeRecipe(recipe, results): - """Initialize the variables of a recipe from a results file. + """**This function has been deprecated and will be** **removed in + version 4.0.0.** + + **Please use** + **diffpy.srfit.fitbase.FitRecipe.initialize_recipe_with_results** + **instead.** + + Initialize the variables of a recipe from a results file. This reads the results from file and initializes any variables (fixed or free) in the recipe to the results values. Note that the recipe has to be diff --git a/src/diffpy/srfit/util/inpututils.py b/src/diffpy/srfit/util/inpututils.py index b5dfa2ac..f2e0a9df 100644 --- a/src/diffpy/srfit/util/inpututils.py +++ b/src/diffpy/srfit/util/inpututils.py @@ -17,6 +17,7 @@ __all__ = ["inputToString"] import os.path +from pathlib import Path def inputToString(input): @@ -51,4 +52,44 @@ def inputToString(input): return inptstr +def get_dict_from_results_file( + results_filepath: Path | str, +) -> dict[str, float]: + """Get a dictionary of parameter names and values from a results + file. + + The file should have lines in the format: + "parameter_name value +/- uncertainty". Lines that do not match this + format will be ignored. + + Parameters + ---------- + results_filepath : pathlib.Path or str + The path to the results file. + + Returns + ------- + parsed_results_dict : dict + The dictionary where keys are parameter names and values are the + corresponding parameter values as floats. + """ + with open(results_filepath, "r") as f: + results_string = f.read() + parsed_results_dict = {} + for raw_line in results_string.splitlines(): + line = raw_line.strip() + # skip blank lines and lines that are just dashes + if not line or set(line) == {"-"}: + continue + line_items = line.split() + if len(line_items) < 2: + continue + if len(line_items) >= 4 and line_items[2] == "+/-": + try: + parsed_results_dict[line_items[0]] = float(line_items[1]) + except ValueError: + pass + return parsed_results_dict + + # End of file diff --git a/tests/conftest.py b/tests/conftest.py index 45c6308a..9b7a99fc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -150,19 +150,23 @@ def _capturestdout(f, *args, **kwargs): @pytest.fixture() def build_recipe_one_contribution(): "helper to build a simple recipe" - profile = Profile() - x = linspace(0, pi, 10) - y = sin(x) - profile.set_observed_profile(x, y) - contribution = FitContribution("c1") - contribution.set_profile(profile) - contribution.set_equation("amplitude*sin(wave_number*x + phase_shift)") - recipe = FitRecipe() - recipe.add_contribution(contribution) - recipe.add_variable(contribution.amplitude, 1) - recipe.add_variable(contribution.wave_number, 1) - recipe.add_variable(contribution.phase_shift, 1) - return recipe + + def _build_recipe(): + profile = Profile() + x = linspace(0, pi, 11) + y = sin(x) + profile.set_observed_profile(x, y) + contribution = FitContribution("c1") + contribution.set_profile(profile) + contribution.set_equation("amplitude*sin(wave_number*x + phase_shift)") + recipe = FitRecipe() + recipe.add_contribution(contribution) + recipe.add_variable(contribution.amplitude, 4) + recipe.add_variable(contribution.wave_number, 3) + recipe.add_variable(contribution.phase_shift, 2) + return recipe + + return _build_recipe @pytest.fixture() @@ -170,7 +174,7 @@ def build_recipe_two_contributions(): """Helper to build a recipe with two physically related contributions.""" profile1 = Profile() - x = linspace(0, pi, 50) + x = linspace(0, pi, 51) y1 = sin(x) # amplitude=1, freq=1 profile1.set_observed_profile(x, y1) diff --git a/tests/test_fitrecipe.py b/tests/test_fitrecipe.py index 6deb1cad..21ce1113 100644 --- a/tests/test_fitrecipe.py +++ b/tests/test_fitrecipe.py @@ -527,11 +527,115 @@ def test_initialize_recipe_from_recipe_bad(build_recipe_two_contributions): recipe2.initialize_recipe_with_recipe(recipe_bad) -# def test_initialize_recipe_from_results(build_recipe_one_contribution): -# # Case: User initializes a FitRecipe from a FitResults object or -# # results file -# # expected: recipe is initialized with variables from previous fit -# assert False +def test_initialize_recipe_from_results_object(build_recipe_one_contribution): + # Case: User initializes a FitRecipe from a FitResults object + # expected: recipe is initialized with variables from previous fit + recipe1 = build_recipe_one_contribution() + optimize_recipe(recipe1) + results1 = FitResults(recipe1) + expected_values = np.round(results1.varvals, 5) + expected_names = results1.varnames + + recipe2 = build_recipe_one_contribution() + recipe2.create_new_variable( + "extra_var", 5 + ) # should be included in the initialized recipe + actual_values_before_init = [val for val in recipe2.get_values()] + actual_names_before_init = recipe2.get_names() + expected_names_before_init = [ + "amplitude", + "extra_var", + "phase_shift", + "wave_number", + ] + expected_values_before_init = [ + 4, + 3, + 2, + 5, + ] # the three variables + the extra_var + + assert actual_values_before_init == expected_values_before_init + assert sorted(actual_names_before_init) == sorted( + expected_names_before_init + ) + + recipe2.initialize_recipe_with_results(results1) + optimize_recipe(recipe2) + results2 = FitResults(recipe2) + actual_values = np.round(results2.varvals, 5) + actual_names = results2.varnames + + expected_names = expected_names + [ + "extra_var" + ] # add the new variable name to expected names + expected_values = list(expected_values) + [ + 5 + ] # add the value of the new variable to expected values + assert sorted(expected_names) == sorted(actual_names) + assert sorted(expected_values) == sorted(list(actual_values)) + + +def test_initialize_recipe_from_results_file( + build_recipe_one_contribution, temp_data_files +): + # Case: User initializes a FitRecipe from a FitResults file + # expected: recipe is initialized with variables from previous fit + results_file = temp_data_files / "fit_results.res" + expected_names = ["amplitude", "phase_shift", "wave_number"] + expected_values = [1, 1, 0] + + recipe = build_recipe_one_contribution() + recipe.initialize_recipe_with_results(results_file) + results = FitResults(recipe) + actual_values = np.round(results.varvals, 5) + actual_names = results.varnames + + assert sorted(expected_names) == sorted(actual_names) + assert list(expected_values) == list(actual_values) + + +def test_initialize_recipe_from_results_file_bad( + build_recipe_one_contribution, +): + # Case: User tries to initialize a recipe with something that + # isn't a path, str, or FitResults object + # Expected: raised ValueError with message + recipe = build_recipe_one_contribution() + bad_input = 12345 # not a valid input type + msg = ( + "The input results must be a FitResults object or a path to a " + "results file, but got ." + ) + with pytest.raises(ValueError, match=msg): + recipe.initialize_recipe_with_results(bad_input) + + +def test_initialize_recipe_from_results_file_wrong( + build_recipe_two_contributions, temp_data_files, capsys +): + # Case: User tries to initialize a FitRecipe from a results file + # that does not match params in the recipe + # expected: Warning message is printed and things proceed as + # usual with the variables in the recipe + + results_file_from_single_contrib = temp_data_files / "fit_results.res" + recipe = build_recipe_two_contributions + recipe.initialize_recipe_with_results(results_file_from_single_contrib) + captured = capsys.readouterr() + actual_print_msg = captured.out # .strip() + + results_file_param_names = ["amplitude", "phase_shift", "wave_number"] + expected_print_messages = [] + for param_name in results_file_param_names: + msg = ( + f"Warning: Parameter '{param_name}' from results not found " + "in FitRecipe and will be ignored." + ) + expected_print_messages.append(msg) + + for expected_print_msg in expected_print_messages: + assert expected_print_msg in actual_print_msg def get_labels_and_linecount(ax): @@ -591,7 +695,7 @@ def build_recipe_from_datafile_deprecated(datafile): def test_plot_recipe_bad_display(build_recipe_one_contribution): - recipe = build_recipe_one_contribution + recipe = build_recipe_one_contribution() # Case: All plots are disabled # expected: raised ValueError with message plt.close("all") @@ -621,7 +725,7 @@ def test_plot_recipe_before_refinement(capsys, build_recipe_one_contribution): # Case: User tries to plot recipe before refinement # expected: Data plotted without fit line or difference curve # and warning message printed - recipe = build_recipe_one_contribution + recipe = build_recipe_one_contribution() plt.close("all") before = set(plt.get_fignums()) # include fit_label="nothing" to make sure fit line is not plotted @@ -649,7 +753,7 @@ def test_plot_recipe_before_refinement(capsys, build_recipe_one_contribution): def test_plot_recipe_after_refinement(build_recipe_one_contribution): # Case: User refines recipe and then plots # expected: Plot generates with no problem - recipe = build_recipe_one_contribution + recipe = build_recipe_one_contribution() optimize_recipe(recipe) plt.close("all") before = set(plt.get_fignums()) @@ -686,7 +790,7 @@ def test_plot_recipe_two_contributions(build_recipe_two_contributions): def test_plot_recipe_on_existing_plot(build_recipe_one_contribution): # Case: User passes axes to plot_recipe to plot on existing figure # expected: User modifications are present in the final figure - recipe = build_recipe_one_contribution + recipe = build_recipe_one_contribution() optimize_recipe(recipe) plt.close("all") fig, ax = plt.subplots() @@ -706,7 +810,7 @@ def test_plot_recipe_on_existing_plot(build_recipe_one_contribution): def test_plot_recipe_add_new_data(build_recipe_one_contribution): # Case: User wants to add data to figure generated by plot_recipe # Expected: New data is added to existing figure (check with labels) - recipe = build_recipe_one_contribution + recipe = build_recipe_one_contribution() optimize_recipe(recipe) plt.close("all") before = set(plt.get_fignums()) @@ -750,7 +854,7 @@ def test_plot_recipe_add_new_data_two_figs(build_recipe_two_contributions): def test_plot_recipe_set_title(build_recipe_one_contribution): # Case: User sets title via plot_recipe # Expected: Title is set correctly - recipe = build_recipe_one_contribution + recipe = build_recipe_one_contribution() optimize_recipe(recipe) plt.close("all") expected_title = "Custom Recipe Title" @@ -764,7 +868,7 @@ def test_plot_recipe_set_title(build_recipe_one_contribution): def test_plot_recipe_set_defaults(build_recipe_one_contribution): # Case: user sets default plot options with set_plot_defaults # Expected: plot_recipe uses the default options for all calls - recipe = build_recipe_one_contribution + recipe = build_recipe_one_contribution() optimize_recipe(recipe) plt.close("all") # set new defaults @@ -792,7 +896,7 @@ def test_plot_recipe_set_defaults(build_recipe_one_contribution): def test_plot_recipe_set_defaults_bad(capsys, build_recipe_one_contribution): # Case: user tries to set kwargs that are not valid plot_recipe options # Expected: Plot is shown and warning is printed - recipe = build_recipe_one_contribution + recipe = build_recipe_one_contribution() optimize_recipe(recipe) plt.close("all") recipe.set_plot_defaults( @@ -902,7 +1006,7 @@ def test_plot_recipe_reset_all_defaults(build_recipe_one_contribution): "show": True, } - recipe = build_recipe_one_contribution + recipe = build_recipe_one_contribution() optimize_recipe(recipe) plt.close("all") diff --git a/tests/test_fitresults.py b/tests/test_fitresults.py index b9fb7492..c8797233 100644 --- a/tests/test_fitresults.py +++ b/tests/test_fitresults.py @@ -38,7 +38,7 @@ Restraints 0.00000000 Chi2 0.00000000 Reduced Chi2 0.00000000 -Rw 0.00000000 +Rw 0.00000010 Variables (Uncertainties invalid) ------------------------------------------------------------------------------ @@ -54,9 +54,10 @@ def optimize_recipe(recipe): def test_formatResults(build_recipe_one_contribution): - recipe = build_recipe_one_contribution + recipe = build_recipe_one_contribution() optimize_recipe(recipe) results = FitResults(recipe) + results.save_results("res.res") actual_results_string = results.formatResults(header="My Custom header") # Because slight variations in refinement, just check # that the header of the results are the same. @@ -67,7 +68,7 @@ def test_formatResults(build_recipe_one_contribution): def test_get_results_string(build_recipe_one_contribution): - recipe = build_recipe_one_contribution + recipe = build_recipe_one_contribution() optimize_recipe(recipe) results = FitResults(recipe) actual_results_string = results.get_results_string( @@ -82,7 +83,7 @@ def test_get_results_string(build_recipe_one_contribution): def test_printResults(build_recipe_one_contribution, capsys): - recipe = build_recipe_one_contribution + recipe = build_recipe_one_contribution() optimize_recipe(recipe) results = FitResults(recipe) results.printResults(header="My Custom header") @@ -96,7 +97,7 @@ def test_printResults(build_recipe_one_contribution, capsys): def test_print_results(build_recipe_one_contribution, capsys): - recipe = build_recipe_one_contribution + recipe = build_recipe_one_contribution() optimize_recipe(recipe) results = FitResults(recipe) results.print_results(header="My Custom header") @@ -110,7 +111,7 @@ def test_print_results(build_recipe_one_contribution, capsys): def test_saveResults(build_recipe_one_contribution, tmp_path): - recipe = build_recipe_one_contribution + recipe = build_recipe_one_contribution() optimize_recipe(recipe) results = FitResults(recipe) actual_results_file = tmp_path / "fit_results.txt" @@ -127,7 +128,7 @@ def test_saveResults(build_recipe_one_contribution, tmp_path): def test_save_results(build_recipe_one_contribution, tmp_path): - recipe = build_recipe_one_contribution + recipe = build_recipe_one_contribution() optimize_recipe(recipe) results = FitResults(recipe) actual_results_file = tmp_path / "fit_results.txt" @@ -146,7 +147,7 @@ def test_save_results(build_recipe_one_contribution, tmp_path): def test_get_results_dictionary(build_recipe_one_contribution): # Case: user gets results dictionary after optimization # expected: results dictionary contains expected keys and values - recipe = build_recipe_one_contribution + recipe = build_recipe_one_contribution() optimize_recipe(recipe) results = FitResults(recipe) actual_results_dict = results.get_results_dictionary()