From e4b78fdd58e1dd03c0aa4af037cac9614216e8a1 Mon Sep 17 00:00:00 2001 From: yucongalicechen Date: Tue, 3 Dec 2024 13:51:34 -0500 Subject: [PATCH 1/4] initial commit --- .../scattering_objects/diffraction_objects.py | 20 ++++++++++++++----- .../test_diffraction_objects.py | 18 +++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/diffpy/utils/scattering_objects/diffraction_objects.py b/src/diffpy/utils/scattering_objects/diffraction_objects.py index 4351c537..c881a5b5 100644 --- a/src/diffpy/utils/scattering_objects/diffraction_objects.py +++ b/src/diffpy/utils/scattering_objects/diffraction_objects.py @@ -614,14 +614,24 @@ def _set_array_from_range(self, begin, end, step_size=None, n_steps=None): return array def get_angle_index(self, angle): - count = 0 + """ + returns the index of a given angle in the angles list + + Parameters + ---------- + angle float + the angle to search for + + Returns + ------- + the index of the angle in the angles list + """ + if not hasattr(self, "angles"): + self.angles = np.array([]) for i, target in enumerate(self.angles): if angle == target: return i - else: - count += 1 - if count >= len(self.angles): - raise IndexError(f"WARNING: no angle {angle} found in angles list") + raise IndexError(f"WARNING: no angle {angle} found in angles list.") def insert_scattering_quantity( self, diff --git a/tests/diffpy/utils/scattering_objects/test_diffraction_objects.py b/tests/diffpy/utils/scattering_objects/test_diffraction_objects.py index ada14ff1..9fe4ef26 100644 --- a/tests/diffpy/utils/scattering_objects/test_diffraction_objects.py +++ b/tests/diffpy/utils/scattering_objects/test_diffraction_objects.py @@ -241,6 +241,24 @@ def _test_valid_diffraction_objects(actual_diffraction_object, function, expecte return np.allclose(actual_array, expected_array) +def test_get_angle_index(): + test = DiffractionObject() + test.angles = np.array([10, 20, 30, 40, 50, 60]) + actual_angle_index = test.get_angle_index(angle=10) + assert actual_angle_index == 0 + + +def test_get_angle_index_bad(): + test = DiffractionObject() + # empty angles list + with pytest.raises(IndexError, match="WARNING: no angle 11 found in angles list."): + test.get_angle_index(angle=11) + # pre-defined angles list + test.angles = np.array([10, 20, 30, 40, 50, 60]) + with pytest.raises(IndexError, match="WARNING: no angle 11 found in angles list."): + test.get_angle_index(angle=11) + + def test_dump(tmp_path, mocker): x, y = np.linspace(0, 5, 6), np.linspace(0, 5, 6) directory = Path(tmp_path) From 5257ee227b26acb2137f219fbf3ccdbdd28b8103 Mon Sep 17 00:00:00 2001 From: yucongalicechen Date: Wed, 4 Dec 2024 16:27:43 -0500 Subject: [PATCH 2/4] add more bad tests --- src/diffpy/utils/diffraction_objects.py | 22 ++++++----- tests/test_diffraction_objects.py | 49 ++++++++++++++++++------- 2 files changed, 47 insertions(+), 24 deletions(-) diff --git a/src/diffpy/utils/diffraction_objects.py b/src/diffpy/utils/diffraction_objects.py index e9b15696..88c4452e 100644 --- a/src/diffpy/utils/diffraction_objects.py +++ b/src/diffpy/utils/diffraction_objects.py @@ -248,25 +248,27 @@ def _set_array_from_range(self, begin, end, step_size=None, n_steps=None): array = np.linspace(begin, end, n_steps) return array - def get_angle_index(self, angle): + def get_array_index(self, xtype, value): """ - returns the index of a given angle in the angles list + returns the index of a given value in the array associated with the specified xtype Parameters ---------- - angle float - the angle to search for + xtype str + the xtype used to access the array + value float + the target value to search for Returns ------- - the index of the angle in the angles list + the index of the value in the array """ - if not hasattr(self, "angles"): - self.angles = np.array([]) - for i, target in enumerate(self.angles): - if angle == target: + if self.on_xtype(xtype) is None: + raise ValueError(_xtype_wmsg(xtype)) + for i, target in enumerate(self.on_xtype(xtype)[0]): + if value == target: return i - raise IndexError(f"WARNING: no angle {angle} found in angles list.") + raise IndexError(f"WARNING: no matching value {value} found in the {xtype} array.") def _set_xarrays(self, xarray, xtype): self.all_arrays = np.empty(shape=(len(xarray), 4)) diff --git a/tests/test_diffraction_objects.py b/tests/test_diffraction_objects.py index c1ee6f15..045c7cf2 100644 --- a/tests/test_diffraction_objects.py +++ b/tests/test_diffraction_objects.py @@ -1,10 +1,11 @@ +import re from pathlib import Path import numpy as np import pytest from freezegun import freeze_time -from diffpy.utils.diffraction_objects import DiffractionObject +from diffpy.utils.diffraction_objects import XQUANTITIES, DiffractionObject from diffpy.utils.transforms import wavelength_warning_emsg @@ -212,21 +213,41 @@ def _test_valid_diffraction_objects(actual_diffraction_object, function, expecte def test_get_angle_index(): - test = DiffractionObject() - test.angles = np.array([10, 20, 30, 40, 50, 60]) - actual_angle_index = test.get_angle_index(angle=10) - assert actual_angle_index == 0 + test = DiffractionObject( + wavelength=0.71, xarray=np.array([30, 60, 90]), yarray=np.array([1, 2, 3]), xtype="tth" + ) + actual_index = test.get_array_index(xtype="tth", value=30) + assert actual_index == 0 + + +params_index_bad = [ + # UC1: empty array + ( + [0.71, np.array([]), np.array([]), "tth", "tth", 10], + [IndexError, "WARNING: no matching value 10 found in the tth array."], + ), + # UC2: invalid xtype + ( + [None, np.array([]), np.array([]), "tth", "invalid", 10], + [ + ValueError, + f"WARNING: I don't know how to handle the xtype, 'invalid'. " + f"Please rerun specifying an xtype from {*XQUANTITIES, }", + ], + ), + # UC3: pre-defined array with non-matching value + ( + [0.71, np.array([30, 60, 90]), np.array([1, 2, 3]), "tth", "q", 30], + [IndexError, "WARNING: no matching value 30 found in the q array."], + ), +] -def test_get_angle_index_bad(): - test = DiffractionObject() - # empty angles list - with pytest.raises(IndexError, match="WARNING: no angle 11 found in angles list."): - test.get_angle_index(angle=11) - # pre-defined angles list - test.angles = np.array([10, 20, 30, 40, 50, 60]) - with pytest.raises(IndexError, match="WARNING: no angle 11 found in angles list."): - test.get_angle_index(angle=11) +@pytest.mark.parametrize("inputs, expected", params_index_bad) +def test_get_angle_index_bad(inputs, expected): + test = DiffractionObject(wavelength=inputs[0], xarray=inputs[1], yarray=inputs[2], xtype=inputs[3]) + with pytest.raises(expected[0], match=re.escape(expected[1])): + test.get_array_index(xtype=inputs[4], value=inputs[5]) def test_dump(tmp_path, mocker): From 56e72788bedea4f90ddcc8985fcc9c3b34e848a8 Mon Sep 17 00:00:00 2001 From: yucongalicechen Date: Thu, 5 Dec 2024 10:55:27 -0500 Subject: [PATCH 3/4] add more tests --- src/diffpy/utils/diffraction_objects.py | 34 ++++++++++---- tests/test_diffraction_objects.py | 61 +++++++++++++++++-------- 2 files changed, 68 insertions(+), 27 deletions(-) diff --git a/src/diffpy/utils/diffraction_objects.py b/src/diffpy/utils/diffraction_objects.py index 88c4452e..3a0f88bc 100644 --- a/src/diffpy/utils/diffraction_objects.py +++ b/src/diffpy/utils/diffraction_objects.py @@ -248,9 +248,9 @@ def _set_array_from_range(self, begin, end, step_size=None, n_steps=None): array = np.linspace(begin, end, n_steps) return array - def get_array_index(self, xtype, value): + def get_array_index(self, value, xtype=None): """ - returns the index of a given value in the array associated with the specified xtype + returns the index of the closest value in the array associated with the specified xtype Parameters ---------- @@ -263,12 +263,30 @@ def get_array_index(self, xtype, value): ------- the index of the value in the array """ - if self.on_xtype(xtype) is None: - raise ValueError(_xtype_wmsg(xtype)) - for i, target in enumerate(self.on_xtype(xtype)[0]): - if value == target: - return i - raise IndexError(f"WARNING: no matching value {value} found in the {xtype} array.") + + if xtype is None: + xtype = self.input_xtype + if self.on_xtype(xtype) is None or len(self.on_xtype(xtype)[0]) == 0: + raise ValueError( + f"The '{xtype}' array is empty. " "Please ensure it is initialized and the correct xtype is used." + ) + array = self.on_xtype(xtype)[0] + i = (np.abs(array - value)).argmin() + nearest_value = np.abs(array[i] - value) + distance = min(np.abs(value - array.min()), np.abs(value - array.max())) + threshold = 0.5 * (array.max() - array.min()) + + if nearest_value != 0 and (array.min() <= value <= array.max() or distance <= threshold): + warnings.warn( + f"WARNING: The value {value} is not an exact match of the '{xtype}' array. " + f"Returning the index of the closest value." + ) + elif distance > threshold: + raise IndexError( + f"The value {value} is too far from any value in the '{xtype}' array. " + f"Please check if you have specified the correct xtype. " + ) + return i def _set_xarrays(self, xarray, xtype): self.all_arrays = np.empty(shape=(len(xarray), 4)) diff --git a/tests/test_diffraction_objects.py b/tests/test_diffraction_objects.py index 045c7cf2..1bf3a459 100644 --- a/tests/test_diffraction_objects.py +++ b/tests/test_diffraction_objects.py @@ -5,7 +5,7 @@ import pytest from freezegun import freeze_time -from diffpy.utils.diffraction_objects import XQUANTITIES, DiffractionObject +from diffpy.utils.diffraction_objects import DiffractionObject from diffpy.utils.transforms import wavelength_warning_emsg @@ -212,42 +212,65 @@ def _test_valid_diffraction_objects(actual_diffraction_object, function, expecte return np.allclose(actual_array, expected_array) -def test_get_angle_index(): - test = DiffractionObject( - wavelength=0.71, xarray=np.array([30, 60, 90]), yarray=np.array([1, 2, 3]), xtype="tth" - ) - actual_index = test.get_array_index(xtype="tth", value=30) - assert actual_index == 0 +params_index = [ + # UC1: exact match + ([4 * np.pi, np.array([30.005, 60]), np.array([1, 2]), "tth", "tth", 30.005], [0]), + # UC2: target value lies in the array, returns the (first) closest index + ([4 * np.pi, np.array([30, 60]), np.array([1, 2]), "tth", "tth", 45], [0]), + ([4 * np.pi, np.array([30, 60]), np.array([1, 2]), "tth", "q", 0.25], [0]), + # UC3: target value out of the range but within reasonable distance, returns the closest index + ([4 * np.pi, np.array([0.25, 0.5, 0.71]), np.array([1, 2, 3]), "q", "q", 0.1], [0]), + ([4 * np.pi, np.array([30, 60]), np.array([1, 2]), "tth", "tth", 63], [1]), +] + + +@pytest.mark.parametrize("inputs, expected", params_index) +def test_get_array_index(inputs, expected): + test = DiffractionObject(wavelength=inputs[0], xarray=inputs[1], yarray=inputs[2], xtype=inputs[3]) + actual = test.get_array_index(value=inputs[5], xtype=inputs[4]) + assert actual == expected[0] params_index_bad = [ - # UC1: empty array + # UC0: empty array ( - [0.71, np.array([]), np.array([]), "tth", "tth", 10], - [IndexError, "WARNING: no matching value 10 found in the tth array."], + [2 * np.pi, np.array([]), np.array([]), "tth", "tth", 30], + [ValueError, "The 'tth' array is empty. Please ensure it is initialized and the correct xtype is used."], ), - # UC2: invalid xtype + # UC1: empty array (because of invalid xtype) ( - [None, np.array([]), np.array([]), "tth", "invalid", 10], + [2 * np.pi, np.array([30, 60]), np.array([1, 2]), "tth", "invalid", 30], [ ValueError, - f"WARNING: I don't know how to handle the xtype, 'invalid'. " - f"Please rerun specifying an xtype from {*XQUANTITIES, }", + "The 'invalid' array is empty. Please ensure it is initialized and the correct xtype is used.", ], ), - # UC3: pre-defined array with non-matching value + # UC3: value is too far from any element in the array ( - [0.71, np.array([30, 60, 90]), np.array([1, 2, 3]), "tth", "q", 30], - [IndexError, "WARNING: no matching value 30 found in the q array."], + [2 * np.pi, np.array([30, 60, 90]), np.array([1, 2, 3]), "tth", "tth", 140], + [ + IndexError, + "The value 140 is too far from any value in the 'tth' array. " + "Please check if you have specified the correct xtype.", + ], + ), + # UC4: value is too far from any element in the array (because of wrong xtype) + ( + [2 * np.pi, np.array([30, 60, 90]), np.array([1, 2, 3]), "tth", "q", 30], + [ + IndexError, + "The value 30 is too far from any value in the 'q' array. " + "Please check if you have specified the correct xtype.", + ], ), ] @pytest.mark.parametrize("inputs, expected", params_index_bad) -def test_get_angle_index_bad(inputs, expected): +def test_get_array_index_bad(inputs, expected): test = DiffractionObject(wavelength=inputs[0], xarray=inputs[1], yarray=inputs[2], xtype=inputs[3]) with pytest.raises(expected[0], match=re.escape(expected[1])): - test.get_array_index(xtype=inputs[4], value=inputs[5]) + test.get_array_index(value=inputs[5], xtype=inputs[4]) def test_dump(tmp_path, mocker): From 3dac06a06cdae0f58d75ad6f936979dd25a82403 Mon Sep 17 00:00:00 2001 From: yucongalicechen Date: Fri, 6 Dec 2024 17:42:48 -0500 Subject: [PATCH 4/4] simplify tests --- news/array_index.rst | 23 +++++++++++++ src/diffpy/utils/diffraction_objects.py | 20 ++--------- tests/test_diffraction_objects.py | 46 +++---------------------- 3 files changed, 30 insertions(+), 59 deletions(-) create mode 100644 news/array_index.rst diff --git a/news/array_index.rst b/news/array_index.rst new file mode 100644 index 00000000..6f687373 --- /dev/null +++ b/news/array_index.rst @@ -0,0 +1,23 @@ +**Added:** + +* function to return the index of the closest value to the specified value in an array. + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/utils/diffraction_objects.py b/src/diffpy/utils/diffraction_objects.py index 3a0f88bc..48fffa7f 100644 --- a/src/diffpy/utils/diffraction_objects.py +++ b/src/diffpy/utils/diffraction_objects.py @@ -266,26 +266,10 @@ def get_array_index(self, value, xtype=None): if xtype is None: xtype = self.input_xtype - if self.on_xtype(xtype) is None or len(self.on_xtype(xtype)[0]) == 0: - raise ValueError( - f"The '{xtype}' array is empty. " "Please ensure it is initialized and the correct xtype is used." - ) array = self.on_xtype(xtype)[0] + if len(array) == 0: + raise ValueError(f"The '{xtype}' array is empty. Please ensure it is initialized.") i = (np.abs(array - value)).argmin() - nearest_value = np.abs(array[i] - value) - distance = min(np.abs(value - array.min()), np.abs(value - array.max())) - threshold = 0.5 * (array.max() - array.min()) - - if nearest_value != 0 and (array.min() <= value <= array.max() or distance <= threshold): - warnings.warn( - f"WARNING: The value {value} is not an exact match of the '{xtype}' array. " - f"Returning the index of the closest value." - ) - elif distance > threshold: - raise IndexError( - f"The value {value} is too far from any value in the '{xtype}' array. " - f"Please check if you have specified the correct xtype. " - ) return i def _set_xarrays(self, xarray, xtype): diff --git a/tests/test_diffraction_objects.py b/tests/test_diffraction_objects.py index 1bf3a459..6fa81d3e 100644 --- a/tests/test_diffraction_objects.py +++ b/tests/test_diffraction_objects.py @@ -218,7 +218,7 @@ def _test_valid_diffraction_objects(actual_diffraction_object, function, expecte # UC2: target value lies in the array, returns the (first) closest index ([4 * np.pi, np.array([30, 60]), np.array([1, 2]), "tth", "tth", 45], [0]), ([4 * np.pi, np.array([30, 60]), np.array([1, 2]), "tth", "q", 0.25], [0]), - # UC3: target value out of the range but within reasonable distance, returns the closest index + # UC3: target value out of the range, returns the closest index ([4 * np.pi, np.array([0.25, 0.5, 0.71]), np.array([1, 2, 3]), "q", "q", 0.1], [0]), ([4 * np.pi, np.array([30, 60]), np.array([1, 2]), "tth", "tth", 63], [1]), ] @@ -231,46 +231,10 @@ def test_get_array_index(inputs, expected): assert actual == expected[0] -params_index_bad = [ - # UC0: empty array - ( - [2 * np.pi, np.array([]), np.array([]), "tth", "tth", 30], - [ValueError, "The 'tth' array is empty. Please ensure it is initialized and the correct xtype is used."], - ), - # UC1: empty array (because of invalid xtype) - ( - [2 * np.pi, np.array([30, 60]), np.array([1, 2]), "tth", "invalid", 30], - [ - ValueError, - "The 'invalid' array is empty. Please ensure it is initialized and the correct xtype is used.", - ], - ), - # UC3: value is too far from any element in the array - ( - [2 * np.pi, np.array([30, 60, 90]), np.array([1, 2, 3]), "tth", "tth", 140], - [ - IndexError, - "The value 140 is too far from any value in the 'tth' array. " - "Please check if you have specified the correct xtype.", - ], - ), - # UC4: value is too far from any element in the array (because of wrong xtype) - ( - [2 * np.pi, np.array([30, 60, 90]), np.array([1, 2, 3]), "tth", "q", 30], - [ - IndexError, - "The value 30 is too far from any value in the 'q' array. " - "Please check if you have specified the correct xtype.", - ], - ), -] - - -@pytest.mark.parametrize("inputs, expected", params_index_bad) -def test_get_array_index_bad(inputs, expected): - test = DiffractionObject(wavelength=inputs[0], xarray=inputs[1], yarray=inputs[2], xtype=inputs[3]) - with pytest.raises(expected[0], match=re.escape(expected[1])): - test.get_array_index(value=inputs[5], xtype=inputs[4]) +def test_get_array_index_bad(): + test = DiffractionObject(wavelength=2 * np.pi, xarray=np.array([]), yarray=np.array([]), xtype="tth") + with pytest.raises(ValueError, match=re.escape("The 'tth' array is empty. Please ensure it is initialized.")): + test.get_array_index(value=30) def test_dump(tmp_path, mocker):