Skip to content
23 changes: 14 additions & 9 deletions src/diffpy/utils/scattering_objects/diffraction_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -763,25 +763,27 @@ 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

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
"""
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("Please check if you entered an incorrect wavelength or q value.")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we would want to raise a value error here because otherwise the code proceeds with tth = nan.

return np.rad2deg(2.0 * np.arcsin(q * pre_factor))

def tth_to_q(self):
Expand All @@ -800,25 +802,28 @@ 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``
"""
two_theta = np.asarray(np.deg2rad(self.on_tth[0]))
if np.any(two_theta > np.pi):
Copy link
Contributor

Choose a reason for hiding this comment

The 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(
"Two theta exceeds 180 degrees. Please check if invalid values were entered "
"or if degrees were incorrectly specified as radians."
)
wavelength = float(self.wavelength)
pre_factor = (4 * np.pi) / wavelength
return pre_factor * np.sin(two_theta / 2)
Expand Down
176 changes: 176 additions & 0 deletions tests/diffpy/utils/scattering_objects/test_diffraction_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,182 @@ 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):
"""Checks the behavior of the DiffractionObject:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test functions don't need docstring, though if it's ok to leave this now you did it.

when there is no wavelength, we expect the correct warning message and output,
otherwise, we only check the output matches the 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) == (
"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, you can use "
"DiffractionObject(wavelength=0.71)."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0.71? Or something like "wavelength=) where you replace with the actual wavelength in the units of..." Etc. Also, are these the correct instructions? This seems to be instantiating a new DO not adding a wavelength to an existing DO.

Let's also make sure to add these instructions to the documentation.

)
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)
# expected tth values are 2*arcsin(q) in degrees
(
[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.",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add ."... 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. "
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.",
],
),
# 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]],
[IndexError, "Please ensure q array and intensity array are 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]],
[IndexError, "Please ensure q array and intensity array are the same length."],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think RunTime wrote may be more appropriate here since I think we will raise it ourselves?

),
# 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]],
[IndexError, "Please ensure two theta array and intensity array are 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]],
[IndexError, "Please ensure two theta array and intensity array are 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)
Expand Down
Loading