From 7841ff298b14246e92fb9ed528691d9c4c4fc184 Mon Sep 17 00:00:00 2001 From: yucongalicechen Date: Thu, 14 Nov 2024 23:08:06 -0500 Subject: [PATCH 01/11] fix docstring and add error messages --- .../scattering_objects/diffraction_objects.py | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/diffpy/utils/scattering_objects/diffraction_objects.py b/src/diffpy/utils/scattering_objects/diffraction_objects.py index 926ed8b2..17a687f5 100644 --- a/src/diffpy/utils/scattering_objects/diffraction_objects.py +++ b/src/diffpy/utils/scattering_objects/diffraction_objects.py @@ -763,25 +763,29 @@ 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( + "Invalid input for arcsin: some values exceed the range [-1, 1]. Check wavelength or q values." + ) return np.rad2deg(2.0 * np.arcsin(q * pre_factor)) def tth_to_q(self): @@ -800,25 +804,25 @@ 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): + raise ValueError("Two theta exceeds 180 degrees.") wavelength = float(self.wavelength) pre_factor = (4 * np.pi) / wavelength return pre_factor * np.sin(two_theta / 2) From 1b0fa19e59f65f45aa1e034e81b4124877127fea Mon Sep 17 00:00:00 2001 From: yucongalicechen Date: Thu, 14 Nov 2024 23:11:08 -0500 Subject: [PATCH 02/11] add tests --- .../test_diffraction_objects.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/diffpy/utils/scattering_objects/test_diffraction_objects.py b/tests/diffpy/utils/scattering_objects/test_diffraction_objects.py index b2225db8..57e6babf 100644 --- a/tests/diffpy/utils/scattering_objects/test_diffraction_objects.py +++ b/tests/diffpy/utils/scattering_objects/test_diffraction_objects.py @@ -231,6 +231,42 @@ def test_diffraction_objects_equality(inputs1, inputs2, expected): assert (diffraction_object1 == diffraction_object2) == expected +def test_q_to_tth(): + # valid q values including edge cases when q=0 or 1 + # expected tth values are 2*arcsin(q) + actual = DiffractionObject(wavelength=4 * np.pi) + setattr(actual, "on_q", [[0, 0.2, 0.4, 0.6, 0.8, 1], [1, 2, 3, 4, 5, 6]]) + actual_tth = actual.q_to_tth() + expected_tth = [0, 23.07392, 47.15636, 73.73980, 106.26020, 180] + assert np.allclose(actual_tth, expected_tth) + + +def test_q_to_tth_bad(): + # invalid q values when arcsin value is not in the range of [-1, 1] + actual = DiffractionObject(wavelength=4 * np.pi) + setattr(actual, "on_q", [[0.6, 0.8, 1, 1.2], [1, 2, 3, 4]]) + with pytest.raises(ValueError): + actual.q_to_tth() + + +def test_tth_to_q(): + # valid tth values including edge cases when tth=0 or 180 degree + # expected q vales are sin15, sin30, sin45, sin60, sin90 + actual = DiffractionObject(wavelength=4 * np.pi) + setattr(actual, "on_tth", [[0, 30, 60, 90, 120, 180], [1, 2, 3, 4, 5, 6]]) + actual_q = actual.tth_to_q() + expected_q = [0, 0.258819, 0.5, 0.707107, 0.866025, 1] + assert np.allclose(actual_q, expected_q) + + +def test_tth_to_q_bad(): + # invalid tth value of > 180 degree + actual = DiffractionObject(wavelength=4 * np.pi) + setattr(actual, "on_tth", [[0, 30, 60, 90, 120, 181], [1, 2, 3, 4, 5, 6]]) + with pytest.raises(ValueError): + 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) From 4e85b0a215d1dd344689616d3a6d1a4d73c923f4 Mon Sep 17 00:00:00 2001 From: yucongalicechen Date: Wed, 20 Nov 2024 12:00:54 -0500 Subject: [PATCH 03/11] edit error message --- .../utils/scattering_objects/diffraction_objects.py | 9 +++++---- .../utils/scattering_objects/test_diffraction_objects.py | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/diffpy/utils/scattering_objects/diffraction_objects.py b/src/diffpy/utils/scattering_objects/diffraction_objects.py index 17a687f5..0ff9f0ce 100644 --- a/src/diffpy/utils/scattering_objects/diffraction_objects.py +++ b/src/diffpy/utils/scattering_objects/diffraction_objects.py @@ -783,9 +783,7 @@ def q_to_tth(self): wavelength = float(self.wavelength) pre_factor = wavelength / (4 * np.pi) if np.any(np.abs(q * pre_factor) > 1): - raise ValueError( - "Invalid input for arcsin: some values exceed the range [-1, 1]. Check wavelength or q values." - ) + raise ValueError("Please check if you entered an incorrect wavelength or q value.") return np.rad2deg(2.0 * np.arcsin(q * pre_factor)) def tth_to_q(self): @@ -822,7 +820,10 @@ def tth_to_q(self): """ two_theta = np.asarray(np.deg2rad(self.on_tth[0])) if np.any(two_theta > np.pi): - raise ValueError("Two theta exceeds 180 degrees.") + 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) diff --git a/tests/diffpy/utils/scattering_objects/test_diffraction_objects.py b/tests/diffpy/utils/scattering_objects/test_diffraction_objects.py index 57e6babf..4caa61ae 100644 --- a/tests/diffpy/utils/scattering_objects/test_diffraction_objects.py +++ b/tests/diffpy/utils/scattering_objects/test_diffraction_objects.py @@ -232,7 +232,7 @@ def test_diffraction_objects_equality(inputs1, inputs2, expected): def test_q_to_tth(): - # valid q values including edge cases when q=0 or 1 + # valid q values including edge cases when q=0 or tth=180 after converting # expected tth values are 2*arcsin(q) actual = DiffractionObject(wavelength=4 * np.pi) setattr(actual, "on_q", [[0, 0.2, 0.4, 0.6, 0.8, 1], [1, 2, 3, 4, 5, 6]]) @@ -242,7 +242,7 @@ def test_q_to_tth(): def test_q_to_tth_bad(): - # invalid q values when arcsin value is not in the range of [-1, 1] + # invalid wavelength or q values when arcsin value is not in the range of [-1, 1] actual = DiffractionObject(wavelength=4 * np.pi) setattr(actual, "on_q", [[0.6, 0.8, 1, 1.2], [1, 2, 3, 4]]) with pytest.raises(ValueError): From a33482b240dd276c508700936c5c07a31e2eb5ed Mon Sep 17 00:00:00 2001 From: yucongalicechen Date: Thu, 21 Nov 2024 18:19:43 -0500 Subject: [PATCH 04/11] add more tests --- .../test_diffraction_objects.py | 79 ++++++++++++++++--- 1 file changed, 67 insertions(+), 12 deletions(-) diff --git a/tests/diffpy/utils/scattering_objects/test_diffraction_objects.py b/tests/diffpy/utils/scattering_objects/test_diffraction_objects.py index 4caa61ae..36aace8f 100644 --- a/tests/diffpy/utils/scattering_objects/test_diffraction_objects.py +++ b/tests/diffpy/utils/scattering_objects/test_diffraction_objects.py @@ -232,8 +232,8 @@ def test_diffraction_objects_equality(inputs1, inputs2, expected): def test_q_to_tth(): - # valid q values including edge cases when q=0 or tth=180 after converting - # expected tth values are 2*arcsin(q) + # Valid q values that should result in 0-180 tth values after conversion + # expected tth values are 2*arcsin(q) in degrees actual = DiffractionObject(wavelength=4 * np.pi) setattr(actual, "on_q", [[0, 0.2, 0.4, 0.6, 0.8, 1], [1, 2, 3, 4, 5, 6]]) actual_tth = actual.q_to_tth() @@ -241,16 +241,43 @@ def test_q_to_tth(): assert np.allclose(actual_tth, expected_tth) -def test_q_to_tth_bad(): - # invalid wavelength or q values when arcsin value is not in the range of [-1, 1] - actual = DiffractionObject(wavelength=4 * np.pi) - setattr(actual, "on_q", [[0.6, 0.8, 1, 1.2], [1, 2, 3, 4]]) +params_q_to_tth_bad = [ + # UC1: user did not specify wavelength + ( + [None, [0, 0.2, 0.4, 0.6, 0.8, 1]], + "Wavelength is not specified. Please provide a valid wavelength, " + "e.g., DiffractionObject(wavelength=0.71).", + ), + # UC2: 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]], + "Wavelength * q > 4 * pi. Please check if you entered an incorrect wavelength or q value.", + ), + # UC3: user specified a wrong wavelength that result in tth > 180 degrees + ( + [100, [0, 0.2, 0.4, 0.6, 0.8, 1]], + "Wavelength * q > 4 * pi. Please check if you entered an incorrect wavelength or q value.", + ), + # UC4: user specified an empty q array + ([4 * np.pi, []], "Q array is empty. Please provide valid q values."), + # UC5: user specified a non-numeric value in q array + ( + [4 * np.pi, [0, 0.2, 0.4, 0.6, 0.8, "invalid"]], + "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]) + setattr(actual, "on_q", [inputs[1], [1, 2, 3, 4, 5, 6]]) with pytest.raises(ValueError): actual.q_to_tth() def test_tth_to_q(): - # valid tth values including edge cases when tth=0 or 180 degree + # Valid tth values between 0-180 degrees # expected q vales are sin15, sin30, sin45, sin60, sin90 actual = DiffractionObject(wavelength=4 * np.pi) setattr(actual, "on_tth", [[0, 30, 60, 90, 120, 180], [1, 2, 3, 4, 5, 6]]) @@ -259,11 +286,39 @@ def test_tth_to_q(): assert np.allclose(actual_q, expected_q) -def test_tth_to_q_bad(): - # invalid tth value of > 180 degree - actual = DiffractionObject(wavelength=4 * np.pi) - setattr(actual, "on_tth", [[0, 30, 60, 90, 120, 181], [1, 2, 3, 4, 5, 6]]) - with pytest.raises(ValueError): +params_tth_to_q_bad = [ + # UC1: user did not specify wavelength + ( + [None, [0, 30, 60, 90, 120, 180]], + "Wavelength is not specified. Please provide a valid wavelength, " + "e.g., DiffractionObject(wavelength=0.71).", + ), + # UC2: user specified an invalid tth value of > 180 degrees + ( + [4 * np.pi, [0, 30, 60, 90, 120, 181]], + "Two theta exceeds 180 degrees. Please check the input values for errors.", + ), + # UC3: user did not specify wavelength and specified invalid tth values + ( + [None, [0, 30, 60, 90, 120, 181]], + "Wavelength is not specified. Please provide a valid wavelength, " + "e.g., DiffractionObject(wavelength=0.71).", + ), + # UC4: user specified an empty two theta array + ([4 * np.pi, []], "Two theta array is empty. Please provide valid two theta values."), + # UC5: user specified a non-numeric value in two theta array + ( + [4 * np.pi, [0, 30, 60, 90, 120, "invalid"]], + "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]) + setattr(actual, "on_tth", [inputs[1], [1, 2, 3, 4, 5, 6]]) + with pytest.raises(ValueError, match=expected): actual.tth_to_q() From 3efde28d9e40ef68a97796e569471e646df24827 Mon Sep 17 00:00:00 2001 From: yucongalicechen Date: Mon, 25 Nov 2024 09:54:09 -0500 Subject: [PATCH 05/11] add warning msg for wavelength, edit error msg --- .../test_diffraction_objects.py | 183 +++++++++++++----- 1 file changed, 134 insertions(+), 49 deletions(-) diff --git a/tests/diffpy/utils/scattering_objects/test_diffraction_objects.py b/tests/diffpy/utils/scattering_objects/test_diffraction_objects.py index 36aace8f..07674a5c 100644 --- a/tests/diffpy/utils/scattering_objects/test_diffraction_objects.py +++ b/tests/diffpy/utils/scattering_objects/test_diffraction_objects.py @@ -231,39 +231,95 @@ def test_diffraction_objects_equality(inputs1, inputs2, expected): assert (diffraction_object1 == diffraction_object2) == expected -def test_q_to_tth(): - # Valid q values that should result in 0-180 tth values after conversion +def _test_valid_diffraction_objects(actual_diffraction_object, function, expected_array): + """Checks the behavior of the DiffractionObject: + 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)." + ) + 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 - actual = DiffractionObject(wavelength=4 * np.pi) - setattr(actual, "on_q", [[0, 0.2, 0.4, 0.6, 0.8, 1], [1, 2, 3, 4, 5, 6]]) - actual_tth = actual.q_to_tth() - expected_tth = [0, 23.07392, 47.15636, 73.73980, 106.26020, 180] - assert np.allclose(actual_tth, expected_tth) + ( + [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 did not specify wavelength + # UC1: user specified invalid q values that result in tth > 180 degrees ( - [None, [0, 0.2, 0.4, 0.6, 0.8, 1]], - "Wavelength is not specified. Please provide a valid wavelength, " - "e.g., DiffractionObject(wavelength=0.71).", + [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.", + ], ), - # UC2: user specified invalid q values that result in tth > 180 degrees + # UC2: user specified a wrong wavelength that result in tth > 180 degrees ( - [4 * np.pi, [0.2, 0.4, 0.6, 0.8, 1, 1.2]], - "Wavelength * q > 4 * pi. Please check if you entered an incorrect wavelength or q value.", + [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. " + "Please check these values and re-instantiate the DiffractionObject.", + ], ), - # UC3: user specified a wrong wavelength that result in tth > 180 degrees + # UC3: user specified a q array that does not match the length of intensity array (without wavelength) ( - [100, [0, 0.2, 0.4, 0.6, 0.8, 1]], - "Wavelength * q > 4 * pi. Please check if you entered an incorrect wavelength or q value.", + [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 an empty q array - ([4 * np.pi, []], "Q array is empty. Please provide valid q values."), - # UC5: user specified a non-numeric value in q array + # 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, "invalid"]], - "Invalid value found in q array. Please ensure all values are numeric.", + [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."], + ), + # 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."], ), ] @@ -271,45 +327,74 @@ def test_q_to_tth(): @pytest.mark.parametrize("inputs, expected", params_q_to_tth_bad) def test_q_to_tth_bad(inputs, expected): actual = DiffractionObject(wavelength=inputs[0]) - setattr(actual, "on_q", [inputs[1], [1, 2, 3, 4, 5, 6]]) - with pytest.raises(ValueError): + actual.on_q = [inputs[1], inputs[2]] + with pytest.raises(expected[0], match=expected[1]): actual.q_to_tth() -def test_tth_to_q(): - # Valid tth values between 0-180 degrees +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 - actual = DiffractionObject(wavelength=4 * np.pi) - setattr(actual, "on_tth", [[0, 30, 60, 90, 120, 180], [1, 2, 3, 4, 5, 6]]) - actual_q = actual.tth_to_q() - expected_q = [0, 0.258819, 0.5, 0.707107, 0.866025, 1] - assert np.allclose(actual_q, expected_q) + ( + [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 did not specify wavelength + # 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, 180]], - "Wavelength is not specified. Please provide a valid wavelength, " - "e.g., DiffractionObject(wavelength=0.71).", + [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."], ), - # UC2: user specified an invalid tth value of > 180 degrees + # 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, 181]], - "Two theta exceeds 180 degrees. Please check the input values for errors.", + [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."], ), - # UC3: user did not specify wavelength and specified invalid tth values + # UC5: user specified a non-numeric value in two theta array (without wavelength) ( - [None, [0, 30, 60, 90, 120, 181]], - "Wavelength is not specified. Please provide a valid wavelength, " - "e.g., DiffractionObject(wavelength=0.71).", + [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."], ), - # UC4: user specified an empty two theta array - ([4 * np.pi, []], "Two theta array is empty. Please provide valid two theta values."), - # UC5: user specified a non-numeric value in two theta array + # UC6: user specified a non-numeric value in two theta array (with wavelength) ( - [4 * np.pi, [0, 30, 60, 90, 120, "invalid"]], - "Invalid value found in two theta array. Please ensure all values are numeric.", + [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."], ), ] @@ -317,8 +402,8 @@ def test_tth_to_q(): @pytest.mark.parametrize("inputs, expected", params_tth_to_q_bad) def test_tth_to_q_bad(inputs, expected): actual = DiffractionObject(wavelength=inputs[0]) - setattr(actual, "on_tth", [inputs[1], [1, 2, 3, 4, 5, 6]]) - with pytest.raises(ValueError, match=expected): + actual.on_tth = [inputs[1], inputs[2]] + with pytest.raises(expected[0], match=expected[1]): actual.tth_to_q() From a53461f5865818a0d0db6dfd838759436e734f25 Mon Sep 17 00:00:00 2001 From: yucongalicechen Date: Mon, 25 Nov 2024 14:36:27 -0500 Subject: [PATCH 06/11] edit warning msg for wavelength, change index error to runtime error --- .../test_diffraction_objects.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/diffpy/utils/scattering_objects/test_diffraction_objects.py b/tests/diffpy/utils/scattering_objects/test_diffraction_objects.py index 07674a5c..59d7cafb 100644 --- a/tests/diffpy/utils/scattering_objects/test_diffraction_objects.py +++ b/tests/diffpy/utils/scattering_objects/test_diffraction_objects.py @@ -241,8 +241,10 @@ def _test_valid_diffraction_objects(actual_diffraction_object, function, expecte 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)." + "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." ) actual_array = getattr(actual_diffraction_object, function)() return np.allclose(actual_array, expected_array) @@ -289,7 +291,7 @@ def test_q_to_tth(inputs, expected): [ ValueError, "The supplied q-array and wavelength will result in an impossible two-theta. " - "Please check these values and re-instantiate the DiffractionObject.", + "Please check these values and re-instantiate the DiffractionObject with correct values.", ], ), # UC2: user specified a wrong wavelength that result in tth > 180 degrees @@ -304,12 +306,12 @@ def test_q_to_tth(inputs, expected): # 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."], + [RuntimeError, "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."], + [RuntimeError, "Please ensure q array and intensity array are the same length."], ), # UC5: user specified a non-numeric value in q array (without wavelength) ( @@ -379,12 +381,12 @@ def test_tth_to_q(inputs, expected): # 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."], + [RuntimeError, "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."], + [RuntimeError, "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) ( From 2a5a819e55b8c14d3fc7e327d4088fbe65aea14f Mon Sep 17 00:00:00 2001 From: yucongalicechen Date: Mon, 25 Nov 2024 15:15:10 -0500 Subject: [PATCH 07/11] add functionality for handling error messages --- .../scattering_objects/diffraction_objects.py | 40 ++++++++++++-- .../test_diffraction_objects.py | 52 +++++-------------- 2 files changed, 48 insertions(+), 44 deletions(-) diff --git a/src/diffpy/utils/scattering_objects/diffraction_objects.py b/src/diffpy/utils/scattering_objects/diffraction_objects.py index 0ff9f0ce..2017b926 100644 --- a/src/diffpy/utils/scattering_objects/diffraction_objects.py +++ b/src/diffpy/utils/scattering_objects/diffraction_objects.py @@ -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. @@ -778,12 +795,20 @@ def q_to_tth(self): two_theta : array 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("Please check if you entered an incorrect wavelength or q value.") + raise ValueError(invalid_q_or_wavelength_emsg) return np.rad2deg(2.0 * np.arcsin(q * pre_factor)) def tth_to_q(self): @@ -818,12 +843,17 @@ def tth_to_q(self): 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): - raise ValueError( - "Two theta exceeds 180 degrees. Please check if invalid values were entered " - "or if degrees were incorrectly specified as radians." - ) + 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) diff --git a/tests/diffpy/utils/scattering_objects/test_diffraction_objects.py b/tests/diffpy/utils/scattering_objects/test_diffraction_objects.py index 59d7cafb..7fd1ef96 100644 --- a/tests/diffpy/utils/scattering_objects/test_diffraction_objects.py +++ b/tests/diffpy/utils/scattering_objects/test_diffraction_objects.py @@ -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 @@ -238,35 +238,18 @@ def _test_valid_diffraction_objects(actual_diffraction_object, function, expecte 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, set " - "diffraction_object.wavelength = [number], " - "where diffraction_object is the variable name of you Diffraction Object, " - "and number is the wavelength in angstroms." - ) + 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, [], []], - [[]], - ), + ([None, [], []], [[]]), # UC2: User specified empty q values (with wavelength) - ( - [4 * np.pi, [], []], - [[]], - ), + ([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]], - [[]], - ), + ([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 ( @@ -300,18 +283,18 @@ def test_q_to_tth(inputs, expected): [ ValueError, "The supplied q-array and wavelength will result in an impossible two-theta. " - "Please check these values and re-instantiate the DiffractionObject.", + "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 the same length."], + [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 the same length."], + [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) ( @@ -336,15 +319,9 @@ def test_q_to_tth_bad(inputs, expected): params_tth_to_q = [ # UC1: User specified empty tth values (without wavelength) - ( - [None, [], []], - [[]], - ), + ([None, [], []], [[]]), # UC2: User specified empty tth values (with wavelength) - ( - [4 * np.pi, [], []], - [[]], - ), + ([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]], @@ -352,10 +329,7 @@ def test_q_to_tth_bad(inputs, expected): ), # 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]], - ), + ([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]]), ] @@ -381,12 +355,12 @@ def test_tth_to_q(inputs, expected): # 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 the same length."], + [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 the same length."], + [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) ( From 64d8693747357a889d922f1c5451717ff2967148 Mon Sep 17 00:00:00 2001 From: yucongalicechen Date: Mon, 25 Nov 2024 20:38:48 -0500 Subject: [PATCH 08/11] remove docstring from private test functions --- .../utils/scattering_objects/test_diffraction_objects.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/diffpy/utils/scattering_objects/test_diffraction_objects.py b/tests/diffpy/utils/scattering_objects/test_diffraction_objects.py index 7fd1ef96..3bdc22d4 100644 --- a/tests/diffpy/utils/scattering_objects/test_diffraction_objects.py +++ b/tests/diffpy/utils/scattering_objects/test_diffraction_objects.py @@ -232,9 +232,6 @@ def test_diffraction_objects_equality(inputs1, inputs2, expected): def _test_valid_diffraction_objects(actual_diffraction_object, function, expected_array): - """Checks the behavior of the DiffractionObject: - 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)() From 728ff36ba12dbda4e12415d695144c33ce5ac587 Mon Sep 17 00:00:00 2001 From: yucongalicechen Date: Tue, 26 Nov 2024 08:20:02 -0500 Subject: [PATCH 09/11] add news --- news/tth-q.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 news/tth-q.rst diff --git a/news/tth-q.rst b/news/tth-q.rst new file mode 100644 index 00000000..c512bf9b --- /dev/null +++ b/news/tth-q.rst @@ -0,0 +1,23 @@ +**Added:** + +* functionality to raise useful warning and error messages during angular conversion between two theta and q + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* From 490486c25d692cd3de07ab9f44ae3ac7b3caad56 Mon Sep 17 00:00:00 2001 From: yucongalicechen Date: Tue, 26 Nov 2024 13:21:42 -0500 Subject: [PATCH 10/11] add example and utility --- .../examples/diffractionobjectsexample.rst | 41 +++++++++++++++++++ .../utilities/diffractionobjectsutility.rst | 16 ++++++++ 2 files changed, 57 insertions(+) create mode 100644 doc/source/examples/diffractionobjectsexample.rst create mode 100644 doc/source/utilities/diffractionobjectsutility.rst diff --git a/doc/source/examples/diffractionobjectsexample.rst b/doc/source/examples/diffractionobjectsexample.rst new file mode 100644 index 00000000..f9def3f4 --- /dev/null +++ b/doc/source/examples/diffractionobjectsexample.rst @@ -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. diff --git a/doc/source/utilities/diffractionobjectsutility.rst b/doc/source/utilities/diffractionobjectsutility.rst new file mode 100644 index 00000000..da9ddac6 --- /dev/null +++ b/doc/source/utilities/diffractionobjectsutility.rst @@ -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. +- ``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 `. From d481253f09ea0ca698f2175f6fb6d9ac4f3e8714 Mon Sep 17 00:00:00 2001 From: yucongalicechen Date: Tue, 26 Nov 2024 18:39:06 -0500 Subject: [PATCH 11/11] edits on docs --- .../examples/diffractionobjectsexample.rst | 39 ++++++++----------- .../utilities/diffractionobjectsutility.rst | 7 +--- 2 files changed, 18 insertions(+), 28 deletions(-) diff --git a/doc/source/examples/diffractionobjectsexample.rst b/doc/source/examples/diffractionobjectsexample.rst index f9def3f4..f3114ab0 100644 --- a/doc/source/examples/diffractionobjectsexample.rst +++ b/doc/source/examples/diffractionobjectsexample.rst @@ -8,34 +8,27 @@ 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``. :: +1) Assuming we have created a ``DiffractionObject`` called my_diffraction_pattern from a measured diffraction pattern, + and we have specified the wavelength (see Section ??, to be added), + we can use the ``q_to_tth`` and ``tth_to_q`` functions to convert between q and two-theta. :: - # 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() + # Example: convert q to tth + my_diffraction_pattern.on_q = [[0, 0.2, 0.4, 0.6, 0.8, 1], [1, 2, 3, 4, 5, 6]] + my_diffraction_pattern.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. :: + To load the converted array, you can either call ``test.q_to_tth()`` or ``test.on_q[0]``. :: - # convert tth to q + # Example: 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() + my_diffraction_pattern.on_tth = [[0, 30, 60, 90, 120, 180], [1, 2, 3, 4, 5, 6]] + my_diffraction_pattern.tth_to_q() - To load the converted array, you can either call ``test.tth_to_q()`` or ``test.on_tth[0]``. + Similarly, 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() +2) Both functions require a wavelength to perform conversions. Without a wavelength, they will return empty arrays. + Therefore, we strongly encourage you to specify a wavelength when using these functions. :: - In this case, the function will return an empty array on two theta. + # Example: without wavelength specified + my_diffraction_pattern.on_q = [[0, 0.2, 0.4, 0.6, 0.8, 1], [1, 2, 3, 4, 5, 6]] + my_diffraction_pattern.q_to_tth() # returns an empty array diff --git a/doc/source/utilities/diffractionobjectsutility.rst b/doc/source/utilities/diffractionobjectsutility.rst index da9ddac6..d388e856 100644 --- a/doc/source/utilities/diffractionobjectsutility.rst +++ b/doc/source/utilities/diffractionobjectsutility.rst @@ -7,10 +7,7 @@ The ``diffpy.utils.scattering_objects.diffraction_objects`` module provides func 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. -- ``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. +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 `.