dkist-processing-math 2.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,190 @@
1
+ import numpy as np
2
+ import pytest
3
+
4
+ from dkist_processing_math.linear_algebra import nd_left_matrix_multiply
5
+ from dkist_processing_math.linear_algebra import nd_right_matrix_multiply
6
+
7
+
8
+ @pytest.fixture
9
+ def D1() -> int:
10
+ return 3
11
+
12
+
13
+ @pytest.fixture
14
+ def D2() -> int:
15
+ return 4
16
+
17
+
18
+ @pytest.fixture
19
+ def extra_dims(num_dims) -> tuple[int]:
20
+ dims = np.random.randint(low=1, high=5, size=num_dims)
21
+ return tuple(dims)
22
+
23
+
24
+ @pytest.fixture
25
+ def matrix_stack(extra_dims: tuple[int, ...], single_matrix: bool, D1: int, D2: int) -> np.ndarray:
26
+ full_shape = extra_dims + (D1, D2)
27
+ if single_matrix:
28
+ full_shape = (D1, D2)
29
+ return np.random.random(full_shape)
30
+
31
+
32
+ @pytest.fixture
33
+ def left_vector_stack(extra_dims: tuple[int, ...], single_vector: bool, D2: int) -> np.ndarray:
34
+ full_shape = extra_dims + (D2,)
35
+ if single_vector:
36
+ full_shape = (D2,)
37
+ return np.random.random(full_shape)
38
+
39
+
40
+ @pytest.fixture
41
+ def right_vector_stack(extra_dims: tuple[int, ...], single_vector: bool, D1: int) -> np.ndarray:
42
+ full_shape = extra_dims + (D1,)
43
+ if single_vector:
44
+ full_shape = (D1,)
45
+ return np.random.random(full_shape)
46
+
47
+
48
+ # Stopping at 7 for no real reason. We need 0 and 2 for sure and then others to confirm generality.
49
+ # single_matrix = True tests the case where the matrix has no higher dimensions. Probably only need to test it once
50
+ # but we do it twice just because.
51
+ @pytest.mark.parametrize(
52
+ "num_dims, single_matrix, single_vector",
53
+ [
54
+ (0, False, False),
55
+ (1, False, False),
56
+ (2, False, False),
57
+ (2, True, False),
58
+ (2, False, True),
59
+ (3, False, False),
60
+ (4, False, False),
61
+ (4, False, True),
62
+ (5, False, False),
63
+ (5, True, False),
64
+ (6, False, False),
65
+ (7, False, False),
66
+ ],
67
+ )
68
+ def test_nd_left_matrix_multiply(
69
+ matrix_stack, left_vector_stack, extra_dims, single_matrix, single_vector, D1, D2, num_dims
70
+ ):
71
+ """
72
+ Given: Matrix and vector stacks with arbitrary higher dimensions
73
+ When: Left matrix multiplying them
74
+ Then: The matrix multiplication is correctly broadcast across the higher dimensions
75
+ """
76
+ if num_dims == 0:
77
+ expected = matrix_stack @ left_vector_stack
78
+ else:
79
+ flattened_size = np.prod(extra_dims)
80
+
81
+ # If single_* is True then just tile it out to match the flattened size so the loop below works
82
+ flat_matrix = (
83
+ np.reshape(matrix_stack, (flattened_size, D1, D2))
84
+ if not single_matrix
85
+ else np.tile(matrix_stack[None, ...], (flattened_size, 1, 1))
86
+ )
87
+ flat_vector = (
88
+ np.reshape(left_vector_stack, (flattened_size, D2))
89
+ if not single_vector
90
+ else np.tile(left_vector_stack[None, ...], (flattened_size, 1))
91
+ )
92
+
93
+ flat_expected = np.empty((flattened_size, D1))
94
+ for i in range(flattened_size):
95
+ flat_expected[i, :] = flat_matrix[i, :, :] @ flat_vector[i, :]
96
+
97
+ expected = np.reshape(flat_expected, extra_dims + (D1,))
98
+
99
+ value = nd_left_matrix_multiply(vector_stack=left_vector_stack, matrix_stack=matrix_stack)
100
+ np.testing.assert_allclose(expected, value, rtol=1e-13)
101
+
102
+
103
+ def test_nd_left_matrix_multiply_errors():
104
+ """
105
+ Given: Matrix and vector stacks with mismatched shapes
106
+ When: Trying to matrix multiply them
107
+ Then: The correct error is raised
108
+ """
109
+ with pytest.raises(ValueError, match="Inputs must have the same shape"):
110
+ nd_left_matrix_multiply(
111
+ vector_stack=np.random.random((1, 3, 4)), matrix_stack=np.random.random((5, 2, 3, 4))
112
+ )
113
+
114
+ with pytest.raises(ValueError, match="Cannot perform left-multiplication"):
115
+ nd_left_matrix_multiply(
116
+ vector_stack=np.random.random((4)), matrix_stack=np.random.random((4, 6))
117
+ )
118
+
119
+
120
+ # Stopping at 7 for no real reason. We need 0 and 2 for sure and then others to confirm generality.
121
+ # single_matrix = True tests the case where the matrix has no higher dimensions. Probably only need to test it once
122
+ # but we do it twice just because.
123
+ @pytest.mark.parametrize(
124
+ "num_dims, single_matrix, single_vector",
125
+ [
126
+ (0, False, False),
127
+ (1, False, False),
128
+ (2, False, False),
129
+ (2, True, False),
130
+ (2, False, True),
131
+ (3, False, False),
132
+ (3, True, False),
133
+ (4, False, False),
134
+ (5, False, False),
135
+ (5, False, True),
136
+ (6, False, False),
137
+ (7, False, False),
138
+ ],
139
+ )
140
+ def test_nd_right_matrix_multiply(
141
+ matrix_stack, right_vector_stack, extra_dims, single_matrix, single_vector, D1, D2, num_dims
142
+ ):
143
+ """
144
+ Given: Matrix and vector stacks with arbitrary higher dimensions
145
+ When: Right matrix multiplying them
146
+ Then: The matrix multiplication is correctly broadcast across the higher dimensions
147
+ """
148
+ if num_dims == 0:
149
+ expected = right_vector_stack @ matrix_stack
150
+ else:
151
+ flattened_size = np.prod(extra_dims)
152
+
153
+ # If single_* is True then just tile it out to match the flattened size so the loop below works
154
+ flat_matrix = (
155
+ np.reshape(matrix_stack, (flattened_size, D1, D2))
156
+ if not single_matrix
157
+ else np.tile(matrix_stack[None, ...], (flattened_size, 1, 1))
158
+ )
159
+
160
+ flat_vector = (
161
+ np.reshape(right_vector_stack, (flattened_size, D1))
162
+ if not single_vector
163
+ else np.tile(right_vector_stack[None, ...], (flattened_size, 1))
164
+ )
165
+
166
+ flat_expected = np.empty((flattened_size, D2))
167
+ for i in range(flattened_size):
168
+ flat_expected[i, :] = flat_vector[i, :] @ flat_matrix[i, :, :]
169
+
170
+ expected = np.reshape(flat_expected, extra_dims + (D2,))
171
+
172
+ value = nd_right_matrix_multiply(vector_stack=right_vector_stack, matrix_stack=matrix_stack)
173
+ np.testing.assert_allclose(expected, value, rtol=1e-13)
174
+
175
+
176
+ def test_nd_right_matrix_multiply_errors():
177
+ """
178
+ Given: Matrix and vector stacks with mismatched shapes
179
+ When: Trying to matrix multiply them
180
+ Then: The correct error is raised
181
+ """
182
+ with pytest.raises(ValueError, match="Inputs must have the same shape"):
183
+ nd_left_matrix_multiply(
184
+ vector_stack=np.random.random((1, 3, 4)), matrix_stack=np.random.random((5, 2, 3, 4))
185
+ )
186
+
187
+ with pytest.raises(ValueError, match="Cannot perform left-multiplication"):
188
+ nd_left_matrix_multiply(
189
+ vector_stack=np.random.random((4)), matrix_stack=np.random.random((4, 6))
190
+ )
@@ -0,0 +1,62 @@
1
+ import numpy as np
2
+ import pytest
3
+
4
+ from dkist_processing_math.statistics import average_numpy_arrays
5
+
6
+
7
+ @pytest.fixture()
8
+ def int_arrays_with_large_values():
9
+ """Some arrays that when all added together will overflow their datatype"""
10
+ arrays = [(np.zeros((10, 10)) + 10000 + (5000 * i)).astype(">i2") for i in range(4)]
11
+ return arrays
12
+
13
+
14
+ def test_average_numpy_arrays_wrong_shape(numpy_arrays_wrong_shape):
15
+ """
16
+ Given: an iterable of numpy arrays that are not all the same shape
17
+ When: averaging arrays
18
+ Then: an error is raised as the shapes are required to be the same
19
+ """
20
+ with pytest.raises(ValueError):
21
+ average_numpy_arrays(numpy_arrays_wrong_shape)
22
+
23
+
24
+ def test_average_numpy_arrays(multiple_test_arrays):
25
+ """
26
+ Given: an iterable of numpy arrays that are all the same shape
27
+ When: calculating the average
28
+ Then: a numpy array containing the average is returned
29
+ """
30
+ if isinstance(multiple_test_arrays, np.ndarray):
31
+ desired_shape = multiple_test_arrays.shape
32
+ desired_result = multiple_test_arrays
33
+ else:
34
+ desired_shape = multiple_test_arrays[0].shape
35
+ desired_result = np.mean(np.array(multiple_test_arrays), axis=0)
36
+ result = average_numpy_arrays(multiple_test_arrays)
37
+ assert isinstance(result, np.ndarray)
38
+ # Dividing an ndarray by an integer is a floating point division
39
+ # and the result is always dtype=float
40
+ assert result.dtype == float
41
+ assert result.shape == desired_shape
42
+ np.testing.assert_allclose(result, desired_result)
43
+
44
+
45
+ def test_average_numpy_arrays_empty_list():
46
+ """
47
+ Given: an empty iterable of numpy arrays
48
+ When: calculating the average
49
+ Then: an error is raised
50
+ """
51
+ with pytest.raises(ValueError):
52
+ average_numpy_arrays([])
53
+
54
+
55
+ def test_average_with_large_values(int_arrays_with_large_values):
56
+ """
57
+ Given: a list of arrays with values that would overflow their datatype if they were all summed
58
+ When: averaging the arrays
59
+ Then: the correct values are returned
60
+ """
61
+ expected = np.ones((10, 10)) * 17500
62
+ np.testing.assert_equal(expected, average_numpy_arrays(int_arrays_with_large_values))