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.
- changelog/.gitempty +0 -0
- dkist_processing_math/__init__.py +9 -0
- dkist_processing_math/arithmetic.py +125 -0
- dkist_processing_math/feature.py +62 -0
- dkist_processing_math/linear_algebra.py +109 -0
- dkist_processing_math/statistics.py +40 -0
- dkist_processing_math/tests/__init__.py +0 -0
- dkist_processing_math/tests/conftest.py +68 -0
- dkist_processing_math/tests/test_arithmetic.py +128 -0
- dkist_processing_math/tests/test_feature.py +76 -0
- dkist_processing_math/tests/test_linear_algebra.py +190 -0
- dkist_processing_math/tests/test_statistics.py +62 -0
- dkist_processing_math/tests/test_transform.py +548 -0
- dkist_processing_math/transform/__init__.py +24 -0
- dkist_processing_math/transform/affine.py +260 -0
- dkist_processing_math/transform/binary.py +37 -0
- dkist_processing_math/transform/binning.py +192 -0
- dkist_processing_math/transform/hough.py +44 -0
- dkist_processing_math-2.1.1.dist-info/METADATA +107 -0
- dkist_processing_math-2.1.1.dist-info/RECORD +29 -0
- dkist_processing_math-2.1.1.dist-info/WHEEL +5 -0
- dkist_processing_math-2.1.1.dist-info/top_level.txt +4 -0
- docs/Makefile +134 -0
- docs/changelog.rst +7 -0
- docs/conf.py +46 -0
- docs/index.rst +9 -0
- docs/make.bat +170 -0
- docs/requirements.txt +1 -0
- licenses/LICENSE.rst +11 -0
|
@@ -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))
|