From a133fb8505e39c17c9340e9666f2583edd6fc1f7 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Sun, 19 Apr 2026 16:41:16 -0400 Subject: [PATCH] fix: change logic of iterPars method with same type atoms to have same ADPs --- news/iteratepars-behavior.rst | 23 +++++++ src/diffpy/srfit/fitbase/recipeorganizer.py | 54 ++++++++++++--- tests/test_pdf.py | 74 +++++++++++++++++++++ 3 files changed, 143 insertions(+), 8 deletions(-) create mode 100644 news/iteratepars-behavior.rst diff --git a/news/iteratepars-behavior.rst b/news/iteratepars-behavior.rst new file mode 100644 index 00000000..556a91c6 --- /dev/null +++ b/news/iteratepars-behavior.rst @@ -0,0 +1,23 @@ +**Added:** + +* + +**Changed:** + +* Changed `iterPars` method to match all equal-type atoms to have same ADPs + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/srfit/fitbase/recipeorganizer.py b/src/diffpy/srfit/fitbase/recipeorganizer.py index 3b95a216..07df90e7 100644 --- a/src/diffpy/srfit/fitbase/recipeorganizer.py +++ b/src/diffpy/srfit/fitbase/recipeorganizer.py @@ -116,32 +116,70 @@ def _iter_managed(self): """Get iterator over managed objects.""" return chain(*(d.values() for d in self.__managed)) - def iterPars(self, pattern="", recurse=True): + def iterPars(self, pattern="", recurse=True, fullnames=False): """Iterate over the Parameters contained in this object. Parameters ---------- pattern : str - Iterate over parameters with names matching this regular - expression (all parameters by default). + Iterate over parameters with names matching this regular expression + (all parameters by default). + + When `fullnames` is True, the regular expression is matched against + dotted parameter names relative to this object, e.g. ``Ni0.Biso``. recurse : bool Recurse into managed objects when True (default). + fullnames : bool + Match against hierarchical dotted names relative to this object + when True. Match only leaf parameter names when False (default). """ regexp = re.compile(pattern) + if not fullnames: + for par in list(self._parameters.values()): + if regexp.search(par.name): + yield par + if not recurse: + return + managed = self.__managed[:] + managed.remove(self._parameters) + for m in managed: + for obj in m.values(): + if hasattr(obj, "iterPars"): + for par in obj.iterPars(pattern=pattern, recurse=True): + yield par + return + for par in self._iterpars_fullnames( + regexp, recurse=recurse, prefix="" + ): + yield par + + def _iterpars_fullnames(self, regexp, recurse=True, prefix=""): + """Internal helper for iterPars(fullnames=True).""" for par in list(self._parameters.values()): - if regexp.search(par.name): + name = f"{prefix}{par.name}" + if regexp.search(name): yield par + if not recurse: return - # Iterate over objects within the managed dictionaries. + managed = self.__managed[:] managed.remove(self._parameters) for m in managed: for obj in m.values(): - if hasattr(obj, "iterPars"): - for par in obj.iterPars(pattern=pattern): + if hasattr(obj, "_iterpars_fullnames"): + childprefix = f"{prefix}{obj.name}." + for par in obj._iterpars_fullnames( + regexp, + recurse=True, + prefix=childprefix, + ): + yield par + elif hasattr(obj, "iterPars"): + for par in obj.iterPars( + pattern=regexp.pattern, recurse=True + ): yield par - return def __iter__(self): """Iterate over top-level parameters.""" diff --git a/tests/test_pdf.py b/tests/test_pdf.py index dbb9799f..60d094f8 100644 --- a/tests/test_pdf.py +++ b/tests/test_pdf.py @@ -23,6 +23,8 @@ import pytest from diffpy.srfit.exceptions import SrFitError +from diffpy.srfit.fitbase.parameter import Parameter +from diffpy.srfit.fitbase.recipeorganizer import RecipeContainer from diffpy.srfit.pdf import PDFContribution, PDFGenerator, PDFParser # ---------------------------------------------------------------------------- @@ -302,3 +304,75 @@ def test_pickling(diffpy_srreal_available, datafile): if __name__ == "__main__": unittest.main() + + +def _make_iterpars_tree(): + """Build a small hierarchy for iterPars tests.""" + root = RecipeContainer("root") + root._containers = {} + root._manage(root._containers) + + root_biso = Parameter("Biso", 10) + root._add_object(root_biso, root._parameters) + + ni0 = RecipeContainer("Ni0") + ni0_biso = Parameter("Biso", 20) + ni0_uiso = Parameter("Uiso", 30) + ni0._add_object(ni0_biso, ni0._parameters) + ni0._add_object(ni0_uiso, ni0._parameters) + + ni1 = RecipeContainer("Ni1") + ni1_biso = Parameter("Biso", 40) + ni1._add_object(ni1_biso, ni1._parameters) + + o0 = RecipeContainer("O0") + o0_biso = Parameter("Biso", 50) + o0._add_object(o0_biso, o0._parameters) + + root._add_object(ni0, root._containers) + root._add_object(ni1, root._containers) + root._add_object(o0, root._containers) + + return { + "root": root, + "root_biso": root_biso, + "ni0": ni0, + "ni0_biso": ni0_biso, + "ni0_uiso": ni0_uiso, + "ni1": ni1, + "ni1_biso": ni1_biso, + "o0": o0, + "o0_biso": o0_biso, + } + + +@pytest.mark.parametrize( + ("pattern", "kwargs", "expected_values"), + [ + (r"^Biso$", {}, [10, 20, 40, 50]), + (r"^Ni\d+\.Biso$", {}, []), + (r"^Ni\d+\.Biso$", {"fullnames": True}, [20, 40]), + (r"^Ni0\.Uiso$", {"fullnames": True}, [30]), + (r"^O0\.Biso$", {"fullnames": True}, [50]), + (r"^Ni\d+\.Biso$", {"fullnames": True, "recurse": False}, []), + (r"^Biso$", {"fullnames": True, "recurse": False}, [10]), + ], +) +def test_iterpars_fullname_matching(pattern, kwargs, expected_values): + """Verify leaf-name and fullname matching in iterPars.""" + objs = _make_iterpars_tree() + root = objs["root"] + + values = [par.value for par in root.iterPars(pattern, **kwargs)] + + assert values == expected_values + + +def test_iterpars_fullnames_are_relative_to_called_container(): + """Verify fullname matching is relative to the called container.""" + objs = _make_iterpars_tree() + ni0 = objs["ni0"] + + assert list(ni0.iterPars(r"^Biso$", fullnames=True)) == [objs["ni0_biso"]] + assert list(ni0.iterPars(r"^Uiso$", fullnames=True)) == [objs["ni0_uiso"]] + assert list(ni0.iterPars(r"^Ni0\.Biso$", fullnames=True)) == []