Skip to content

Commit 2caac08

Browse files
authored
Merge pull request #183 from stevenhua0320/iterpar-behavior
fix: fix iterpars behavior
2 parents edb4388 + 0b16bfd commit 2caac08

3 files changed

Lines changed: 199 additions & 12 deletions

File tree

news/iterpar-behavior.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
**Added:**
2+
3+
* <news item>
4+
5+
**Changed:**
6+
7+
* Changed `iterPars` method to match all equal-type atoms to have same ADPs
8+
9+
**Deprecated:**
10+
11+
* <news item>
12+
13+
**Removed:**
14+
15+
* <news item>
16+
17+
**Fixed:**
18+
19+
* <news item>
20+
21+
**Security:**
22+
23+
* <news item>

src/diffpy/srfit/fitbase/recipeorganizer.py

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,9 @@ def _iter_managed(self):
240240
"""Get iterator over managed objects."""
241241
return chain(*(d.values() for d in self.__managed))
242242

243-
def iterate_over_parameters(self, pattern="", recurse=True):
243+
def iterate_over_parameters(
244+
self, pattern="", recurse=True, fullnames=False
245+
):
244246
"""Iterate over the Parameters contained in this object.
245247
246248
Parameters
@@ -256,6 +258,10 @@ def iterate_over_parameters(self, pattern="", recurse=True):
256258
(default), the method will also iterate over
257259
parameters in managed sub-objects. If False, only
258260
top-level parameters will be iterated over.
261+
fullnames : bool, optional
262+
The flag indicating whether to match against hierarchical
263+
dotted namesrelative to this object.
264+
If False (default), match only leaf parameter names.
259265
260266
Example
261267
-------
@@ -266,22 +272,67 @@ def iterate_over_parameters(self, pattern="", recurse=True):
266272
print(f"{param.name}={param.value}")
267273
"""
268274
regexp = re.compile(pattern)
275+
if not fullnames:
276+
yield from self._iterpars_leafnames(regexp, pattern, recurse)
277+
else:
278+
yield from self._iterpars_fullnames(
279+
regexp, recurse=recurse, prefix=""
280+
)
281+
282+
def _iter_local_parameters(self, regexp, prefix=""):
283+
"""Iterate over local Parameters with matching names."""
269284
for parameter in list(self._parameters.values()):
270-
if regexp.search(parameter.name):
285+
name = f"{prefix}{parameter.name}"
286+
if regexp.search(name):
271287
yield parameter
272-
if not recurse:
273-
return
274-
# Iterate over objects within the managed dictionaries.
288+
289+
def _iter_managed_parameter_containers(self):
290+
"""Iterate over managed objects that can iterate over
291+
Parameters."""
275292
managed = self.__managed[:]
276293
managed.remove(self._parameters)
277-
for m in managed:
278-
for obj in m.values():
294+
for managed_dict in managed:
295+
for obj in managed_dict.values():
279296
if hasattr(obj, "iterate_over_parameters"):
280-
for parameter in obj.iterate_over_parameters(
281-
pattern=pattern
282-
):
283-
yield parameter
284-
return
297+
yield obj
298+
299+
def _iterpars_leafnames(self, regexp, pattern, recurse=True):
300+
"""Iterate over Parameters matched by leaf parameter names."""
301+
yield from self._iter_local_parameters(regexp)
302+
if recurse:
303+
for obj in self._iter_managed_parameter_containers():
304+
yield from obj.iterate_over_parameters(
305+
pattern=pattern,
306+
recurse=True,
307+
)
308+
309+
def _iter_child_fullname_parameters(self, obj, regexp, prefix):
310+
"""Iterate over one child's Parameters with hierarchical
311+
names."""
312+
if hasattr(obj, "_iterpars_fullnames"):
313+
childprefix = f"{prefix}{obj.name}."
314+
yield from obj._iterpars_fullnames(
315+
regexp,
316+
recurse=True,
317+
prefix=childprefix,
318+
)
319+
else:
320+
yield from obj.iterate_over_parameters(
321+
pattern=regexp.pattern,
322+
recurse=True,
323+
)
324+
325+
def _iterpars_fullnames(self, regexp, recurse=True, prefix=""):
326+
"""Iterate over Parameters matched by hierarchical dotted
327+
names."""
328+
yield from self._iter_local_parameters(regexp, prefix=prefix)
329+
if recurse:
330+
for obj in self._iter_managed_parameter_containers():
331+
yield from self._iter_child_fullname_parameters(
332+
obj,
333+
regexp,
334+
prefix,
335+
)
285336

286337
@deprecated(iterPars_deprecation_msg)
287338
def iterPars(self, pattern="", recurse=True):

tests/test_pdf.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424

2525
from diffpy.srfit.exceptions import SrFitError
2626
from diffpy.srfit.fitbase import ProfileParser
27+
from diffpy.srfit.fitbase.parameter import Parameter
28+
from diffpy.srfit.fitbase.recipeorganizer import RecipeContainer
2729
from diffpy.srfit.pdf import PDFContribution, PDFGenerator, PDFParser
2830

2931
# ----------------------------------------------------------------------------
@@ -321,3 +323,114 @@ def test_pickling(
321323

322324
if __name__ == "__main__":
323325
unittest.main()
326+
327+
328+
def _make_iterpars_tree():
329+
"""Build a small hierarchy for iterPars tests."""
330+
root = RecipeContainer("root")
331+
root._containers = {}
332+
root._manage(root._containers)
333+
334+
root_biso = Parameter("Biso", 10)
335+
root._add_object(root_biso, root._parameters)
336+
337+
ni0 = RecipeContainer("Ni0")
338+
ni0_biso = Parameter("Biso", 20)
339+
ni0_uiso = Parameter("Uiso", 30)
340+
ni0._add_object(ni0_biso, ni0._parameters)
341+
ni0._add_object(ni0_uiso, ni0._parameters)
342+
343+
ni1 = RecipeContainer("Ni1")
344+
ni1_biso = Parameter("Biso", 40)
345+
ni1._add_object(ni1_biso, ni1._parameters)
346+
347+
o0 = RecipeContainer("O0")
348+
o0_biso = Parameter("Biso", 50)
349+
o0._add_object(o0_biso, o0._parameters)
350+
351+
root._add_object(ni0, root._containers)
352+
root._add_object(ni1, root._containers)
353+
root._add_object(o0, root._containers)
354+
355+
return {
356+
"root": root,
357+
"root_biso": root_biso,
358+
"ni0": ni0,
359+
"ni0_biso": ni0_biso,
360+
"ni0_uiso": ni0_uiso,
361+
"ni1": ni1,
362+
"ni1_biso": ni1_biso,
363+
"o0": o0,
364+
"o0_biso": o0_biso,
365+
}
366+
367+
368+
@pytest.mark.parametrize(
369+
("pattern", "kwargs", "expected_values"),
370+
[
371+
# C1: Match leaf parameter names without fullnames.
372+
# Expected: all Biso parameters in the hierarchy are returned.
373+
(r"^Biso$", {}, [10, 20, 40, 50]),
374+
# C2: Match hierarchical names without fullnames.
375+
# Expected: no leaf names match the hierarchical pattern.
376+
(r"^Ni\d+\.Biso$", {}, []),
377+
# C3: Match hierarchical names with fullnames enabled.
378+
# Expected: matching Ni Biso parameters are returned.
379+
(r"^Ni\d+\.Biso$", {"fullnames": True}, [20, 40]),
380+
# C4: Match one hierarchical Uiso name.
381+
# Expected: only Ni0.Uiso is returned.
382+
(r"^Ni0\.Uiso$", {"fullnames": True}, [30]),
383+
# C5: Match one hierarchical Biso name outside Ni containers.
384+
# Expected: only O0.Biso is returned.
385+
(r"^O0\.Biso$", {"fullnames": True}, [50]),
386+
# C6: Disable recursion while matching child fullnames.
387+
# Expected: no child parameters are returned.
388+
(r"^Ni\d+\.Biso$", {"fullnames": True, "recurse": False}, []),
389+
# C7: Disable recursion while matching root fullname.
390+
# Expected: only the root-level Biso parameter is returned.
391+
(r"^Biso$", {"fullnames": True, "recurse": False}, [10]),
392+
],
393+
)
394+
def test_iterpars_fullname_matching(pattern, kwargs, expected_values):
395+
"""Verify leaf-name and fullname matching in
396+
iterate_over_parameters."""
397+
objs = _make_iterpars_tree()
398+
root = objs["root"]
399+
400+
actual_values = [
401+
parameter.value
402+
for parameter in root.iterate_over_parameters(pattern, **kwargs)
403+
]
404+
405+
assert actual_values == expected_values
406+
407+
408+
@pytest.mark.parametrize(
409+
("pattern", "expected_name"),
410+
[
411+
# C1: Match Biso relative to the called Ni0 container.
412+
# Expected: Ni0.Biso is returned without the Ni0 prefix.
413+
(r"^Biso$", ["Biso"]),
414+
# C2: Match Uiso relative to the called Ni0 container.
415+
# Expected: Ni0.Uiso is returned without the Ni0 prefix.
416+
(r"^Uiso$", ["Uiso"]),
417+
# C3: Match with the parent container prefix from inside Ni0.
418+
# Expected: no parameter is returned because fullnames are relative
419+
# to the container on which iterate_over_parameters is called.
420+
(r"^Ni0\.Biso$", []),
421+
],
422+
)
423+
def test_iterpars_fullnames_are_relative_to_called_container(
424+
pattern,
425+
expected_name,
426+
):
427+
"""Verify fullname matching is relative to the called container."""
428+
objs = _make_iterpars_tree()
429+
ni0 = objs["ni0"]
430+
431+
actual_name = [
432+
parameter.name
433+
for parameter in ni0.iterate_over_parameters(pattern, fullnames=True)
434+
]
435+
436+
assert actual_name == expected_name

0 commit comments

Comments
 (0)