diff --git a/CHANGELOG.md b/CHANGELOG.md index ad1414d0..8c383bba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,6 +80,11 @@ project. Both wrapper (Stride-level and Region) are both aware of this parameter and pass them correctly down to the respective methods. (https://github.com/mad-lab-fau/gaitmap/pull/14) +- The util method `start_end_array_to_bool_array` now assumes that the end index of all regions is inclusive. + This enables roundtrip conversion with the `bool_array_to_start_end_array` method and is in line with the definitions + used for strides, ROIs, and ZUPTs in gaitmap. + (https://github.com/mad-lab-fau/gaitmap/pull/14) + ### Removed - The `find_zupts` method of the `RtsKalmanFilter` and all deprecated arguments are now fully removed in favor of the diff --git a/gaitmap/utils/array_handling.py b/gaitmap/utils/array_handling.py index 04ea0b0a..e0c31f31 100644 --- a/gaitmap/utils/array_handling.py +++ b/gaitmap/utils/array_handling.py @@ -141,6 +141,9 @@ def start_end_array_to_bool_array(start_end_array: np.ndarray, pad_to_length: in start_end_array : array with shape (n,2) 2d-array indicating start and end values e.g. [[10,20],[20,40],[70,80]] + .. warning:: We assume that the end index is exclusiv! + This is in line with the definitions of stride and roi lists in gaitmap. + pad_to_length: int define length of resulting array if None is given the array will have the length of the last element of the initial start_end_array @@ -152,24 +155,28 @@ def start_end_array_to_bool_array(start_end_array: np.ndarray, pad_to_length: in Examples -------- - >>> example_array = np.array([[3,5],[7,8], pad_to_length=12) - >>> bool_array = start_end_array_to_bool_array(example_array) - >>> bool_array - array([False,False,False,True,True,True,False,True,True, False, False, False]) - >>> example_array = np.array([[3,5],[7,8], pad_to_length=None) - array([False,False,False,True,True,True,False,True,True]) + >>> import numpy as np + >>> example_array = np.array([[3,5],[7,8]]) + >>> start_end_array_to_bool_array(example_array, pad_to_length=12) + array([False, False, False, True, True, True, False, True, True, + False, False, False]) + >>> example_array = np.array([[3,5],[7,8]]) + >>> start_end_array_to_bool_array(example_array, pad_to_length=None) + array([False, False, False, True, True, True, False, True, True]) """ start_end_array = np.atleast_2d(start_end_array) - if pad_to_length and pad_to_length <= start_end_array[-1][-1]: - raise ValueError("Padding length must be larger than last element of start end array!") - n_elements = start_end_array[-1][-1] + 1 + n_elements = start_end_array.max() + if pad_to_length: + if pad_to_length <= n_elements: + raise ValueError("Padding length must be larger than last element of start end array!") n_elements = pad_to_length + bool_array = np.zeros(n_elements) for start, end in start_end_array: - bool_array[start : end + 1] = 1 + bool_array[start:end] = 1 return bool_array.astype(bool) diff --git a/tests/test_utils/test_array_handling.py b/tests/test_utils/test_array_handling.py index 3d0e86f5..75168e88 100644 --- a/tests/test_utils/test_array_handling.py +++ b/tests/test_utils/test_array_handling.py @@ -254,25 +254,34 @@ class TestStartEndArrayToBoolArray: def test_simple_input_no_padding(self): input_array = np.array([[2, 3], [5, 9]]) output_array = start_end_array_to_bool_array(input_array) - expected_output = np.array([0, 0, 1, 1, 0, 1, 1, 1, 1, 1]).astype(bool) + expected_output = np.array([0, 0, 1, 0, 0, 1, 1, 1, 1]).astype(bool) assert_array_equal(expected_output, output_array) def test_simple_input_1d_no_padding(self): input_array = np.array([2, 3]) output_array = start_end_array_to_bool_array(input_array, pad_to_length=5) - expected_output = np.array([0, 0, 1, 1, 0]).astype(bool) + expected_output = np.array([0, 0, 1, 0, 0]).astype(bool) assert_array_equal(expected_output, output_array) def test_simple_input_with_padding(self): input_array = np.array([[2, 3], [5, 9]]) output_array = start_end_array_to_bool_array(input_array, pad_to_length=12) - expected_output = np.array([0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0]).astype(bool) + expected_output = np.array([0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0]).astype(bool) + assert_array_equal(expected_output, output_array) + + @pytest.mark.parametrize("reverse", [True, False]) + def test_length_unsorted(self, reverse): + input_array = np.array([[5, 9], [2, 3]]) + if reverse: + input_array = input_array[::-1] + output_array = start_end_array_to_bool_array(input_array) + expected_output = np.array([0, 0, 1, 0, 0, 1, 1, 1, 1]).astype(bool) assert_array_equal(expected_output, output_array) def test_overlapping_input_with_padding(self): input_array = np.array([[2, 6], [5, 9]]) output_array = start_end_array_to_bool_array(input_array, pad_to_length=12) - expected_output = np.array([0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0]).astype(bool) + expected_output = np.array([0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0]).astype(bool) assert_array_equal(expected_output, output_array) def test_invalid_padding(self): @@ -287,6 +296,12 @@ def test_correct_output_dtype(self): output_array = start_end_array_to_bool_array(input_array) assert output_array.dtype == "bool" + def test_bool_array_round_trip(self): + input_array = np.array([[2, 3], [5, 9]]) + output_array = start_end_array_to_bool_array(input_array) + output_array = bool_array_to_start_end_array(output_array) + assert_array_equal(input_array, output_array) + class TestLocalMinimaBelowThreshold: @pytest.mark.parametrize(