diff --git a/news/d-tth.rst b/news/d-tth.rst new file mode 100644 index 00000000..2bfc091f --- /dev/null +++ b/news/d-tth.rst @@ -0,0 +1,23 @@ +**Added:** + +* functions to convert between d and tth + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/utils/transforms.py b/src/diffpy/utils/transforms.py index 0d02fe54..c150b2a4 100644 --- a/src/diffpy/utils/transforms.py +++ b/src/diffpy/utils/transforms.py @@ -12,12 +12,12 @@ "and number is the wavelength in angstroms." ) invalid_tth_emsg = "Two theta exceeds 180 degrees. Please check the input values for errors." -invalid_q_or_wavelength_emsg = ( - "The supplied q-array and wavelength will result in an impossible two-theta. " +invalid_q_or_d_or_wavelength_emsg = ( + "The supplied input array and wavelength will result in an impossible two-theta. " "Please check these values and re-instantiate the DiffractionObject with correct values." ) inf_output_wmsg = ( - "INFO: The largest d-value in the array is infinite. This is allowed, but it will not be plotted." + "INFO: The largest output value in the array is infinite. This is allowed, but it will not be plotted." ) @@ -27,7 +27,7 @@ def _validate_inputs(q, wavelength): return np.empty(0) pre_factor = wavelength / (4 * np.pi) if np.any(np.abs(q * pre_factor) > 1.0): - raise ValueError(invalid_q_or_wavelength_emsg) + raise ValueError(invalid_q_or_d_or_wavelength_emsg) def q_to_tth(q, wavelength): @@ -141,9 +141,38 @@ def q_to_d(q): return 2.0 * np.pi / copy(q) -def tth_to_d(ttharray, wavelength): - qarray = tth_to_q(ttharray, wavelength) - return 2.0 * np.pi / copy(qarray) +def tth_to_d(tth, wavelength): + r""" + Helper function to convert two-theta to d on independent variable axis. + + The formula is .. math:: d = \frac{\lambda}{2 \sin\left(\frac{2\theta}{2}\right)}. + + Here we convert tth to q first, then to d. + + Parameters + ---------- + tth : 1D array + The array of :math:`2\theta` values np.array([tths]). + The units of tth are expected in degrees. + + wavelength : float + Wavelength of the incoming x-rays/neutrons/electrons + + Returns + ------- + d : 1D array + The array of :math:`d` values np.array([ds]). + """ + q = tth_to_q(tth, wavelength) + d = copy(tth) + if wavelength is None: + warnings.warn(wavelength_warning_emsg, UserWarning) + for i, _ in enumerate(tth): + d[i] = i + return d + if 0 in q: + warnings.warn(inf_output_wmsg) + return 2.0 * np.pi / copy(q) def d_to_q(d): @@ -166,6 +195,27 @@ def d_to_q(d): return 2.0 * np.pi / copy(d) -def d_to_tth(darray, wavelength): - qarray = d_to_q(darray) - return q_to_tth(qarray, wavelength) +def d_to_tth(d, wavelength): + r""" + Helper function to convert d to two-theta on independent variable axis. + + The formula is .. math:: 2\theta = 2 \arcsin\left(\frac{\lambda}{2d}\right). + + Here we convert d to q first, then to tth. + + Parameters + ---------- + d : 1D array + The array of :math:`d` values np.array([ds]). + + wavelength : float + Wavelength of the incoming x-rays/neutrons/electrons + + Returns + ------- + tth : 1D array + The array of :math:`2\theta` values np.array([tths]). + The units of tth are expected in degrees. + """ + q = d_to_q(d) + return q_to_tth(q, wavelength) diff --git a/tests/test_transforms.py b/tests/test_transforms.py index 6c2e4c6e..f1c940b7 100644 --- a/tests/test_transforms.py +++ b/tests/test_transforms.py @@ -1,7 +1,7 @@ import numpy as np import pytest -from diffpy.utils.transforms import d_to_q, q_to_d, q_to_tth, tth_to_q +from diffpy.utils.transforms import d_to_q, d_to_tth, q_to_d, q_to_tth, tth_to_d, tth_to_q params_q_to_tth = [ # UC1: Empty q values, no wavelength, return empty arrays @@ -31,7 +31,7 @@ def test_q_to_tth(inputs, expected): [4 * np.pi, np.array([0.2, 0.4, 0.6, 0.8, 1, 1.2])], [ ValueError, - "The supplied q-array and wavelength will result in an impossible two-theta. " + "The supplied input array and wavelength will result in an impossible two-theta. " "Please check these values and re-instantiate the DiffractionObject with correct values.", ], ), @@ -40,7 +40,7 @@ def test_q_to_tth(inputs, expected): [100, np.array([0, 0.2, 0.4, 0.6, 0.8, 1])], [ ValueError, - "The supplied q-array and wavelength will result in an impossible two-theta. " + "The supplied input array and wavelength will result in an impossible two-theta. " "Please check these values and re-instantiate the DiffractionObject with correct values.", ], ), @@ -103,8 +103,8 @@ def test_tth_to_q_bad(inputs, expected): ([np.array([])], np.array([])), # UC2: User specified valid q values ( - [np.array([5 * np.pi, 4 * np.pi, 3 * np.pi, 2 * np.pi, np.pi, 0])], - np.array([0.4, 0.5, 0.66667, 1, 2, np.inf]), + [np.array([0, 1 * np.pi, 2 * np.pi, 3 * np.pi, 4 * np.pi, 5 * np.pi])], + np.array([np.inf, 2, 1, 0.66667, 0.5, 0.4]), ), ] @@ -120,8 +120,8 @@ def test_q_to_d(inputs, expected): ([np.array([])], np.array([])), # UC2: User specified valid d values ( - [np.array([0, 1 * np.pi, 2 * np.pi, 3 * np.pi, 4 * np.pi, 5 * np.pi])], - np.array([np.inf, 2, 1, 0.66667, 0.5, 0.4]), + [np.array([5 * np.pi, 4 * np.pi, 3 * np.pi, 2 * np.pi, np.pi, 0])], + np.array([0.4, 0.5, 0.66667, 1, 2, np.inf]), ), ] @@ -130,3 +130,99 @@ def test_q_to_d(inputs, expected): def test_d_to_q(inputs, expected): actual = d_to_q(inputs[0]) assert np.allclose(actual, expected) + + +params_tth_to_d = [ + # UC0: User specified empty tth values (without wavelength) + ([None, np.array([])], np.array([])), + # UC1: User specified empty tth values (with wavelength) + ([4 * np.pi, np.array([])], np.array([])), + # UC2: User specified valid tth values between 0-180 degrees (without wavelength) + ( + [None, np.array([0, 30, 60, 90, 120, 180])], + np.array([0, 1, 2, 3, 4, 5]), + ), + # UC3: User specified valid tth values between 0-180 degrees (with wavelength) + ( + [4 * np.pi, np.array([0, 30.0, 60.0, 90.0, 120.0, 180.0])], + np.array([np.inf, 24.27636, 12.56637, 8.88577, 7.25520, 6.28319]), + ), +] + + +@pytest.mark.parametrize("inputs, expected", params_tth_to_d) +def test_tth_to_d(inputs, expected): + actual = tth_to_d(inputs[1], inputs[0]) + assert np.allclose(actual, expected) + + +params_tth_to_d_bad = [ + # UC1: user specified an invalid tth value of > 180 degrees (without wavelength) + ( + [None, np.array([0, 30, 60, 90, 120, 181])], + [ValueError, "Two theta exceeds 180 degrees. Please check the input values for errors."], + ), + # UC2: user specified an invalid tth value of > 180 degrees (with wavelength) + ( + [4 * np.pi, np.array([0, 30, 60, 90, 120, 181])], + [ValueError, "Two theta exceeds 180 degrees. Please check the input values for errors."], + ), +] + + +@pytest.mark.parametrize("inputs, expected", params_tth_to_d_bad) +def test_tth_to_d_bad(inputs, expected): + with pytest.raises(expected[0], match=expected[1]): + tth_to_d(inputs[1], inputs[0]) + + +params_d_to_tth = [ + # UC1: Empty d values, no wavelength, return empty arrays + ([None, np.empty((0))], np.empty((0))), + # UC2: Empty d values, wavelength specified, return empty arrays + ([4 * np.pi, np.empty((0))], np.empty(0)), + # UC3: User specified valid d values, no wavelength, return empty arrays + ( + [None, np.array([1, 0.8, 0.6, 0.4, 0.2, 0])], + np.array([0, 1, 2, 3, 4, 5]), + ), + # UC4: User specified valid d values (with wavelength) + ( + [4 * np.pi, np.array([4 * np.pi, 4 / np.sqrt(2) * np.pi, 4 / np.sqrt(3) * np.pi])], + np.array([60.0, 90.0, 120.0]), + ), +] + + +@pytest.mark.parametrize("inputs, expected", params_d_to_tth) +def test_d_to_tth(inputs, expected): + actual = d_to_tth(inputs[1], inputs[0]) + assert np.allclose(expected, actual) + + +params_d_to_tth_bad = [ + # UC1: user specified invalid d values that result in tth > 180 degrees + ( + [4 * np.pi, np.array([1.2, 1, 0.8, 0.6, 0.4, 0.2])], + [ + ValueError, + "The supplied input array and wavelength will result in an impossible two-theta. " + "Please check these values and re-instantiate the DiffractionObject with correct values.", + ], + ), + # UC2: user specified a wrong wavelength that result in tth > 180 degrees + ( + [100, np.array([1, 0.8, 0.6, 0.4, 0.2, 0])], + [ + ValueError, + "The supplied input array and wavelength will result in an impossible two-theta. " + "Please check these values and re-instantiate the DiffractionObject with correct values.", + ], + ), +] + + +@pytest.mark.parametrize("inputs, expected", params_d_to_tth_bad) +def test_d_to_tth_bad(inputs, expected): + with pytest.raises(expected[0], match=expected[1]): + d_to_tth(inputs[1], inputs[0])