From 838d72009591faa70911bf21aea724e306abcf5d Mon Sep 17 00:00:00 2001 From: Clemens Schmid Date: Thu, 27 Aug 2026 15:47:02 +0200 Subject: [PATCH 1/4] add new PV profile ReflectionProfilePseudoVoigtTCH with TCH instead of explicit Eta mixing --- src/extensions/reflectionprofile_ext.cpp | 14 +++++++ src/pyobjcryst/reflectionprofile.py | 10 +++-- tests/test_reflectionprofile.py | 51 ++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 4 deletions(-) diff --git a/src/extensions/reflectionprofile_ext.cpp b/src/extensions/reflectionprofile_ext.cpp index e72bc26..6c40d5d 100644 --- a/src/extensions/reflectionprofile_ext.cpp +++ b/src/extensions/reflectionprofile_ext.cpp @@ -139,6 +139,20 @@ void wrap_reflectionprofile() bp::arg("eta1") = 0), "Set the isotropic pseudo-Voigt profile parameters."); + class_ >( + "ReflectionProfilePseudoVoigtTCH", init<>()) + .def(init(bp::arg("old"))) + .def("CreateCopy", &ReflectionProfilePseudoVoigtTCH::CreateCopy, + return_value_policy(), + "Return an independent copy of this profile.") + .def("SetProfilePar", &ReflectionProfilePseudoVoigtTCH::SetProfilePar, + (bp::arg("fwhmCagliotiW"), bp::arg("fwhmCagliotiU") = 0, + bp::arg("fwhmCagliotiV") = 0, + bp::arg("fwhmLorentzX") = 0, + bp::arg("fwhmLorentzY") = 0, + bp::arg("fwhmLorentzZ") = 0), + "Set the TCH pseudo-Voigt U, V, W, X, Y and Z parameters."); + class_ >( "ReflectionProfilePseudoVoigtAnisotropic", init<>()) diff --git a/src/pyobjcryst/reflectionprofile.py b/src/pyobjcryst/reflectionprofile.py index c88f4b6..44bc474 100644 --- a/src/pyobjcryst/reflectionprofile.py +++ b/src/pyobjcryst/reflectionprofile.py @@ -20,10 +20,10 @@ ``XMLOutput`` / ``XMLInput`` and ``CreateCopy``. ``GetProfile`` accepts a Python sequence or numpy array for ``x``. -Concrete isotropic and anisotropic pseudo-Voigt profiles can be installed -with ``PowderPatternDiffraction.SetProfile``. The diffraction component -stores an independent copy, so one configured profile can be reused as a -template for multiple phases:: +Concrete empirical isotropic, TCH isotropic, and anisotropic pseudo-Voigt +profiles can be installed with ``PowderPatternDiffraction.SetProfile``. The +diffraction component stores an independent copy, so one configured profile +can be reused as a template for multiple phases:: from pyobjcryst.reflectionprofile import ( ReflectionProfilePseudoVoigtAnisotropic, @@ -67,6 +67,7 @@ __all__ = [ "ReflectionProfile", "ReflectionProfilePseudoVoigt", + "ReflectionProfilePseudoVoigtTCH", "ReflectionProfilePseudoVoigtAnisotropic", "ReflectionProfileType", ] @@ -74,6 +75,7 @@ from pyobjcryst._pyobjcryst import ( ReflectionProfile, ReflectionProfilePseudoVoigt, + ReflectionProfilePseudoVoigtTCH, ReflectionProfilePseudoVoigtAnisotropic, ReflectionProfileType, ) diff --git a/tests/test_reflectionprofile.py b/tests/test_reflectionprofile.py index c304e44..542845c 100644 --- a/tests/test_reflectionprofile.py +++ b/tests/test_reflectionprofile.py @@ -10,6 +10,7 @@ from pyobjcryst.reflectionprofile import ( ReflectionProfile, ReflectionProfilePseudoVoigt, + ReflectionProfilePseudoVoigtTCH, ReflectionProfilePseudoVoigtAnisotropic, ) @@ -73,6 +74,56 @@ def test_concrete_pseudo_voigt_profiles(self): }, ) + def test_tch_profile_width_and_limits(self): + """TCH derives one common FWHM and reaches both pure limits.""" + profile = ReflectionProfilePseudoVoigtTCH() + self.assertFalse(profile.IsAnisotropic()) + self.assertEqual( + {"U", "V", "W", "X", "Y", "Z"}, + { + profile.GetPar(i).GetName() + for i in range(profile.GetNbPar()) + }, + ) + + center = np.deg2rad(30.0) + hg = np.deg2rad(0.08) + hl = np.deg2rad(0.04) + profile.SetProfilePar(hg**2, fwhmLorentzZ=hl) + h = ( + hg**5 + + 2.69269 * hg**4 * hl + + 2.42843 * hg**3 * hl**2 + + 4.47163 * hg**2 * hl**3 + + 0.07842 * hg * hl**4 + + hl**5 + ) ** 0.2 + self.assertAlmostEqual( + profile.GetFullProfileWidth(0.5, center, 1, 0, 0), h + ) + + x = np.array([center - h / 2, center, center + h / 2]) + y = profile.GetProfile(x, center, 1, 0, 0) + np.testing.assert_allclose(y[[0, 2]] / y[1], 0.5, rtol=1e-6) + + profile.SetProfilePar(hg**2) + self.assertAlmostEqual( + profile.GetFullProfileWidth(0.5, center, 1, 0, 0), hg + ) + profile.SetProfilePar(0, fwhmLorentzZ=hl) + self.assertAlmostEqual( + profile.GetFullProfileWidth(0.5, center, 1, 0, 0), hl + ) + + self.ppd.SetProfile(profile) + installed = self.ppd.GetProfile() + self.assertIsInstance(installed, ReflectionProfilePseudoVoigtTCH) + self.assertAlmostEqual(installed.GetPar("Z").GetValue(), hl) + + restored = ReflectionProfilePseudoVoigtTCH() + RefinableObj.XMLInput(restored, profile.xml()) + self.assertAlmostEqual(restored.GetPar("Z").GetValue(), hl) + def test_anisotropic_set_profile_par(self): """SetProfilePar assigns widths and retains symmetric default asymmetry.""" From 2671f7a5ca6598df8091a46afe94097871f9861a Mon Sep 17 00:00:00 2001 From: Clemens Schmid Date: Fri, 28 Aug 2026 13:56:00 +0200 Subject: [PATCH 2/4] Add Scherrer P peak broadening and LGmix like GSAS-II in ReflectionProfilePseudoVoigtTCH by default, P=0 and LGmix=1 LGmix=0 means Gaussian, LGmix=1 means Lorentzian broadening --- src/extensions/reflectionprofile_ext.cpp | 12 +++++++++--- tests/test_reflectionprofile.py | 22 +++++++++++++++++++--- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/extensions/reflectionprofile_ext.cpp b/src/extensions/reflectionprofile_ext.cpp index 6c40d5d..64abcee 100644 --- a/src/extensions/reflectionprofile_ext.cpp +++ b/src/extensions/reflectionprofile_ext.cpp @@ -145,13 +145,19 @@ void wrap_reflectionprofile() .def("CreateCopy", &ReflectionProfilePseudoVoigtTCH::CreateCopy, return_value_policy(), "Return an independent copy of this profile.") - .def("SetProfilePar", &ReflectionProfilePseudoVoigtTCH::SetProfilePar, + .def("SetProfilePar", + (void (ReflectionProfilePseudoVoigtTCH::*)( + const REAL, const REAL, const REAL, const REAL, + const REAL, const REAL, const REAL, const REAL)) + &ReflectionProfilePseudoVoigtTCH::SetProfilePar, (bp::arg("fwhmCagliotiW"), bp::arg("fwhmCagliotiU") = 0, bp::arg("fwhmCagliotiV") = 0, bp::arg("fwhmLorentzX") = 0, bp::arg("fwhmLorentzY") = 0, - bp::arg("fwhmLorentzZ") = 0), - "Set the TCH pseudo-Voigt U, V, W, X, Y and Z parameters."); + bp::arg("fwhmLorentzZ") = 0, + bp::arg("fwhmScherrerP") = 0, + bp::arg("scherrerLGmix") = 1), + "Set the TCH pseudo-Voigt U, V, W, X, Y, Z, P and LGmix parameters."); class_ >( diff --git a/tests/test_reflectionprofile.py b/tests/test_reflectionprofile.py index 542845c..fb4c716 100644 --- a/tests/test_reflectionprofile.py +++ b/tests/test_reflectionprofile.py @@ -79,7 +79,7 @@ def test_tch_profile_width_and_limits(self): profile = ReflectionProfilePseudoVoigtTCH() self.assertFalse(profile.IsAnisotropic()) self.assertEqual( - {"U", "V", "W", "X", "Y", "Z"}, + {"U", "V", "W", "X", "Y", "Z", "P", "LGmix"}, { profile.GetPar(i).GetName() for i in range(profile.GetNbPar()) @@ -115,14 +115,30 @@ def test_tch_profile_width_and_limits(self): profile.GetFullProfileWidth(0.5, center, 1, 0, 0), hl ) + size_width = np.deg2rad(0.03) / np.cos(center / 2) + profile.SetProfilePar( + 0, fwhmScherrerP=np.deg2rad(0.03), scherrerLGmix=1 + ) + self.assertAlmostEqual( + profile.GetFullProfileWidth(0.5, center, 1, 0, 0), size_width + ) + profile.SetProfilePar( + 0, fwhmScherrerP=np.deg2rad(0.03), scherrerLGmix=0 + ) + self.assertAlmostEqual( + profile.GetFullProfileWidth(0.5, center, 1, 0, 0), size_width + ) + self.ppd.SetProfile(profile) installed = self.ppd.GetProfile() self.assertIsInstance(installed, ReflectionProfilePseudoVoigtTCH) - self.assertAlmostEqual(installed.GetPar("Z").GetValue(), hl) + self.assertAlmostEqual(installed.GetPar("P").GetValue(), np.deg2rad(0.03)) + self.assertAlmostEqual(installed.GetPar("LGmix").GetValue(), 0) restored = ReflectionProfilePseudoVoigtTCH() RefinableObj.XMLInput(restored, profile.xml()) - self.assertAlmostEqual(restored.GetPar("Z").GetValue(), hl) + self.assertAlmostEqual(restored.GetPar("P").GetValue(), np.deg2rad(0.03)) + self.assertAlmostEqual(restored.GetPar("LGmix").GetValue(), 0) def test_anisotropic_set_profile_par(self): """SetProfilePar assigns widths and retains symmetric default From d2ccff3ec887375d71d00f3b8323a9e4c7b5a579 Mon Sep 17 00:00:00 2001 From: Clemens Schmid Date: Mon, 31 Aug 2026 13:49:34 +0200 Subject: [PATCH 3/4] add news file --- news/tch-pv-profile.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 news/tch-pv-profile.rst diff --git a/news/tch-pv-profile.rst b/news/tch-pv-profile.rst new file mode 100644 index 0000000..4c1d9d4 --- /dev/null +++ b/news/tch-pv-profile.rst @@ -0,0 +1,23 @@ +**Added:** + +* Expose reflection profile ReflectionProfilePseudoVoigtTCH + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* From 6fa3ae0087d1f54fa3b1efc203188657cdea5f4b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 11:53:41 +0000 Subject: [PATCH 4/4] [pre-commit.ci] auto fixes from pre-commit hooks --- src/pyobjcryst/reflectionprofile.py | 2 +- tests/test_reflectionprofile.py | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/pyobjcryst/reflectionprofile.py b/src/pyobjcryst/reflectionprofile.py index 44bc474..2bb16f3 100644 --- a/src/pyobjcryst/reflectionprofile.py +++ b/src/pyobjcryst/reflectionprofile.py @@ -75,7 +75,7 @@ from pyobjcryst._pyobjcryst import ( ReflectionProfile, ReflectionProfilePseudoVoigt, - ReflectionProfilePseudoVoigtTCH, ReflectionProfilePseudoVoigtAnisotropic, + ReflectionProfilePseudoVoigtTCH, ReflectionProfileType, ) diff --git a/tests/test_reflectionprofile.py b/tests/test_reflectionprofile.py index fb4c716..e0a8678 100644 --- a/tests/test_reflectionprofile.py +++ b/tests/test_reflectionprofile.py @@ -10,8 +10,8 @@ from pyobjcryst.reflectionprofile import ( ReflectionProfile, ReflectionProfilePseudoVoigt, - ReflectionProfilePseudoVoigtTCH, ReflectionProfilePseudoVoigtAnisotropic, + ReflectionProfilePseudoVoigtTCH, ) @@ -80,10 +80,7 @@ def test_tch_profile_width_and_limits(self): self.assertFalse(profile.IsAnisotropic()) self.assertEqual( {"U", "V", "W", "X", "Y", "Z", "P", "LGmix"}, - { - profile.GetPar(i).GetName() - for i in range(profile.GetNbPar()) - }, + {profile.GetPar(i).GetName() for i in range(profile.GetNbPar())}, ) center = np.deg2rad(30.0) @@ -132,12 +129,16 @@ def test_tch_profile_width_and_limits(self): self.ppd.SetProfile(profile) installed = self.ppd.GetProfile() self.assertIsInstance(installed, ReflectionProfilePseudoVoigtTCH) - self.assertAlmostEqual(installed.GetPar("P").GetValue(), np.deg2rad(0.03)) + self.assertAlmostEqual( + installed.GetPar("P").GetValue(), np.deg2rad(0.03) + ) self.assertAlmostEqual(installed.GetPar("LGmix").GetValue(), 0) restored = ReflectionProfilePseudoVoigtTCH() RefinableObj.XMLInput(restored, profile.xml()) - self.assertAlmostEqual(restored.GetPar("P").GetValue(), np.deg2rad(0.03)) + self.assertAlmostEqual( + restored.GetPar("P").GetValue(), np.deg2rad(0.03) + ) self.assertAlmostEqual(restored.GetPar("LGmix").GetValue(), 0) def test_anisotropic_set_profile_par(self):