From bc39b5ace58c0989cf6b74f6023be9e7a2e6a267 Mon Sep 17 00:00:00 2001 From: Caden Myers Date: Wed, 25 Feb 2026 10:12:50 -0500 Subject: [PATCH 1/6] set fixtures to scope=function --- tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 9e5332bd..6c396963 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -146,7 +146,7 @@ def _capturestdout(f, *args, **kwargs): return _capturestdout -@pytest.fixture(scope="session") +@pytest.fixture() def build_recipe_one_contribution(): "helper to build a simple recipe" profile = Profile() @@ -164,7 +164,7 @@ def build_recipe_one_contribution(): return recipe -@pytest.fixture(scope="session") +@pytest.fixture() def build_recipe_two_contributions(): "helper to build a recipe with two contributions" profile1 = Profile() From 1bb14ecd9ab933701fa835fadfddea43e68ae517 Mon Sep 17 00:00:00 2001 From: Caden Myers Date: Wed, 25 Feb 2026 11:47:23 -0500 Subject: [PATCH 2/6] add constraints and restraints for more strict testing --- tests/conftest.py | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 6c396963..0417ad17 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -166,30 +166,43 @@ def build_recipe_one_contribution(): @pytest.fixture() def build_recipe_two_contributions(): - "helper to build a recipe with two contributions" + """Helper to build a recipe with two physically related contributions.""" profile1 = Profile() - x = linspace(0, pi, 10) - y1 = sin(x) + x = linspace(0, pi, 50) + y1 = sin(x) # amplitude=1, freq=1 profile1.set_observed_profile(x, y1) + contribution1 = FitContribution("c1") contribution1.set_profile(profile1) contribution1.set_equation("A*sin(k*x + c)") profile2 = Profile() - y2 = 0.5 * sin(2 * x) + y2 = 0.5 * sin(2 * x) # amplitude=0.5, freq=2 profile2.set_observed_profile(x, y2) + contribution2 = FitContribution("c2") contribution2.set_profile(profile2) contribution2.set_equation("B*sin(m*x + d)") + recipe = FitRecipe() recipe.add_contribution(contribution1) recipe.add_contribution(contribution2) - recipe.add_variable(contribution1.A, 1) - recipe.add_variable(contribution1.k, 1) - recipe.add_variable(contribution1.c, 1) - recipe.add_variable(contribution2.B, 0.5) - recipe.add_variable(contribution2.m, 2) - recipe.add_variable(contribution2.d, 0) + + # Add variables with reasonable initial guesses + recipe.add_variable(contribution1.A, 0.8) + recipe.add_variable(contribution1.k, 1.0) + recipe.add_variable(contribution1.c, 0.1) + + recipe.add_variable(contribution2.B, 0.4) + recipe.add_variable(contribution2.m, 2.0) + recipe.add_variable(contribution2.d, 0.1) + + # ---- Meaningful constraints ---- + recipe.constrain(contribution2.m, "2*k") + recipe.constrain(contribution2.d, contribution1.c) + recipe.constrain(contribution2.B, "0.5*A") + recipe.restrain(contribution1.A, 0.5, 1.5) + recipe.restrain(contribution1.k, 0.8, 1.2) return recipe From acfb6741deaefb5eb3362689151d193eeebdb9c7 Mon Sep 17 00:00:00 2001 From: Caden Myers Date: Wed, 25 Feb 2026 11:49:03 -0500 Subject: [PATCH 3/6] add initialization test and method --- src/diffpy/srfit/fitbase/fitrecipe.py | 44 +++++++++++++++++++ src/diffpy/srfit/fitbase/fitresults.py | 7 +++ tests/test_fitrecipe.py | 59 ++++++++++++++++++++++++++ 3 files changed, 110 insertions(+) diff --git a/src/diffpy/srfit/fitbase/fitrecipe.py b/src/diffpy/srfit/fitbase/fitrecipe.py index c28acc3e..894dbc18 100644 --- a/src/diffpy/srfit/fitbase/fitrecipe.py +++ b/src/diffpy/srfit/fitbase/fitrecipe.py @@ -1140,6 +1140,50 @@ def getBounds2(self): """ return self.get_bounds_array() + def initialize_recipe_with_recipe(self, recipe_object): + """Initialize a FitRecipe with another FitRecipe. + + This is used to initialize a FitRecipe with the contribution(s), + parameters, constraints and restraints of another FitRecipe. + If a duplicate contribution, parameter, constraint, or restraint + is added to the FitRecipe you are initializing, the value from the + added object will be used. + + Parameters + ---------- + recipe_object : FitRecipe + The FitRecipe to initialize with. + + Raises + ------ + ValueError + If the object passed is not a FitRecipe. + """ + if not isinstance(recipe_object, FitRecipe): + raise ValueError( + "The input recipe_object must be a FitRecipe, " + f"but got {type(recipe_object)}" + ) + + for contrib_object in recipe_object._contributions.values(): + if contrib_object not in self._contributions.values(): + self.add_contribution(contrib_object) + + for param_name, param_object in recipe_object._parameters.items(): + if param_name not in self._parameters: + self._parameters.update({param_name: param_object}) + + for ( + parameter_object, + constraint_object, + ) in recipe_object._constraints.items(): + if parameter_object not in self._constraints: + self._constraints.update({parameter_object: constraint_object}) + + for restraint in recipe_object._restraints: + if restraint not in self._restraints: + self._restraints.add(restraint) + def 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 dabebded..163bb18b 100644 --- a/src/diffpy/srfit/fitbase/fitresults.py +++ b/src/diffpy/srfit/fitbase/fitresults.py @@ -57,6 +57,13 @@ removal_version, ) +saveResults_dep_msg = build_deprecation_message( + fitresults_base, + "saveResults", + "save_results", + removal_version, +) + class FitResults(object): """Class for processing, presenting and storing results of a fit. diff --git a/tests/test_fitrecipe.py b/tests/test_fitrecipe.py index 48e0bf15..eeb70e34 100644 --- a/tests/test_fitrecipe.py +++ b/tests/test_fitrecipe.py @@ -18,10 +18,12 @@ import matplotlib import matplotlib.pyplot as plt +import numpy as np import pytest from numpy import array_equal, dot, linspace, pi, sin from scipy.optimize import leastsq +from diffpy.srfit.fitbase import FitResults from diffpy.srfit.fitbase.fitcontribution import FitContribution from diffpy.srfit.fitbase.fitrecipe import FitRecipe from diffpy.srfit.fitbase.parameter import Parameter @@ -462,6 +464,63 @@ def optimize_recipe(recipe): leastsq(residuals, values) +def test_initialize_recipe_from_recipe(build_recipe_two_contributions): + # Case: User initializes a FitRecipe from a previously optimized fit + # expected: recipe is initialized with everything: + # contributions, profiles (contained in contributions), + # variables, restraints, and constraints + recipe1 = build_recipe_two_contributions + optimize_recipe(recipe1) + expected_parameters_dict = recipe1._parameters + expected_constraints_dict = recipe1._constraints + expected_restraints_set = recipe1._restraints + expected_contributions_dict = recipe1._contributions + expected_profiles_list = [] + for con_name, contribution in expected_contributions_dict.items(): + expected_profile = contribution.profile + expected_profiles_list.append(expected_profile) + + recipe2 = FitRecipe() + recipe2.initialize_recipe_with_recipe(recipe1) + actual_parameters_dict = recipe2._parameters + actual_constraints_dict = recipe2._constraints + actual_restraints_set = recipe2._restraints + actual_contributions_dict = recipe2._contributions + actual_profiles_list = [] + for con_name, contribution in actual_contributions_dict.items(): + actual_profile = contribution.profile + actual_profiles_list.append(actual_profile) + + assert expected_parameters_dict == actual_parameters_dict + assert expected_constraints_dict == actual_constraints_dict + assert expected_restraints_set == actual_restraints_set + assert expected_contributions_dict == actual_contributions_dict + assert expected_profiles_list == actual_profiles_list + + # Check to see if the refined values and variable names are + # the same in the results objects for each recipe + results1 = FitResults(recipe1) + # round to account for small numerical differences + expected_values = np.round(results1.varvals, 7) + expected_names = results1.varnames + + optimize_recipe(recipe2) + results2 = FitResults(recipe2) + # round to account for small numerical differences + actual_values = np.round(results2.varvals, 7) + actual_names = results2.varnames + + assert sorted(expected_names) == sorted(actual_names) + assert sorted(list(expected_values)) == sorted(list(actual_values)) + + +# 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 get_labels_and_linecount(ax): """Helper to get line labels and count from a matplotlib Axes.""" labels = [ From a934f424b1c5db0afd398649dfecff5968877d20 Mon Sep 17 00:00:00 2001 From: Caden Myers Date: Wed, 25 Feb 2026 11:50:10 -0500 Subject: [PATCH 4/6] news --- news/init-w-recipe.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 news/init-w-recipe.rst diff --git a/news/init-w-recipe.rst b/news/init-w-recipe.rst new file mode 100644 index 00000000..073926e7 --- /dev/null +++ b/news/init-w-recipe.rst @@ -0,0 +1,23 @@ +**Added:** + +* Added initialize_recipe_from_recipe to ``FitRecipe``. + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* From 9c9fdc343fab0560e408250e040e7e73b43c4df4 Mon Sep 17 00:00:00 2001 From: Caden Myers Date: Wed, 25 Feb 2026 11:51:46 -0500 Subject: [PATCH 5/6] remove accidental commit --- src/diffpy/srfit/fitbase/fitresults.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/diffpy/srfit/fitbase/fitresults.py b/src/diffpy/srfit/fitbase/fitresults.py index 163bb18b..dabebded 100644 --- a/src/diffpy/srfit/fitbase/fitresults.py +++ b/src/diffpy/srfit/fitbase/fitresults.py @@ -57,13 +57,6 @@ removal_version, ) -saveResults_dep_msg = build_deprecation_message( - fitresults_base, - "saveResults", - "save_results", - removal_version, -) - class FitResults(object): """Class for processing, presenting and storing results of a fit. From 4b60d8a4a46ef7511e44d0bc9c4ae78818f8c3c8 Mon Sep 17 00:00:00 2001 From: Caden Myers Date: Wed, 25 Feb 2026 11:57:35 -0500 Subject: [PATCH 6/6] add test for ValueError --- src/diffpy/srfit/fitbase/fitrecipe.py | 2 +- tests/test_fitrecipe.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/diffpy/srfit/fitbase/fitrecipe.py b/src/diffpy/srfit/fitbase/fitrecipe.py index 894dbc18..72946e40 100644 --- a/src/diffpy/srfit/fitbase/fitrecipe.py +++ b/src/diffpy/srfit/fitbase/fitrecipe.py @@ -1162,7 +1162,7 @@ def initialize_recipe_with_recipe(self, recipe_object): if not isinstance(recipe_object, FitRecipe): raise ValueError( "The input recipe_object must be a FitRecipe, " - f"but got {type(recipe_object)}" + f"but got {type(recipe_object)}." ) for contrib_object in recipe_object._contributions.values(): diff --git a/tests/test_fitrecipe.py b/tests/test_fitrecipe.py index eeb70e34..39af10e8 100644 --- a/tests/test_fitrecipe.py +++ b/tests/test_fitrecipe.py @@ -514,6 +514,19 @@ def test_initialize_recipe_from_recipe(build_recipe_two_contributions): assert sorted(list(expected_values)) == sorted(list(actual_values)) +def test_initialize_recipe_from_recipe_bad(build_recipe_two_contributions): + # Case: User tries to initialize a FitRecipe from a non recipe object + # expected: raised ValueError with message + recipe_bad = 12345 # not a FitRecipe object + recipe2 = FitRecipe() + msg = ( + "The input recipe_object must be a FitRecipe, " + "but got ." + ) + with pytest.raises(ValueError, match=msg): + 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