Skip to content

Commit d3f83b2

Browse files
authored
Merge pull request #178 from yucongalicechen/q-tth
q_to_tth & tth_to_q
2 parents b31135d + d481253 commit d3f83b2

File tree

5 files changed

+264
-10
lines changed

5 files changed

+264
-10
lines changed

Diff for: doc/source/examples/diffractionobjectsexample.rst

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
.. _Diffraction Objects Example:
2+
3+
:tocdepth: -1
4+
5+
Diffraction Objects Example
6+
###########################
7+
8+
This example will demonstrate how to use the ``DiffractionObject`` class in the
9+
``diffpy.utils.scattering_objects.diffraction_objects`` module to process and analyze diffraction data.
10+
11+
1) Assuming we have created a ``DiffractionObject`` called my_diffraction_pattern from a measured diffraction pattern,
12+
and we have specified the wavelength (see Section ??, to be added),
13+
we can use the ``q_to_tth`` and ``tth_to_q`` functions to convert between q and two-theta. ::
14+
15+
# Example: convert q to tth
16+
my_diffraction_pattern.on_q = [[0, 0.2, 0.4, 0.6, 0.8, 1], [1, 2, 3, 4, 5, 6]]
17+
my_diffraction_pattern.q_to_tth()
18+
19+
This function will convert your provided q array and return a two theta array in degrees.
20+
To load the converted array, you can either call ``test.q_to_tth()`` or ``test.on_q[0]``. ::
21+
22+
# Example: convert tth to q
23+
from diffpy.utils.scattering_objects.diffraction_objects import DiffractionObject
24+
my_diffraction_pattern.on_tth = [[0, 30, 60, 90, 120, 180], [1, 2, 3, 4, 5, 6]]
25+
my_diffraction_pattern.tth_to_q()
26+
27+
Similarly, to load the converted array, you can either call ``test.tth_to_q()`` or ``test.on_tth[0]``.
28+
29+
2) Both functions require a wavelength to perform conversions. Without a wavelength, they will return empty arrays.
30+
Therefore, we strongly encourage you to specify a wavelength when using these functions. ::
31+
32+
# Example: without wavelength specified
33+
my_diffraction_pattern.on_q = [[0, 0.2, 0.4, 0.6, 0.8, 1], [1, 2, 3, 4, 5, 6]]
34+
my_diffraction_pattern.q_to_tth() # returns an empty array

Diff for: doc/source/utilities/diffractionobjectsutility.rst

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.. _Diffraction Objects Utility:
2+
3+
Diffraction Objects Utility
4+
===========================
5+
6+
The ``diffpy.utils.scattering_objects.diffraction_objects`` module provides functions
7+
for managing and analyzing diffraction data, including angle-space conversions
8+
and interactions between diffraction data.
9+
10+
These functions help developers standardize diffraction data and update the arrays
11+
in the associated ``DiffractionObject``, enabling easier analysis and further processing.
12+
13+
For a more in-depth tutorial for how to use these functions, click :ref:`here <Diffraction Objects Example>`.

Diff for: news/tth-q.rst

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
**Added:**
2+
3+
* functionality to raise useful warning and error messages during angular conversion between two theta and q
4+
5+
**Changed:**
6+
7+
* <news item>
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>

Diff for: src/diffpy/utils/scattering_objects/diffraction_objects.py

+44-9
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,23 @@
1717
"and specifying how to handle the mismatch."
1818
)
1919

20+
wavelength_warning_emsg = (
21+
"INFO: no wavelength has been specified. You can continue "
22+
"to use the DiffractionObject but some of its powerful features "
23+
"will not be available. To specify a wavelength, set "
24+
"diffraction_object.wavelength = [number], "
25+
"where diffraction_object is the variable name of you Diffraction Object, "
26+
"and number is the wavelength in angstroms."
27+
)
28+
29+
length_mismatch_emsg = "Please ensure {array_name} array and intensity array are of the same length."
30+
non_numeric_value_emsg = "Invalid value found in {array_name} array. Please ensure all values are numeric."
31+
invalid_tth_emsg = "Two theta exceeds 180 degrees. Please check the input values for errors."
32+
invalid_q_or_wavelength_emsg = (
33+
"The supplied q-array and wavelength will result in an impossible two-theta. "
34+
"Please check these values and re-instantiate the DiffractionObject with correct values."
35+
)
36+
2037

2138
class Diffraction_object:
2239
"""A class to represent and manipulate data associated with diffraction experiments.
@@ -763,25 +780,35 @@ def q_to_tth(self):
763780
764781
2\theta_n = 2 \arcsin\left(\frac{\lambda q}{4 \pi}\right)
765782
783+
Function adapted from scikit-beam. Thanks to those developers
784+
766785
Parameters
767786
----------
768787
q : array
769-
An array of :math:`q` values
788+
The array of :math:`q` values
770789
771790
wavelength : float
772791
Wavelength of the incoming x-rays
773792
774-
Function adapted from scikit-beam. Thanks to those developers
775-
776793
Returns
777794
-------
778795
two_theta : array
779-
An array of :math:`2\theta` values in radians
796+
The array of :math:`2\theta` values in radians
780797
"""
798+
for i, value in enumerate(self.on_q[0]):
799+
if not isinstance(value, (int, float)):
800+
raise TypeError(non_numeric_value_emsg.format(array_name="q"))
801+
if len(self.on_q[0]) != len(self.on_q[1]):
802+
raise RuntimeError(length_mismatch_emsg.format(array_name="q"))
803+
if self.wavelength is None:
804+
warnings.warn(wavelength_warning_emsg, UserWarning)
805+
return np.empty(0)
781806
q = self.on_q[0]
782807
q = np.asarray(q)
783808
wavelength = float(self.wavelength)
784809
pre_factor = wavelength / (4 * np.pi)
810+
if np.any(np.abs(q * pre_factor) > 1):
811+
raise ValueError(invalid_q_or_wavelength_emsg)
785812
return np.rad2deg(2.0 * np.arcsin(q * pre_factor))
786813

787814
def tth_to_q(self):
@@ -800,25 +827,33 @@ def tth_to_q(self):
800827
801828
q = \frac{4 \pi \sin\left(\frac{2\theta}{2}\right)}{\lambda}
802829
803-
830+
Function adapted from scikit-beam. Thanks to those developers.
804831
805832
Parameters
806833
----------
807834
two_theta : array
808-
An array of :math:`2\theta` values in units of degrees
835+
The array of :math:`2\theta` values in units of degrees
809836
810837
wavelength : float
811838
Wavelength of the incoming x-rays
812839
813-
Function adapted from scikit-beam. Thanks to those developers.
814-
815840
Returns
816841
-------
817842
q : array
818-
An array of :math:`q` values in the inverse of the units
843+
The array of :math:`q` values in the inverse of the units
819844
of ``wavelength``
820845
"""
846+
for i, value in enumerate(self.on_tth[0]):
847+
if not isinstance(value, (int, float)):
848+
raise TypeError(non_numeric_value_emsg.format(array_name="two theta"))
849+
if len(self.on_tth[0]) != len(self.on_tth[1]):
850+
raise RuntimeError(length_mismatch_emsg.format(array_name="two theta"))
821851
two_theta = np.asarray(np.deg2rad(self.on_tth[0]))
852+
if np.any(two_theta > np.pi):
853+
raise ValueError(invalid_tth_emsg)
854+
if self.wavelength is None:
855+
warnings.warn(wavelength_warning_emsg, UserWarning)
856+
return np.empty(0)
822857
wavelength = float(self.wavelength)
823858
pre_factor = (4 * np.pi) / wavelength
824859
return pre_factor * np.sin(two_theta / 2)

Diff for: tests/diffpy/utils/scattering_objects/test_diffraction_objects.py

+150-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import pytest
55
from freezegun import freeze_time
66

7-
from diffpy.utils.scattering_objects.diffraction_objects import DiffractionObject
7+
from diffpy.utils.scattering_objects.diffraction_objects import DiffractionObject, wavelength_warning_emsg
88

99
params = [
1010
( # Default
@@ -231,6 +231,155 @@ def test_diffraction_objects_equality(inputs1, inputs2, expected):
231231
assert (diffraction_object1 == diffraction_object2) == expected
232232

233233

234+
def _test_valid_diffraction_objects(actual_diffraction_object, function, expected_array):
235+
if actual_diffraction_object.wavelength is None:
236+
with pytest.warns(UserWarning) as warn_record:
237+
getattr(actual_diffraction_object, function)()
238+
assert str(warn_record[0].message) == wavelength_warning_emsg
239+
actual_array = getattr(actual_diffraction_object, function)()
240+
return np.allclose(actual_array, expected_array)
241+
242+
243+
params_q_to_tth = [
244+
# UC1: User specified empty q values (without wavelength)
245+
([None, [], []], [[]]),
246+
# UC2: User specified empty q values (with wavelength)
247+
([4 * np.pi, [], []], [[]]),
248+
# UC3: User specified valid q values (without wavelength)
249+
([None, [0, 0.2, 0.4, 0.6, 0.8, 1], [1, 2, 3, 4, 5, 6]], [[]]),
250+
# UC4: User specified valid q values (with wavelength)
251+
# expected tth values are 2*arcsin(q) in degrees
252+
(
253+
[4 * np.pi, [0, 0.2, 0.4, 0.6, 0.8, 1], [1, 2, 3, 4, 5, 6]],
254+
[[0, 23.07392, 47.15636, 73.73980, 106.26020, 180]],
255+
),
256+
]
257+
258+
259+
@pytest.mark.parametrize("inputs, expected", params_q_to_tth)
260+
def test_q_to_tth(inputs, expected):
261+
actual = DiffractionObject(wavelength=inputs[0])
262+
actual.on_q = [inputs[1], inputs[2]]
263+
expected_tth = expected[0]
264+
assert _test_valid_diffraction_objects(actual, "q_to_tth", expected_tth)
265+
266+
267+
params_q_to_tth_bad = [
268+
# UC1: user specified invalid q values that result in tth > 180 degrees
269+
(
270+
[4 * np.pi, [0.2, 0.4, 0.6, 0.8, 1, 1.2], [1, 2, 3, 4, 5, 6]],
271+
[
272+
ValueError,
273+
"The supplied q-array and wavelength will result in an impossible two-theta. "
274+
"Please check these values and re-instantiate the DiffractionObject with correct values.",
275+
],
276+
),
277+
# UC2: user specified a wrong wavelength that result in tth > 180 degrees
278+
(
279+
[100, [0, 0.2, 0.4, 0.6, 0.8, 1], [1, 2, 3, 4, 5, 6]],
280+
[
281+
ValueError,
282+
"The supplied q-array and wavelength will result in an impossible two-theta. "
283+
"Please check these values and re-instantiate the DiffractionObject with correct values.",
284+
],
285+
),
286+
# UC3: user specified a q array that does not match the length of intensity array (without wavelength)
287+
(
288+
[None, [0, 0.2, 0.4, 0.6, 0.8, 1], [1, 2, 3, 4, 5]],
289+
[RuntimeError, "Please ensure q array and intensity array are of the same length."],
290+
),
291+
# UC4: user specified a q array that does not match the length of intensity array (with wavelength)
292+
(
293+
[4 * np.pi, [0, 0.2, 0.4, 0.6, 0.8, 1], [1, 2, 3, 4, 5]],
294+
[RuntimeError, "Please ensure q array and intensity array are of the same length."],
295+
),
296+
# UC5: user specified a non-numeric value in q array (without wavelength)
297+
(
298+
[None, [0, 0.2, 0.4, 0.6, 0.8, "invalid"], [1, 2, 3, 4, 5, 6]],
299+
[TypeError, "Invalid value found in q array. Please ensure all values are numeric."],
300+
),
301+
# UC5: user specified a non-numeric value in q array (with wavelength)
302+
(
303+
[4 * np.pi, [0, 0.2, 0.4, 0.6, 0.8, "invalid"], [1, 2, 3, 4, 5, 6]],
304+
[TypeError, "Invalid value found in q array. Please ensure all values are numeric."],
305+
),
306+
]
307+
308+
309+
@pytest.mark.parametrize("inputs, expected", params_q_to_tth_bad)
310+
def test_q_to_tth_bad(inputs, expected):
311+
actual = DiffractionObject(wavelength=inputs[0])
312+
actual.on_q = [inputs[1], inputs[2]]
313+
with pytest.raises(expected[0], match=expected[1]):
314+
actual.q_to_tth()
315+
316+
317+
params_tth_to_q = [
318+
# UC1: User specified empty tth values (without wavelength)
319+
([None, [], []], [[]]),
320+
# UC2: User specified empty tth values (with wavelength)
321+
([4 * np.pi, [], []], [[]]),
322+
# UC3: User specified valid tth values between 0-180 degrees (without wavelength)
323+
(
324+
[None, [0, 30, 60, 90, 120, 180], [1, 2, 3, 4, 5, 6]],
325+
[[]],
326+
),
327+
# UC4: User specified valid tth values between 0-180 degrees (with wavelength)
328+
# expected q vales are sin15, sin30, sin45, sin60, sin90
329+
([4 * np.pi, [0, 30, 60, 90, 120, 180], [1, 2, 3, 4, 5, 6]], [[0, 0.258819, 0.5, 0.707107, 0.866025, 1]]),
330+
]
331+
332+
333+
@pytest.mark.parametrize("inputs, expected", params_tth_to_q)
334+
def test_tth_to_q(inputs, expected):
335+
actual = DiffractionObject(wavelength=inputs[0])
336+
actual.on_tth = [inputs[1], inputs[2]]
337+
expected_q = expected[0]
338+
assert _test_valid_diffraction_objects(actual, "tth_to_q", expected_q)
339+
340+
341+
params_tth_to_q_bad = [
342+
# UC1: user specified an invalid tth value of > 180 degrees (without wavelength)
343+
(
344+
[None, [0, 30, 60, 90, 120, 181], [1, 2, 3, 4, 5, 6]],
345+
[ValueError, "Two theta exceeds 180 degrees. Please check the input values for errors."],
346+
),
347+
# UC2: user specified an invalid tth value of > 180 degrees (with wavelength)
348+
(
349+
[4 * np.pi, [0, 30, 60, 90, 120, 181], [1, 2, 3, 4, 5, 6]],
350+
[ValueError, "Two theta exceeds 180 degrees. Please check the input values for errors."],
351+
),
352+
# UC3: user specified a two theta array that does not match the length of intensity array (without wavelength)
353+
(
354+
[None, [0, 30, 60, 90, 120], [1, 2, 3, 4, 5, 6]],
355+
[RuntimeError, "Please ensure two theta array and intensity array are of the same length."],
356+
),
357+
# UC4: user specified a two theta array that does not match the length of intensity array (with wavelength)
358+
(
359+
[4 * np.pi, [0, 30, 60, 90, 120], [1, 2, 3, 4, 5, 6]],
360+
[RuntimeError, "Please ensure two theta array and intensity array are of the same length."],
361+
),
362+
# UC5: user specified a non-numeric value in two theta array (without wavelength)
363+
(
364+
[None, [0, 30, 60, 90, 120, "invalid"], [1, 2, 3, 4, 5, 6]],
365+
[TypeError, "Invalid value found in two theta array. Please ensure all values are numeric."],
366+
),
367+
# UC6: user specified a non-numeric value in two theta array (with wavelength)
368+
(
369+
[4 * np.pi, [0, 30, 60, 90, 120, "invalid"], [1, 2, 3, 4, 5, 6]],
370+
[TypeError, "Invalid value found in two theta array. Please ensure all values are numeric."],
371+
),
372+
]
373+
374+
375+
@pytest.mark.parametrize("inputs, expected", params_tth_to_q_bad)
376+
def test_tth_to_q_bad(inputs, expected):
377+
actual = DiffractionObject(wavelength=inputs[0])
378+
actual.on_tth = [inputs[1], inputs[2]]
379+
with pytest.raises(expected[0], match=expected[1]):
380+
actual.tth_to_q()
381+
382+
234383
def test_dump(tmp_path, mocker):
235384
x, y = np.linspace(0, 5, 6), np.linspace(0, 5, 6)
236385
directory = Path(tmp_path)

0 commit comments

Comments
 (0)