From 001addefb75c6adc1e6ba097072c58862a65b105 Mon Sep 17 00:00:00 2001 From: Caden Myers Date: Tue, 27 Oct 2026 14:54:37 -0400 Subject: [PATCH 1/3] feat: expose ReflectionProfileDoubleExponentialPseudoVoigt TOF profile Bind the double-exponential pseudo-Voigt profile in reflectionprofile_ext.cpp so TOF Rietveld refinement no longer needs an XMLInput workaround. PowderPatternDiffraction.SetProfile now gives this profile the component crystal's unit cell, as ObjCryst++ does in SetCrystal and XMLInput, since it needs d_hkl. Co-Authored-By: Claude Opus 5.5 --- news/tof-profile.rst | 23 ++++ .../powderpatterndiffraction_ext.cpp | 9 +- src/extensions/reflectionprofile_ext.cpp | 24 ++++ src/pyobjcryst/reflectionprofile.py | 7 ++ tests/test_reflectionprofile.py | 114 ++++++++++++++++++ 5 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 news/tof-profile.rst diff --git a/news/tof-profile.rst b/news/tof-profile.rst new file mode 100644 index 0000000..ad9d2c6 --- /dev/null +++ b/news/tof-profile.rst @@ -0,0 +1,23 @@ +**Added:** + +* Expose the time-of-flight reflection profile ReflectionProfileDoubleExponentialPseudoVoigt. + +**Changed:** + +* PowderPatternDiffraction.SetProfile gives a ReflectionProfileDoubleExponentialPseudoVoigt the unit cell of the component's crystal. + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/extensions/powderpatterndiffraction_ext.cpp b/src/extensions/powderpatterndiffraction_ext.cpp index d504e99..bcdc09a 100644 --- a/src/extensions/powderpatterndiffraction_ext.cpp +++ b/src/extensions/powderpatterndiffraction_ext.cpp @@ -41,7 +41,14 @@ namespace void SetProfileCopy(PowderPatternDiffraction &pdiff, const ReflectionProfile &profile) { - pdiff.SetProfile(profile.CreateCopy()); + ReflectionProfile *copy = profile.CreateCopy(); + // The TOF profile needs a unit cell to compute d_hkl, as in + // PowderPatternDiffraction::SetCrystal and XMLInput. + ReflectionProfileDoubleExponentialPseudoVoigt *tofprofile = + dynamic_cast(copy); + if (tofprofile != 0 && pdiff.HasCrystal()) + tofprofile->SetUnitCell(pdiff.GetCrystal()); + pdiff.SetProfile(copy); } // PowderPatternDiffraction has no public accessor for its texture diff --git a/src/extensions/reflectionprofile_ext.cpp b/src/extensions/reflectionprofile_ext.cpp index 64abcee..13639cf 100644 --- a/src/extensions/reflectionprofile_ext.cpp +++ b/src/extensions/reflectionprofile_ext.cpp @@ -159,6 +159,30 @@ void wrap_reflectionprofile() bp::arg("scherrerLGmix") = 1), "Set the TCH pseudo-Voigt U, V, W, X, Y, Z, P and LGmix parameters."); + class_ >( + "ReflectionProfileDoubleExponentialPseudoVoigt", init<>()) + .def(init(bp::arg("cell")) + [with_custodian_and_ward<1, 2>()]) + .def(init( + bp::arg("old"))) + .def("CreateCopy", + &ReflectionProfileDoubleExponentialPseudoVoigt::CreateCopy, + return_value_policy(), + "Return an independent copy of this profile.") + .def("SetProfilePar", + &ReflectionProfileDoubleExponentialPseudoVoigt::SetProfilePar, + (bp::arg("instrumentAlpha0"), bp::arg("instrumentAlpha1"), + bp::arg("instrumentBeta0"), bp::arg("instrumentBeta1"), + bp::arg("gaussianSigma0"), bp::arg("gaussianSigma1"), + bp::arg("gaussianSigma2"), bp::arg("lorentzianGamma0"), + bp::arg("lorentzianGamma1"), bp::arg("lorentzianGamma2")), + "Set the double-exponential pseudo-Voigt TOF profile parameters.") + .def("SetUnitCell", + &ReflectionProfileDoubleExponentialPseudoVoigt::SetUnitCell, + bp::arg("cell"), with_custodian_and_ward<1, 2>(), + "Set the unit cell used to compute d_hkl for reflections."); + class_ >( "ReflectionProfilePseudoVoigtAnisotropic", init<>()) diff --git a/src/pyobjcryst/reflectionprofile.py b/src/pyobjcryst/reflectionprofile.py index 2bb16f3..a9f69fe 100644 --- a/src/pyobjcryst/reflectionprofile.py +++ b/src/pyobjcryst/reflectionprofile.py @@ -36,6 +36,11 @@ for pdiff in powder_pattern.get_crystalline_components(): pdiff.SetProfile(profile) +For time-of-flight neutron data, use the double-exponential pseudo-Voigt +profile ``ReflectionProfileDoubleExponentialPseudoVoigt``. It needs the +unit cell to compute ``d_hkl``, which ``PowderPatternDiffraction.SetProfile`` +sets from the component's crystal. + Example ------- @@ -69,11 +74,13 @@ "ReflectionProfilePseudoVoigt", "ReflectionProfilePseudoVoigtTCH", "ReflectionProfilePseudoVoigtAnisotropic", + "ReflectionProfileDoubleExponentialPseudoVoigt", "ReflectionProfileType", ] from pyobjcryst._pyobjcryst import ( ReflectionProfile, + ReflectionProfileDoubleExponentialPseudoVoigt, ReflectionProfilePseudoVoigt, ReflectionProfilePseudoVoigtAnisotropic, ReflectionProfilePseudoVoigtTCH, diff --git a/tests/test_reflectionprofile.py b/tests/test_reflectionprofile.py index e0a8678..227bd92 100644 --- a/tests/test_reflectionprofile.py +++ b/tests/test_reflectionprofile.py @@ -6,9 +6,11 @@ import pytest from pyobjcryst.powderpattern import PowderPattern +from pyobjcryst.radiation import RadiationType, WavelengthType from pyobjcryst.refinableobj import RefinableObj from pyobjcryst.reflectionprofile import ( ReflectionProfile, + ReflectionProfileDoubleExponentialPseudoVoigt, ReflectionProfilePseudoVoigt, ReflectionProfilePseudoVoigtAnisotropic, ReflectionProfilePseudoVoigtTCH, @@ -293,5 +295,117 @@ def test_xml_output(self): self.assertTrue(xml_from_stream.startswith(" Date: Tue, 3 Nov 2026 08:51:59 +0100 Subject: [PATCH 2/3] fix: keep source profile alive for copied TOF profiles Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/extensions/reflectionprofile_ext.cpp | 5 +++-- tests/test_reflectionprofile.py | 25 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/extensions/reflectionprofile_ext.cpp b/src/extensions/reflectionprofile_ext.cpp index 13639cf..acbfb94 100644 --- a/src/extensions/reflectionprofile_ext.cpp +++ b/src/extensions/reflectionprofile_ext.cpp @@ -165,10 +165,11 @@ void wrap_reflectionprofile() .def(init(bp::arg("cell")) [with_custodian_and_ward<1, 2>()]) .def(init( - bp::arg("old"))) + bp::arg("old"))[with_custodian_and_ward<1, 2>()]) .def("CreateCopy", &ReflectionProfileDoubleExponentialPseudoVoigt::CreateCopy, - return_value_policy(), + return_value_policy >(), "Return an independent copy of this profile.") .def("SetProfilePar", &ReflectionProfileDoubleExponentialPseudoVoigt::SetProfilePar, diff --git a/tests/test_reflectionprofile.py b/tests/test_reflectionprofile.py index 227bd92..7db3481 100644 --- a/tests/test_reflectionprofile.py +++ b/tests/test_reflectionprofile.py @@ -1,6 +1,8 @@ """Unit tests for pyobjcryst.reflectionprofile bindings.""" +import gc import unittest +import weakref import numpy as np import pytest @@ -407,5 +409,28 @@ def test_get_powder_pattern_calc_after_tof_profile_change( assert np.allclose(actual_calc, expected_calc) +@pytest.mark.parametrize("copy_method", ["CreateCopy", "copy_constructor"]) +def test_tof_profile_copy_keeps_original_alive(loadcifdata, copy_method): + # Test that a copy of a TOF profile keeps its source and unit cell alive + # Expected: The original profile and crystal survive until the copy is + # deleted, since both profiles reference the same unit cell. + crystal = loadcifdata("Si.cif") + original = ReflectionProfileDoubleExponentialPseudoVoigt(crystal) + if copy_method == "CreateCopy": + copied = original.CreateCopy() + else: + copied = ReflectionProfileDoubleExponentialPseudoVoigt(original) + original_ref = weakref.ref(original) + crystal_ref = weakref.ref(crystal) + del original, crystal + gc.collect() + assert original_ref() is not None + assert crystal_ref() is not None + del copied + gc.collect() + assert original_ref() is None + assert crystal_ref() is None + + if __name__ == "__main__": unittest.main() From 72bda18cfd778998d219288fb87bd1df9afc7972 Mon Sep 17 00:00:00 2001 From: Vincent Favre-Nicolin Date: Tue, 3 Nov 2026 09:15:22 +0100 Subject: [PATCH 3/3] test: avoid nested pytest.approx in test_add_phase Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tests/test_powderpattern.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_powderpattern.py b/tests/test_powderpattern.py index 02ba692..697de67 100644 --- a/tests/test_powderpattern.py +++ b/tests/test_powderpattern.py @@ -483,7 +483,7 @@ def test_add_phase(loadcifdata, phases, expected_par_names): texture.GetPar(i).GetName() for i in range(texture.GetNbPar()) ] assert actual_type == expected_type - assert actual_phases == pytest.approx(expected_phases) + assert actual_phases == [pytest.approx(e) for e in expected_phases] assert actual_par_names == expected_par_names