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:** + +* diff --git a/src/extensions/reflectionprofile_ext.cpp b/src/extensions/reflectionprofile_ext.cpp index e72bc26..64abcee 100644 --- a/src/extensions/reflectionprofile_ext.cpp +++ b/src/extensions/reflectionprofile_ext.cpp @@ -139,6 +139,26 @@ 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", + (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, + bp::arg("fwhmScherrerP") = 0, + bp::arg("scherrerLGmix") = 1), + "Set the TCH pseudo-Voigt U, V, W, X, Y, Z, P and LGmix parameters."); + class_ >( "ReflectionProfilePseudoVoigtAnisotropic", init<>()) diff --git a/src/pyobjcryst/reflectionprofile.py b/src/pyobjcryst/reflectionprofile.py index c88f4b6..2bb16f3 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", ] @@ -75,5 +76,6 @@ ReflectionProfile, ReflectionProfilePseudoVoigt, ReflectionProfilePseudoVoigtAnisotropic, + ReflectionProfilePseudoVoigtTCH, ReflectionProfileType, ) diff --git a/tests/test_reflectionprofile.py b/tests/test_reflectionprofile.py index c304e44..e0a8678 100644 --- a/tests/test_reflectionprofile.py +++ b/tests/test_reflectionprofile.py @@ -11,6 +11,7 @@ ReflectionProfile, ReflectionProfilePseudoVoigt, ReflectionProfilePseudoVoigtAnisotropic, + ReflectionProfilePseudoVoigtTCH, ) @@ -73,6 +74,73 @@ 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", "P", "LGmix"}, + {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 + ) + + 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("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("LGmix").GetValue(), 0) + def test_anisotropic_set_profile_par(self): """SetProfilePar assigns widths and retains symmetric default asymmetry."""