Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions news/init-w-recipe.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
**Added:**

* Added initialize_recipe_from_recipe to ``FitRecipe``.

**Changed:**

* <news item>

**Deprecated:**

* <news item>

**Removed:**

* <news item>

**Fixed:**

* <news item>

**Security:**

* <news item>
44 changes: 44 additions & 0 deletions src/diffpy/srfit/fitbase/fitrecipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Contributor Author

@cadenmyers13 cadenmyers13 Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used .add_contribution() since it actually takes a contribution object.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's what I am talkin' about. Much better than parsing a results file string.


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)

Copy link
Contributor Author

@cadenmyers13 cadenmyers13 Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to use .add() and .update() here because the .restrain(), .constrain(), and .create_new_parameter() methods dont accept the respective objects

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

def set_plot_defaults(self, **kwargs):
"""Set default plotting options for all future plots.

Expand Down
37 changes: 25 additions & 12 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -164,32 +164,45 @@ 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"
"""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)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improved this so we can actually test for constrain and restrain

return recipe

Expand Down
72 changes: 72 additions & 0 deletions tests/test_fitrecipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -462,6 +464,76 @@ 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_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 <class 'int'>."
)
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
# # 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 = [
Expand Down
Loading