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 ADDED
File without changes
@@ -0,0 +1,9 @@
1
+ """init."""
2
+ from pkg_resources import DistributionNotFound
3
+ from pkg_resources import get_distribution
4
+
5
+ try:
6
+ __version__ = get_distribution(__name__).version
7
+ except DistributionNotFound:
8
+ # package is not installed
9
+ __version__ = "unknown"
@@ -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
+ )