-
Notifications
You must be signed in to change notification settings - Fork 21
q_to_tth & tth_to_q #178
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
q_to_tth & tth_to_q #178
Changes from 10 commits
7841ff2
1b0fa19
4e85b0a
a33482b
3efde28
a53461f
2a5a819
64d8693
728ff36
490486c
d481253
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
.. _Diffraction Objects Example: | ||
|
||
:tocdepth: -1 | ||
|
||
Diffraction Objects Example | ||
########################### | ||
|
||
This example will demonstrate how to use the ``DiffractionObject`` class in the | ||
``diffpy.utils.scattering_objects.diffraction_objects`` module to process and analyze diffraction data. | ||
|
||
1) We have the function ``q_to_tth`` to convert q to two theta values in degrees, and ``tth_to_q`` to do the reverse. | ||
You can use these functions with a pre-defined ``DiffractionObject``. :: | ||
|
||
# convert q to tth | ||
from diffpy.utils.scattering_objects.diffraction_objects import DiffractionObject | ||
test = DiffractionObject(wavelength=1.54) | ||
test.on_q = [[0, 0.2, 0.4, 0.6, 0.8, 1], [1, 2, 3, 4, 5, 6]] | ||
test.q_to_tth() | ||
|
||
This function will convert your provided q array and return a two theta array in degrees. | ||
To load the converted array, you can either call ``test.q_to_tth()`` or ``test.on_q[0]``. | ||
|
||
Similarly, use the function ``tth_to_q`` to convert two theta values in degrees to q values. :: | ||
|
||
# convert tth to q | ||
from diffpy.utils.scattering_objects.diffraction_objects import DiffractionObject | ||
test = DiffractionObject(wavelength=1.54) | ||
test.on_tth = [[0, 30, 60, 90, 120, 180], [1, 2, 3, 4, 5, 6]] | ||
test.tth_to_q() | ||
|
||
To load the converted array, you can either call ``test.tth_to_q()`` or ``test.on_tth[0]``. | ||
|
||
2) You can use these functions without specifying a wavelength. However, if so, the function will return an empty array, | ||
so we strongly encourage you to specify a wavelength when using these functions. :: | ||
|
||
from diffpy.utils.scattering_objects.diffraction_objects import DiffractionObject | ||
test = DiffractionObject() | ||
test.on_q = [[0, 0.2, 0.4, 0.6, 0.8, 1], [1, 2, 3, 4, 5, 6]] | ||
test.q_to_tth() | ||
|
||
In this case, the function will return an empty array on two theta. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
.. _Diffraction Objects Utility: | ||
|
||
Diffraction Objects Utility | ||
=========================== | ||
|
||
The ``diffpy.utils.scattering_objects.diffraction_objects`` module provides functions | ||
for managing and analyzing diffraction data, including angle-space conversions | ||
and interactions between diffraction data. | ||
|
||
- ``q_to_tth()``: Converts an array of q values to their corresponding two theta values, based on specified wavelength. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we don't need this here. It will be auto-generated in the API docs, and it is shown as an aexample above. |
||
- ``tth_to_q()``: Converts an array of two theta values to their corresponding q values, based on specified wavelength. | ||
|
||
These functions help developers standardize diffraction data and update the arrays | ||
in the associated ``DiffractionObject``, enabling easier analysis and further processing. | ||
|
||
For a more in-depth tutorial for how to use these functions, click :ref:`here <Diffraction Objects Example>`. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
**Added:** | ||
|
||
* functionality to raise useful warning and error messages during angular conversion between two theta and q | ||
|
||
**Changed:** | ||
|
||
* <news item> | ||
|
||
**Deprecated:** | ||
|
||
* <news item> | ||
|
||
**Removed:** | ||
|
||
* <news item> | ||
|
||
**Fixed:** | ||
|
||
* <news item> | ||
|
||
**Security:** | ||
|
||
* <news item> |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,23 @@ | |
"and specifying how to handle the mismatch." | ||
) | ||
|
||
wavelength_warning_emsg = ( | ||
"INFO: no wavelength has been specified. You can continue " | ||
"to use the DiffractionObject but some of its powerful features " | ||
"will not be available. To specify a wavelength, set " | ||
"diffraction_object.wavelength = [number], " | ||
"where diffraction_object is the variable name of you Diffraction Object, " | ||
"and number is the wavelength in angstroms." | ||
) | ||
|
||
length_mismatch_emsg = "Please ensure {array_name} array and intensity array are of the same length." | ||
non_numeric_value_emsg = "Invalid value found in {array_name} array. Please ensure all values are numeric." | ||
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. " | ||
"Please check these values and re-instantiate the DiffractionObject with correct values." | ||
) | ||
|
||
|
||
class Diffraction_object: | ||
"""A class to represent and manipulate data associated with diffraction experiments. | ||
|
@@ -763,25 +780,35 @@ def q_to_tth(self): | |
|
||
2\theta_n = 2 \arcsin\left(\frac{\lambda q}{4 \pi}\right) | ||
|
||
Function adapted from scikit-beam. Thanks to those developers | ||
sbillinge marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Parameters | ||
---------- | ||
q : array | ||
An array of :math:`q` values | ||
The array of :math:`q` values | ||
|
||
wavelength : float | ||
Wavelength of the incoming x-rays | ||
|
||
Function adapted from scikit-beam. Thanks to those developers | ||
|
||
Returns | ||
------- | ||
two_theta : array | ||
An array of :math:`2\theta` values in radians | ||
The array of :math:`2\theta` values in radians | ||
""" | ||
for i, value in enumerate(self.on_q[0]): | ||
if not isinstance(value, (int, float)): | ||
raise TypeError(non_numeric_value_emsg.format(array_name="q")) | ||
if len(self.on_q[0]) != len(self.on_q[1]): | ||
raise RuntimeError(length_mismatch_emsg.format(array_name="q")) | ||
if self.wavelength is None: | ||
warnings.warn(wavelength_warning_emsg, UserWarning) | ||
return np.empty(0) | ||
q = self.on_q[0] | ||
q = np.asarray(q) | ||
wavelength = float(self.wavelength) | ||
pre_factor = wavelength / (4 * np.pi) | ||
if np.any(np.abs(q * pre_factor) > 1): | ||
raise ValueError(invalid_q_or_wavelength_emsg) | ||
return np.rad2deg(2.0 * np.arcsin(q * pre_factor)) | ||
|
||
def tth_to_q(self): | ||
|
@@ -800,25 +827,33 @@ def tth_to_q(self): | |
|
||
q = \frac{4 \pi \sin\left(\frac{2\theta}{2}\right)}{\lambda} | ||
|
||
|
||
Function adapted from scikit-beam. Thanks to those developers. | ||
|
||
Parameters | ||
---------- | ||
two_theta : array | ||
An array of :math:`2\theta` values in units of degrees | ||
The array of :math:`2\theta` values in units of degrees | ||
|
||
wavelength : float | ||
Wavelength of the incoming x-rays | ||
|
||
Function adapted from scikit-beam. Thanks to those developers. | ||
|
||
Returns | ||
------- | ||
q : array | ||
An array of :math:`q` values in the inverse of the units | ||
The array of :math:`q` values in the inverse of the units | ||
of ``wavelength`` | ||
""" | ||
for i, value in enumerate(self.on_tth[0]): | ||
if not isinstance(value, (int, float)): | ||
raise TypeError(non_numeric_value_emsg.format(array_name="two theta")) | ||
if len(self.on_tth[0]) != len(self.on_tth[1]): | ||
raise RuntimeError(length_mismatch_emsg.format(array_name="two theta")) | ||
two_theta = np.asarray(np.deg2rad(self.on_tth[0])) | ||
if np.any(two_theta > np.pi): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. as above, I think this isn't doing quite what we want. |
||
raise ValueError(invalid_tth_emsg) | ||
if self.wavelength is None: | ||
warnings.warn(wavelength_warning_emsg, UserWarning) | ||
return np.empty(0) | ||
wavelength = float(self.wavelength) | ||
pre_factor = (4 * np.pi) / wavelength | ||
return pre_factor * np.sin(two_theta / 2) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,7 +4,7 @@ | |
import pytest | ||
from freezegun import freeze_time | ||
|
||
from diffpy.utils.scattering_objects.diffraction_objects import DiffractionObject | ||
from diffpy.utils.scattering_objects.diffraction_objects import DiffractionObject, wavelength_warning_emsg | ||
|
||
params = [ | ||
( # Default | ||
|
@@ -231,6 +231,155 @@ def test_diffraction_objects_equality(inputs1, inputs2, expected): | |
assert (diffraction_object1 == diffraction_object2) == expected | ||
|
||
|
||
def _test_valid_diffraction_objects(actual_diffraction_object, function, expected_array): | ||
if actual_diffraction_object.wavelength is None: | ||
with pytest.warns(UserWarning) as warn_record: | ||
getattr(actual_diffraction_object, function)() | ||
assert str(warn_record[0].message) == wavelength_warning_emsg | ||
actual_array = getattr(actual_diffraction_object, function)() | ||
return np.allclose(actual_array, expected_array) | ||
|
||
|
||
params_q_to_tth = [ | ||
# UC1: User specified empty q values (without wavelength) | ||
([None, [], []], [[]]), | ||
# UC2: User specified empty q values (with wavelength) | ||
([4 * np.pi, [], []], [[]]), | ||
# UC3: User specified valid q values (without wavelength) | ||
([None, [0, 0.2, 0.4, 0.6, 0.8, 1], [1, 2, 3, 4, 5, 6]], [[]]), | ||
# UC4: User specified valid q values (with wavelength) | ||
# expected tth values are 2*arcsin(q) in degrees | ||
( | ||
[4 * np.pi, [0, 0.2, 0.4, 0.6, 0.8, 1], [1, 2, 3, 4, 5, 6]], | ||
[[0, 23.07392, 47.15636, 73.73980, 106.26020, 180]], | ||
), | ||
] | ||
|
||
|
||
@pytest.mark.parametrize("inputs, expected", params_q_to_tth) | ||
def test_q_to_tth(inputs, expected): | ||
actual = DiffractionObject(wavelength=inputs[0]) | ||
actual.on_q = [inputs[1], inputs[2]] | ||
expected_tth = expected[0] | ||
assert _test_valid_diffraction_objects(actual, "q_to_tth", expected_tth) | ||
|
||
|
||
params_q_to_tth_bad = [ | ||
# UC1: user specified invalid q values that result in tth > 180 degrees | ||
( | ||
[4 * np.pi, [0.2, 0.4, 0.6, 0.8, 1, 1.2], [1, 2, 3, 4, 5, 6]], | ||
[ | ||
ValueError, | ||
"The supplied q-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, [0, 0.2, 0.4, 0.6, 0.8, 1], [1, 2, 3, 4, 5, 6]], | ||
[ | ||
ValueError, | ||
"The supplied q-array and wavelength will result in an impossible two-theta. " | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See above. Also since we are reusing the error message do we want to minimize word by defining it in a variable once and reusing the variable in multiple tests? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes I will store them in diffraction_objects.py file |
||
"Please check these values and re-instantiate the DiffractionObject with correct values.", | ||
], | ||
), | ||
# UC3: user specified a q array that does not match the length of intensity array (without wavelength) | ||
( | ||
[None, [0, 0.2, 0.4, 0.6, 0.8, 1], [1, 2, 3, 4, 5]], | ||
[RuntimeError, "Please ensure q array and intensity array are of the same length."], | ||
), | ||
# UC4: user specified a q array that does not match the length of intensity array (with wavelength) | ||
( | ||
[4 * np.pi, [0, 0.2, 0.4, 0.6, 0.8, 1], [1, 2, 3, 4, 5]], | ||
[RuntimeError, "Please ensure q array and intensity array are of the same length."], | ||
), | ||
# UC5: user specified a non-numeric value in q array (without wavelength) | ||
( | ||
[None, [0, 0.2, 0.4, 0.6, 0.8, "invalid"], [1, 2, 3, 4, 5, 6]], | ||
[TypeError, "Invalid value found in q array. Please ensure all values are numeric."], | ||
), | ||
# UC5: user specified a non-numeric value in q array (with wavelength) | ||
( | ||
[4 * np.pi, [0, 0.2, 0.4, 0.6, 0.8, "invalid"], [1, 2, 3, 4, 5, 6]], | ||
[TypeError, "Invalid value found in q array. Please ensure all values are numeric."], | ||
), | ||
] | ||
|
||
|
||
@pytest.mark.parametrize("inputs, expected", params_q_to_tth_bad) | ||
def test_q_to_tth_bad(inputs, expected): | ||
actual = DiffractionObject(wavelength=inputs[0]) | ||
actual.on_q = [inputs[1], inputs[2]] | ||
with pytest.raises(expected[0], match=expected[1]): | ||
actual.q_to_tth() | ||
|
||
|
||
params_tth_to_q = [ | ||
# UC1: User specified empty tth values (without wavelength) | ||
([None, [], []], [[]]), | ||
# UC2: User specified empty tth values (with wavelength) | ||
([4 * np.pi, [], []], [[]]), | ||
# UC3: User specified valid tth values between 0-180 degrees (without wavelength) | ||
( | ||
[None, [0, 30, 60, 90, 120, 180], [1, 2, 3, 4, 5, 6]], | ||
[[]], | ||
), | ||
# UC4: User specified valid tth values between 0-180 degrees (with wavelength) | ||
# expected q vales are sin15, sin30, sin45, sin60, sin90 | ||
([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]]), | ||
] | ||
|
||
|
||
@pytest.mark.parametrize("inputs, expected", params_tth_to_q) | ||
def test_tth_to_q(inputs, expected): | ||
actual = DiffractionObject(wavelength=inputs[0]) | ||
actual.on_tth = [inputs[1], inputs[2]] | ||
expected_q = expected[0] | ||
assert _test_valid_diffraction_objects(actual, "tth_to_q", expected_q) | ||
|
||
|
||
params_tth_to_q_bad = [ | ||
# UC1: user specified an invalid tth value of > 180 degrees (without wavelength) | ||
( | ||
[None, [0, 30, 60, 90, 120, 181], [1, 2, 3, 4, 5, 6]], | ||
[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, [0, 30, 60, 90, 120, 181], [1, 2, 3, 4, 5, 6]], | ||
[ValueError, "Two theta exceeds 180 degrees. Please check the input values for errors."], | ||
), | ||
# UC3: user specified a two theta array that does not match the length of intensity array (without wavelength) | ||
( | ||
[None, [0, 30, 60, 90, 120], [1, 2, 3, 4, 5, 6]], | ||
[RuntimeError, "Please ensure two theta array and intensity array are of the same length."], | ||
), | ||
# UC4: user specified a two theta array that does not match the length of intensity array (with wavelength) | ||
( | ||
[4 * np.pi, [0, 30, 60, 90, 120], [1, 2, 3, 4, 5, 6]], | ||
[RuntimeError, "Please ensure two theta array and intensity array are of the same length."], | ||
), | ||
# UC5: user specified a non-numeric value in two theta array (without wavelength) | ||
( | ||
[None, [0, 30, 60, 90, 120, "invalid"], [1, 2, 3, 4, 5, 6]], | ||
[TypeError, "Invalid value found in two theta array. Please ensure all values are numeric."], | ||
), | ||
# UC6: user specified a non-numeric value in two theta array (with wavelength) | ||
( | ||
[4 * np.pi, [0, 30, 60, 90, 120, "invalid"], [1, 2, 3, 4, 5, 6]], | ||
[TypeError, "Invalid value found in two theta array. Please ensure all values are numeric."], | ||
), | ||
] | ||
|
||
|
||
@pytest.mark.parametrize("inputs, expected", params_tth_to_q_bad) | ||
def test_tth_to_q_bad(inputs, expected): | ||
actual = DiffractionObject(wavelength=inputs[0]) | ||
actual.on_tth = [inputs[1], inputs[2]] | ||
with pytest.raises(expected[0], match=expected[1]): | ||
actual.tth_to_q() | ||
|
||
|
||
def test_dump(tmp_path, mocker): | ||
x, y = np.linspace(0, 5, 6), np.linspace(0, 5, 6) | ||
directory = Path(tmp_path) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of test let's call it
my_diffraction_pattern
We are not writing all of the docs here, so you can start with something like "assuming we have created a
DiffractionObject
calledmy_diffraction_pattern
from a measured diffraction pattern, and we have specified the wavelenth (see Section ??) we can use theq_to_tth
andtth_to_q
functions to convert between Q and two-theta by typingmy_diffraction_pattern.q_to_tth()
..." ..and so on. Let's make an issue to add to docs "How to set a wavelength" and how to instantiate a diffraction object and how to add a diffraction pattern.