Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 39 additions & 9 deletions src/diffpy/utils/diffraction_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,15 +248,45 @@ 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):
count = 0
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")
def get_array_index(self, value, xtype=None):
"""
returns the index of the closest value in the array associated with the specified xtype

Parameters
----------
xtype str
the xtype used to access the array
value float
the target value to search for

Returns
-------
the index of the value in the 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."
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The current on_xtype function does not raise an error when the specified xtype is invalid. I was thinking about letting that function raise an error in those cases, but it looks like we can handle invalid xtype together with empty array.

)
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())
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 am not super sure about the threshold here. I'm thinking about a dynamic threshold within the reasonable range of each array, but this here can cause a bit of problem if the array is sparse.


if nearest_value != 0 and (array.min() <= value <= array.max() or distance <= threshold):
Copy link
Contributor

Choose a reason for hiding this comment

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

I am rethinking all this. I wonder if we should just return i and be done with it. Raise an error if the array is empty as you do, but then just return i. what do you think? Then it is up to the user to make sure the result makes sense.

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))
Expand Down
62 changes: 62 additions & 0 deletions tests/test_diffraction_objects.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import re
from pathlib import Path

import numpy as np
Expand Down Expand Up @@ -211,6 +212,67 @@ def _test_valid_diffraction_objects(actual_diffraction_object, function, expecte
return np.allclose(actual_array, expected_array)


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 = [
# 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_dump(tmp_path, mocker):
x, y = np.linspace(0, 5, 6), np.linspace(0, 5, 6)
directory = Path(tmp_path)
Expand Down
Loading