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
changelog/.gitempty
ADDED
|
File without changes
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Arithmetic."""
|
|
2
|
+
from typing import Generator
|
|
3
|
+
from typing import Iterable
|
|
4
|
+
from typing import Union
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from dkist_processing_common.models.fits_access import FitsAccessBase
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def subtract_array_from_arrays(
|
|
11
|
+
arrays: Union[Iterable[np.ndarray], np.ndarray],
|
|
12
|
+
array_to_subtract: np.ndarray,
|
|
13
|
+
) -> Generator[np.ndarray, None, None]:
|
|
14
|
+
"""
|
|
15
|
+
Subtract a single array from an iterable of arrays.
|
|
16
|
+
|
|
17
|
+
This will work if a single array is used in lieu of an iterable as well.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
arrays
|
|
22
|
+
The arrays from which to subtract
|
|
23
|
+
array_to_subtract
|
|
24
|
+
The array to be subtracted
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
An generator of modified arrays
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
arrays = [arrays] if isinstance(arrays, np.ndarray) else arrays
|
|
32
|
+
for array in arrays:
|
|
33
|
+
if array.shape != array_to_subtract.shape:
|
|
34
|
+
raise ValueError(
|
|
35
|
+
f"All arrays must be the same shape. "
|
|
36
|
+
f"Shape of subtraction array = {array_to_subtract.shape} "
|
|
37
|
+
f"Shape of current array = {array.shape}"
|
|
38
|
+
)
|
|
39
|
+
yield array - array_to_subtract
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def subtract_array_from_fits_access(
|
|
43
|
+
access_objs: Union[Iterable[FitsAccessBase], FitsAccessBase], array_to_subtract: np.ndarray
|
|
44
|
+
) -> Generator[FitsAccessBase, None, None]:
|
|
45
|
+
"""
|
|
46
|
+
Subtract a single array from an iterable of FitsAccess objects.
|
|
47
|
+
|
|
48
|
+
This will work if a single object is used in lieu of an iterable as well. The objects themselves are modified in place.
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
access_objs
|
|
53
|
+
The objects from which to subtract
|
|
54
|
+
|
|
55
|
+
array_to_subtract
|
|
56
|
+
The array to be subtracted
|
|
57
|
+
|
|
58
|
+
Returns
|
|
59
|
+
-------
|
|
60
|
+
An generator of modified FitsAccess objects
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
access_objs = [access_objs] if isinstance(access_objs, FitsAccessBase) else access_objs
|
|
64
|
+
for obj in access_objs:
|
|
65
|
+
obj.data = next(subtract_array_from_arrays(obj.data, array_to_subtract))
|
|
66
|
+
yield obj
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def divide_arrays_by_array(
|
|
70
|
+
arrays: Union[Iterable[np.ndarray], np.ndarray],
|
|
71
|
+
array_to_divide_by: np.ndarray,
|
|
72
|
+
) -> Generator[np.ndarray, None, None]:
|
|
73
|
+
"""
|
|
74
|
+
Divide an iterable of arrays by a single array.
|
|
75
|
+
|
|
76
|
+
This will work if a single array is used in lieu of an iterable as well.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
arrays
|
|
81
|
+
The arrays to be divided
|
|
82
|
+
array_to_divide_by
|
|
83
|
+
The array by which to divide
|
|
84
|
+
|
|
85
|
+
Returns
|
|
86
|
+
-------
|
|
87
|
+
A generator of modified arrays
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
arrays = [arrays] if isinstance(arrays, np.ndarray) else arrays
|
|
91
|
+
for array in arrays:
|
|
92
|
+
if array.shape != array_to_divide_by.shape:
|
|
93
|
+
raise ValueError(
|
|
94
|
+
f"All arrays must be the same shape. "
|
|
95
|
+
f"Shape of array to divide by = {array_to_divide_by.shape} "
|
|
96
|
+
f"Shape of current array = {array.shape}"
|
|
97
|
+
)
|
|
98
|
+
yield array / array_to_divide_by
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def divide_fits_access_by_array(
|
|
102
|
+
access_objs: Union[Iterable[FitsAccessBase], FitsAccessBase], array_to_divide_by: np.ndarray
|
|
103
|
+
) -> Generator[FitsAccessBase, None, None]:
|
|
104
|
+
"""
|
|
105
|
+
Divide an iterable of FitsAccess objects by a single array.
|
|
106
|
+
|
|
107
|
+
This will work if a single object is used in lieu of an iterable as well. The objects themselves are modified in place.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
access_objs
|
|
112
|
+
The FitsAccess objects to be divided
|
|
113
|
+
|
|
114
|
+
array_to_divide_by
|
|
115
|
+
The array by which to divide
|
|
116
|
+
|
|
117
|
+
Returns
|
|
118
|
+
-------
|
|
119
|
+
A generator of modified FitsAccess objects
|
|
120
|
+
|
|
121
|
+
"""
|
|
122
|
+
access_objs = [access_objs] if isinstance(access_objs, FitsAccessBase) else access_objs
|
|
123
|
+
for obj in access_objs:
|
|
124
|
+
obj.data = next(divide_arrays_by_array(obj.data, array_to_divide_by))
|
|
125
|
+
yield obj
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Feature finding."""
|
|
2
|
+
from typing import Callable
|
|
3
|
+
from typing import Tuple
|
|
4
|
+
|
|
5
|
+
import numpy
|
|
6
|
+
import numpy as np
|
|
7
|
+
import peakutils as pku
|
|
8
|
+
from peakutils import gaussian_fit
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def find_px_angles(
|
|
12
|
+
hough_accumulator: numpy.ndarray,
|
|
13
|
+
theta: numpy.ndarray,
|
|
14
|
+
peak_func: Callable = gaussian_fit,
|
|
15
|
+
px_width: int = 10,
|
|
16
|
+
raise_error_on_failed_interpolation: bool = False,
|
|
17
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
18
|
+
"""
|
|
19
|
+
Find the most significant angles in a Hough transform.
|
|
20
|
+
|
|
21
|
+
Peaks in the Hough transform are first found with a simple max filter and then refined via interpolation of the
|
|
22
|
+
surrounding peak.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
hough_accumulator
|
|
27
|
+
A 2D array representing a Hough accumulator
|
|
28
|
+
|
|
29
|
+
theta
|
|
30
|
+
A 1D array of the angle values corresponding to hough_accumulator
|
|
31
|
+
|
|
32
|
+
peak_func
|
|
33
|
+
Function that will be called to detect a unique peak in the x,y data. Must have args (x, y) and return x peak value.
|
|
34
|
+
|
|
35
|
+
px_width
|
|
36
|
+
The width, in pixels, around (+/-) the initial peak to apply peak_func for peak refinement
|
|
37
|
+
|
|
38
|
+
raise_error_on_failed_interpolation
|
|
39
|
+
If True, raise an error if the peak refinement minimization fails. Otherwise return the non-refined peak in this case.
|
|
40
|
+
|
|
41
|
+
Returns
|
|
42
|
+
-------
|
|
43
|
+
peak_theta
|
|
44
|
+
The most significant angles found
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
rss = np.sqrt(np.sum(hough_accumulator**2, axis=0))
|
|
48
|
+
# This min distance limits us to only finding the single most prominent angle
|
|
49
|
+
idx = pku.indexes(rss, min_dist=theta.size)
|
|
50
|
+
peak_theta = pku.interpolate(theta, rss, ind=idx, width=px_width, func=peak_func)
|
|
51
|
+
|
|
52
|
+
# If pku.interpolate fails to find a good fit it will return idx, which is not what we want.
|
|
53
|
+
# In this case we either...
|
|
54
|
+
if peak_theta == idx:
|
|
55
|
+
if raise_error_on_failed_interpolation:
|
|
56
|
+
# ...raise an Error
|
|
57
|
+
raise RuntimeError("Interpolation of peak failed")
|
|
58
|
+
|
|
59
|
+
# ...return the non-interpolated peak angle
|
|
60
|
+
peak_theta = theta[peak_theta]
|
|
61
|
+
|
|
62
|
+
return peak_theta
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Linear algebra functions."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def nd_left_matrix_multiply(*, vector_stack: np.ndarray, matrix_stack: np.ndarray) -> np.ndarray:
|
|
6
|
+
"""
|
|
7
|
+
Left-multiply an arbitrarily dimensioned stack of vectors by a similarly dimensioned stack of matrices.
|
|
8
|
+
|
|
9
|
+
In math:
|
|
10
|
+
|
|
11
|
+
result = M @ v
|
|
12
|
+
|
|
13
|
+
where M is a matrix with dimensions ([n1, ...nn], D1, D2)
|
|
14
|
+
and v is a vector with dimensions ([n1, ...nn], D2).
|
|
15
|
+
|
|
16
|
+
The higher-order dimensions (n1, ...nn) can be anything (or nothing), but they must be the same for M and v.
|
|
17
|
+
A special case where either the matrix or vector has NO higher-order dimensions is also allowed; the result will be
|
|
18
|
+
the single matrix/vector broadcast over the higher-order dimensionality of the other element.
|
|
19
|
+
|
|
20
|
+
Processing inputs with different numbers of higher-order dummy dimensions (i.e., dimension of length 1), while
|
|
21
|
+
technically possible, is not supported because it would be unclear which input should set the output shape.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
vector_stack : np.ndarray
|
|
26
|
+
([n1, ...nn], D2) ND stack of vectors with length D2
|
|
27
|
+
|
|
28
|
+
matrix_stack : np.ndarray
|
|
29
|
+
([n1, ...nn], D1, D2) ND stack of matrices with shape (D1, D2)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
Returns
|
|
33
|
+
-------
|
|
34
|
+
np.ndarray
|
|
35
|
+
([n1, ...nn], D1) ND stack of vectors with length D1
|
|
36
|
+
"""
|
|
37
|
+
_check_nd_dimensionality(matrix_stack=matrix_stack, vector_stack=vector_stack)
|
|
38
|
+
|
|
39
|
+
matrix_shape = matrix_stack.shape[-2:]
|
|
40
|
+
vector_size = vector_stack.shape[-1]
|
|
41
|
+
if matrix_shape[-1] != vector_size:
|
|
42
|
+
raise ValueError(
|
|
43
|
+
f"Cannot perform left-multiplication with shapes {matrix_shape} @ {vector_size}."
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
result = np.sum(matrix_stack * vector_stack[..., None, :], axis=-1)
|
|
47
|
+
return result
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def nd_right_matrix_multiply(*, vector_stack: np.ndarray, matrix_stack: np.ndarray) -> np.ndarray:
|
|
51
|
+
"""
|
|
52
|
+
Right-multiply an arbitrarily dimensioned stack of vectors by a similarly dimensioned stack of matrices.
|
|
53
|
+
|
|
54
|
+
In math:
|
|
55
|
+
|
|
56
|
+
result = v @ M
|
|
57
|
+
|
|
58
|
+
where M is a matrix with dimensions ([n1, ...nn], D1, D2)
|
|
59
|
+
and v is a vector with dimensions ([n1, ...nn], D1).
|
|
60
|
+
|
|
61
|
+
The higher-order dimensions (n1, ...nn) can be anything (or nothing), but they must be the same for M and v.
|
|
62
|
+
A special case where either the matrix or vector has NO higher-order dimensions is also allowed; the result will be
|
|
63
|
+
the single matrix/vector broadcast over the higher-order dimensionality of the other element.
|
|
64
|
+
|
|
65
|
+
Processing inputs with different numbers of higher-order dummy dimensions (i.e., dimension of length 1), while
|
|
66
|
+
technically possible, is not supported because it would be unclear which input should set the output shape.
|
|
67
|
+
|
|
68
|
+
Parameters
|
|
69
|
+
----------
|
|
70
|
+
vector_stack : np.ndarray
|
|
71
|
+
([n1, ...nn], D2) ND stack of vectors with length D1
|
|
72
|
+
|
|
73
|
+
matrix_stack : np.ndarray
|
|
74
|
+
([n1, ...nn], D1, D2) ND stack of matrices with shape (D1, D2)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
Returns
|
|
78
|
+
-------
|
|
79
|
+
np.ndarray
|
|
80
|
+
([n1, ...nn], D1) ND stack of vectors with length D2
|
|
81
|
+
"""
|
|
82
|
+
_check_nd_dimensionality(matrix_stack=matrix_stack, vector_stack=vector_stack)
|
|
83
|
+
|
|
84
|
+
matrix_shape = matrix_stack.shape[-2:]
|
|
85
|
+
vector_size = vector_stack.shape[-1]
|
|
86
|
+
if matrix_shape[-2] != vector_size:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
f"Cannot perform left-multiplication with shapes {vector_size} @ {matrix_shape}."
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
result = np.sum(matrix_stack * vector_stack[..., :, None], axis=-2)
|
|
92
|
+
return result
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _check_nd_dimensionality(matrix_stack, vector_stack):
|
|
96
|
+
"""
|
|
97
|
+
Check that the matrix and vector stacks have higher-order dimensions that can be broadcast together.
|
|
98
|
+
|
|
99
|
+
Enforces matrix stack shape[:-2] equals vector stack shape[:-1]. Raises a ValueError if the condition is not met.
|
|
100
|
+
|
|
101
|
+
A special case of a matrix or vector with no higher-order dimensions is allowed.
|
|
102
|
+
"""
|
|
103
|
+
matrix_ND_shape = matrix_stack.shape[:-2]
|
|
104
|
+
vector_ND_shape = vector_stack.shape[:-1]
|
|
105
|
+
if matrix_ND_shape and vector_ND_shape and matrix_ND_shape != vector_ND_shape:
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"Inputs must have the same shape for all but the last dimensions. "
|
|
108
|
+
f"{matrix_ND_shape} != {vector_ND_shape}"
|
|
109
|
+
)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Statistics."""
|
|
2
|
+
from typing import Iterable
|
|
3
|
+
from typing import Union
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def average_numpy_arrays(arrays: Union[Iterable[np.ndarray], np.ndarray]) -> np.ndarray:
|
|
9
|
+
"""
|
|
10
|
+
Given an iterable of numpy arrays, calculate the pixel-wise average and return it in a numpy array.
|
|
11
|
+
|
|
12
|
+
This will work for a single array as well, just in case...
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
arrays
|
|
17
|
+
The arrays to be averaged
|
|
18
|
+
|
|
19
|
+
Returns
|
|
20
|
+
-------
|
|
21
|
+
The average of the input arrays
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
arrays = [arrays] if isinstance(arrays, np.ndarray) else arrays
|
|
25
|
+
count = 0 # This statement is here only to suppress an uninitialized variable warning
|
|
26
|
+
output = None
|
|
27
|
+
for count, array in enumerate(arrays):
|
|
28
|
+
if output is None:
|
|
29
|
+
output = np.array(array).astype(float)
|
|
30
|
+
else:
|
|
31
|
+
if array.shape != output.shape:
|
|
32
|
+
raise ValueError(
|
|
33
|
+
f"All arrays must be the same shape. "
|
|
34
|
+
f"Shape of initial array = {output.shape} "
|
|
35
|
+
f"Shape of current array = {array.shape}"
|
|
36
|
+
)
|
|
37
|
+
output += array
|
|
38
|
+
if output is not None:
|
|
39
|
+
return output / (count + 1)
|
|
40
|
+
raise ValueError("data_arrays is empty")
|
|
File without changes
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
rng = np.random.default_rng()
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.fixture()
|
|
8
|
+
def numpy_arrays():
|
|
9
|
+
"""
|
|
10
|
+
Create an iterable of random 10 x 10 arrays
|
|
11
|
+
|
|
12
|
+
Returns
|
|
13
|
+
-------
|
|
14
|
+
List[np.ndarray]
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
arrays = [rng.standard_normal((10, 10)), rng.standard_normal((10, 10))]
|
|
18
|
+
return arrays
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture()
|
|
22
|
+
def numpy_array(numpy_arrays):
|
|
23
|
+
"""
|
|
24
|
+
Create a single random 10 x 10 array
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
np.ndarray
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
return numpy_arrays[0]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.fixture()
|
|
35
|
+
def numpy_arrays_wrong_shape():
|
|
36
|
+
"""
|
|
37
|
+
Create an iterable of random arrays of differing sizes
|
|
38
|
+
|
|
39
|
+
Returns
|
|
40
|
+
-------
|
|
41
|
+
List[np.ndarray]
|
|
42
|
+
|
|
43
|
+
"""
|
|
44
|
+
arrays = [rng.standard_normal((10, 10)), rng.standard_normal((10, 15))]
|
|
45
|
+
return arrays
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@pytest.fixture(params=["multiple", "single"])
|
|
49
|
+
def multiple_test_arrays(request, numpy_arrays):
|
|
50
|
+
"""
|
|
51
|
+
Create an input test array or list of test arrays
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
request: Union['multiple, 'single']
|
|
56
|
+
indicates whether to return a single array or an iterable of arrays
|
|
57
|
+
numpy_arrays: List[np.ndarray]
|
|
58
|
+
the arrays from which the return value is selected
|
|
59
|
+
|
|
60
|
+
Returns
|
|
61
|
+
-------
|
|
62
|
+
Union[List[np.ndarray], np.ndarray]
|
|
63
|
+
|
|
64
|
+
"""
|
|
65
|
+
if request.param == "multiple":
|
|
66
|
+
return numpy_arrays
|
|
67
|
+
else:
|
|
68
|
+
return numpy_arrays[0]
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from typing import Generator
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pytest
|
|
5
|
+
from astropy.io import fits
|
|
6
|
+
from dkist_processing_common.models.fits_access import FitsAccessBase
|
|
7
|
+
|
|
8
|
+
from dkist_processing_math.arithmetic import divide_arrays_by_array
|
|
9
|
+
from dkist_processing_math.arithmetic import divide_fits_access_by_array
|
|
10
|
+
from dkist_processing_math.arithmetic import subtract_array_from_arrays
|
|
11
|
+
from dkist_processing_math.arithmetic import subtract_array_from_fits_access
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture(params=["multiple", "single"])
|
|
15
|
+
def multiple_test_fits_access(request, numpy_arrays):
|
|
16
|
+
|
|
17
|
+
access_list = []
|
|
18
|
+
for i, array in enumerate(numpy_arrays):
|
|
19
|
+
hdu = fits.PrimaryHDU(data=array)
|
|
20
|
+
hdu.header["TEST"] = i
|
|
21
|
+
access_list.append(FitsAccessBase(hdu=hdu))
|
|
22
|
+
if request.param == "multiple":
|
|
23
|
+
return access_list
|
|
24
|
+
return access_list[0]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_subtract_array_from_arrays(multiple_test_arrays, numpy_array):
|
|
28
|
+
"""
|
|
29
|
+
Given: an iterable of numpy arrays that are all the same shape
|
|
30
|
+
When: subtracting a fixed array from each array in the iterable
|
|
31
|
+
Then: an Generator of subtracted arrays is returned
|
|
32
|
+
"""
|
|
33
|
+
if isinstance(multiple_test_arrays, np.ndarray):
|
|
34
|
+
desired_shape = multiple_test_arrays.shape
|
|
35
|
+
desired_result = [multiple_test_arrays - numpy_array]
|
|
36
|
+
else:
|
|
37
|
+
desired_shape = multiple_test_arrays[0].shape
|
|
38
|
+
desired_result = multiple_test_arrays - numpy_array
|
|
39
|
+
result = subtract_array_from_arrays(multiple_test_arrays, numpy_array)
|
|
40
|
+
assert isinstance(result, Generator)
|
|
41
|
+
for result_array, test_array in zip(result, desired_result):
|
|
42
|
+
assert result_array.shape == desired_shape
|
|
43
|
+
assert result_array.dtype == np.result_type(test_array)
|
|
44
|
+
np.testing.assert_allclose(result_array, test_array)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_subtract_array_from_fits_access(multiple_test_fits_access, numpy_array):
|
|
48
|
+
"""
|
|
49
|
+
Given: an iterable of or single FitsAccess object(s) with data
|
|
50
|
+
When: subtracting an array from all input objects
|
|
51
|
+
Then: a generator of FitsAccess objects is returned with correctly subtracted data
|
|
52
|
+
"""
|
|
53
|
+
if isinstance(multiple_test_fits_access, FitsAccessBase):
|
|
54
|
+
desired_result = [multiple_test_fits_access.data - numpy_array]
|
|
55
|
+
desired_header_val = [multiple_test_fits_access.header["TEST"]]
|
|
56
|
+
else:
|
|
57
|
+
desired_result = [i.data - numpy_array for i in multiple_test_fits_access]
|
|
58
|
+
desired_header_val = [i.header["TEST"] for i in multiple_test_fits_access]
|
|
59
|
+
|
|
60
|
+
result = subtract_array_from_fits_access(multiple_test_fits_access, numpy_array)
|
|
61
|
+
assert isinstance(result, Generator)
|
|
62
|
+
|
|
63
|
+
for result_obj, test_array, test_header_val in zip(result, desired_result, desired_header_val):
|
|
64
|
+
assert result_obj.data.dtype == np.result_type(test_array)
|
|
65
|
+
assert result_obj.header["TEST"] == test_header_val
|
|
66
|
+
np.testing.assert_allclose(result_obj.data, test_array)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_subtract_array_from_arrays_wrong_shape(numpy_arrays_wrong_shape, numpy_array):
|
|
70
|
+
"""
|
|
71
|
+
Given: an iterable of numpy arrays that are not all the same shape
|
|
72
|
+
When: subtracting arrays
|
|
73
|
+
Then: an error is raised as the shapes are required to be the same
|
|
74
|
+
"""
|
|
75
|
+
with pytest.raises(ValueError):
|
|
76
|
+
list(subtract_array_from_arrays(numpy_arrays_wrong_shape, numpy_array))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_divide_arrays_by_array(multiple_test_arrays, numpy_array):
|
|
80
|
+
"""
|
|
81
|
+
Given: an iterable of numpy arrays that are all the same shape
|
|
82
|
+
When: dividing each array in the iterable by a fixed array
|
|
83
|
+
Then: an Generator of divided arrays is returned
|
|
84
|
+
"""
|
|
85
|
+
if isinstance(multiple_test_arrays, np.ndarray):
|
|
86
|
+
desired_shape = multiple_test_arrays.shape
|
|
87
|
+
desired_result = [multiple_test_arrays / numpy_array]
|
|
88
|
+
else:
|
|
89
|
+
desired_shape = multiple_test_arrays[0].shape
|
|
90
|
+
desired_result = multiple_test_arrays / numpy_array
|
|
91
|
+
result = divide_arrays_by_array(multiple_test_arrays, numpy_array)
|
|
92
|
+
assert isinstance(result, Generator)
|
|
93
|
+
for result_array, test_array in zip(result, desired_result):
|
|
94
|
+
assert result_array.shape == desired_shape
|
|
95
|
+
assert result_array.dtype == np.result_type(test_array)
|
|
96
|
+
np.testing.assert_allclose(result_array, test_array)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_divide_fits_access_by_array(multiple_test_fits_access, numpy_array):
|
|
100
|
+
"""
|
|
101
|
+
Given: an iterable of or single FitsAccess object(s) with data
|
|
102
|
+
When: dividing an array from all input objects
|
|
103
|
+
Then: a generator of FitsAccess objects is returned with correctly subtracted data
|
|
104
|
+
"""
|
|
105
|
+
if isinstance(multiple_test_fits_access, FitsAccessBase):
|
|
106
|
+
desired_result = [multiple_test_fits_access.data / numpy_array]
|
|
107
|
+
desired_header_val = [multiple_test_fits_access.header["TEST"]]
|
|
108
|
+
else:
|
|
109
|
+
desired_result = [i.data / numpy_array for i in multiple_test_fits_access]
|
|
110
|
+
desired_header_val = [i.header["TEST"] for i in multiple_test_fits_access]
|
|
111
|
+
|
|
112
|
+
result = divide_fits_access_by_array(multiple_test_fits_access, numpy_array)
|
|
113
|
+
assert isinstance(result, Generator)
|
|
114
|
+
|
|
115
|
+
for result_obj, test_array, test_header_val in zip(result, desired_result, desired_header_val):
|
|
116
|
+
assert result_obj.data.dtype == np.result_type(test_array)
|
|
117
|
+
assert result_obj.header["TEST"] == test_header_val
|
|
118
|
+
np.testing.assert_allclose(result_obj.data, test_array)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_divide_arrays_by_array_wrong_shape(numpy_arrays_wrong_shape, numpy_array):
|
|
122
|
+
"""
|
|
123
|
+
Given: an iterable of numpy arrays that are not all the same shape
|
|
124
|
+
When: dividing arrays
|
|
125
|
+
Then: an error is raised as the shapes are required to be the same
|
|
126
|
+
"""
|
|
127
|
+
with pytest.raises(ValueError):
|
|
128
|
+
list(divide_arrays_by_array(numpy_arrays_wrong_shape, numpy_array))
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from dkist_processing_math.feature import find_px_angles
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_find_px_angles():
|
|
8
|
+
"""
|
|
9
|
+
Given: a single numpy array
|
|
10
|
+
When: finding the most significant angles in a hough transform
|
|
11
|
+
Then: return the correct peak angle
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
theta = np.linspace(-np.pi / 4, np.pi / 4, 1500)
|
|
15
|
+
|
|
16
|
+
def Gaussian(x, mu, sig):
|
|
17
|
+
return np.exp(-np.power(x - mu, 2.0) / (2 * np.power(sig, 2.0)))
|
|
18
|
+
|
|
19
|
+
mu = 0.1
|
|
20
|
+
sigma = 0.2
|
|
21
|
+
gaussian = Gaussian(theta, mu, sigma)
|
|
22
|
+
|
|
23
|
+
H = np.zeros((20, 1500)) + gaussian[None, :]
|
|
24
|
+
desired_peaktheta = np.array(mu)
|
|
25
|
+
|
|
26
|
+
result_peaktheta = find_px_angles(H, theta)
|
|
27
|
+
assert isinstance(result_peaktheta, np.ndarray)
|
|
28
|
+
|
|
29
|
+
np.testing.assert_array_almost_equal(result_peaktheta, desired_peaktheta)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def always_fail_gaussian_fitter(*args):
|
|
33
|
+
"""This is how we force a failure to fit.
|
|
34
|
+
|
|
35
|
+
Easier than trying to contrive a signal that fails to fit.
|
|
36
|
+
"""
|
|
37
|
+
raise RuntimeError("Failing for a test")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_find_px_angles_interpolation_failure():
|
|
41
|
+
"""
|
|
42
|
+
Given: A signal that will cause a fitting error in peakutils
|
|
43
|
+
When: Trying to find the peak angle through interpolation
|
|
44
|
+
Then: The non-interpolated peak angle is returned
|
|
45
|
+
"""
|
|
46
|
+
theta = np.linspace(-np.pi / 4, np.pi / 4, 1500)
|
|
47
|
+
signal = np.random.random(1500) * 10
|
|
48
|
+
peak_idx = 400
|
|
49
|
+
signal[peak_idx] = 100.0
|
|
50
|
+
H = np.zeros((20, 1500)) + signal[None, :]
|
|
51
|
+
|
|
52
|
+
desired_peaktheta = theta[peak_idx]
|
|
53
|
+
result_peaktheta = find_px_angles(H, theta, peak_func=always_fail_gaussian_fitter)
|
|
54
|
+
|
|
55
|
+
np.testing.assert_array_equal(desired_peaktheta, result_peaktheta)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_find_px_angles_interpolation_failure_raise_error():
|
|
59
|
+
"""
|
|
60
|
+
Given: A signal that will cause a fitting error in peakutils
|
|
61
|
+
When: Trying to find the peak angle through interpolation with the flag set to raise an error on interpolation failure
|
|
62
|
+
Then: An error is raised
|
|
63
|
+
"""
|
|
64
|
+
theta = np.linspace(-np.pi / 4, np.pi / 4, 1500)
|
|
65
|
+
signal = np.random.random(1500) * 10
|
|
66
|
+
peak_idx = 400
|
|
67
|
+
signal[peak_idx] = 100.0
|
|
68
|
+
H = np.zeros((20, 1500)) + signal[None, :]
|
|
69
|
+
|
|
70
|
+
with pytest.raises(RuntimeError):
|
|
71
|
+
find_px_angles(
|
|
72
|
+
H,
|
|
73
|
+
theta,
|
|
74
|
+
peak_func=always_fail_gaussian_fitter,
|
|
75
|
+
raise_error_on_failed_interpolation=True,
|
|
76
|
+
)
|