computils 0.0.1__tar.gz

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 k-moussa
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.1
2
+ Name: computils
3
+ Version: 0.0.1
4
+ Summary: A package collecting functionality for various numerical computations (numerical differentiation, interpolation, optimization, sorting, ).
5
+ Author-email: Karim Moussa <research@k-moussa.com>
6
+ Project-URL: Homepage, https://github.com/k-moussa/computils
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.7
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+
14
+ # computils
15
+ A package collecting utilities and other convenient functionality for various numerical computations:
16
+ * Numerical differentiation: based on finite differences.
17
+ * Interpolation: provide a common interface for several useful interpolators from the excellent 'splines' and 'scipy.interpolate' packages.
18
+ * Numerical optimization: transformation functions to impose constraints using unconstrained optimization algorithms.
19
+ * Fast or robust python implementations of certain matrix operations.
20
+ * Sorting, timing, and other utilities.
@@ -0,0 +1,7 @@
1
+ # computils
2
+ A package collecting utilities and other convenient functionality for various numerical computations:
3
+ * Numerical differentiation: based on finite differences.
4
+ * Interpolation: provide a common interface for several useful interpolators from the excellent 'splines' and 'scipy.interpolate' packages.
5
+ * Numerical optimization: transformation functions to impose constraints using unconstrained optimization algorithms.
6
+ * Fast or robust python implementations of certain matrix operations.
7
+ * Sorting, timing, and other utilities.
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = [
3
+ "setuptools>=61.0",
4
+ "numpy >= 1.26.4",
5
+ "scipy >= 1.13.0",
6
+ "numba >= 0.59.1",
7
+ "splines >= 0.3.1",
8
+ ]
9
+ build-backend = "setuptools.build_meta"
10
+
11
+ [project]
12
+ name = "computils"
13
+ version = "0.0.1"
14
+ authors = [
15
+ { name="Karim Moussa", email="research@k-moussa.com" },
16
+ ]
17
+ description = "A package collecting functionality for various numerical computations (numerical differentiation, interpolation, optimization, sorting, )."
18
+ readme = "README.md"
19
+ requires-python = ">=3.7"
20
+ classifiers = [
21
+ "Programming Language :: Python :: 3",
22
+ "License :: OSI Approved :: MIT License",
23
+ "Operating System :: OS Independent",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/k-moussa/computils"
28
+ # Issues = "https://github.com/pypa/sampleproject/issues"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,22 @@
1
+ """ This module serves as the interface of the computils package.
2
+
3
+ References:
4
+ FC80: Frederick N. Fritsch and Ralph E. Carlson. Monotone piecewise cubic interpolation. SIAM Journal on
5
+ Numerical Analysis, 17(2):238–246, 1980. doi:10.1137/0717021.
6
+ FB84: Frederick N. Fritsch and Judy Butland. A method for constructing local monotone piecewise cubic
7
+ interpolants. SIAM Journal on Scientific and Statistical Computing, 5(2):300–304, 1984. doi:10.1137/0905021.
8
+ """
9
+
10
+ from .globals import *
11
+ from .factory import create_interpolator
12
+ from .sorting_algorithms import index_eq, find_le, find_ge, find_gt, find_lt
13
+ from .finite_difference import compute_derivative, compute_gradient, compute_jacobian, compute_hessian
14
+ from .type_utils import size
15
+ from .fast_eval import fast_matrix_inversion, fast_determinant, fast_quadratic_form, fast_diag_mult_AD, \
16
+ fast_diag_mult_DA, fast_columnwise_bilinear_form
17
+ from .gaussian_elim_spp import compute_inverse_using_gauss_elim_spp
18
+ from .matrix_computations import robust_inverse, is_diagonal
19
+ from .parameter_transformations import impose_lower_bound, inverse_impose_lower_bound, impose_upper_bound, \
20
+ inverse_impose_upper_bound, impose_bounds, inverse_impose_bounds, impose_upper_bound_sum, \
21
+ inverse_impose_upper_bound_sum
22
+ from .performance_checking import compute_average_running_time
@@ -0,0 +1,12 @@
1
+ """ This module is used to instantiate classes in the package. """
2
+
3
+ from .globals import *
4
+ from .interpolation import InternalInterpolator
5
+
6
+
7
+ def create_interpolator(x: np.ndarray,
8
+ y: np.ndarray,
9
+ inter_type: InterpolationType,
10
+ extra_type: ExtrapolationType = ExtrapolationType.nan) -> Interpolator:
11
+
12
+ return InternalInterpolator(x=x, y=y, inter_type=inter_type, extra_type=extra_type)
@@ -0,0 +1,68 @@
1
+ """ This module collects functionality for fast evaluation of matrix operations. """
2
+
3
+ import numpy as np
4
+ from scipy.sparse import issparse
5
+ from scipy.sparse.linalg import inv as sparse_inv
6
+ from scipy.linalg import inv, det
7
+
8
+
9
+ def fast_matrix_inversion(M: np.ndarray) -> np.ndarray:
10
+ if M.size == 1:
11
+ return 1.0 / M
12
+ elif issparse(M):
13
+ return sparse_inv(M)
14
+ else:
15
+ return inv(M)
16
+
17
+
18
+ def fast_determinant(M: np.ndarray) -> float:
19
+ if M.size == 1:
20
+ return np.abs(M[0, 0])
21
+ else:
22
+ return det(M)
23
+
24
+
25
+ def fast_quadratic_form(A: np.ndarray,
26
+ x: np.ndarray) -> float:
27
+ return (np.dot(x.T, np.dot(A, x)))[0, 0]
28
+
29
+
30
+ def fast_diag_mult_AD(A: np.ndarray,
31
+ D: np.ndarray) -> np.ndarray:
32
+ return np.multiply(A, np.diag(D))
33
+
34
+
35
+ def fast_diag_mult_DA(A: np.ndarray,
36
+ D: np.ndarray) -> np.ndarray:
37
+ return np.multiply(np.diag(D)[:, None], A)
38
+
39
+
40
+ def fast_columnwise_bilinear_form(A: np.ndarray,
41
+ B: np.ndarray,
42
+ C: np.ndarray) -> np.ndarray:
43
+ """ Efficiently compute the bilinear form A_i.T @ B @ C_i for i = 1,...,n, with A_i and C_i denoting the columns
44
+ of the matrices A_i and C_i, respectively.
45
+
46
+ :param A: (m, n) np array
47
+ :param B: (m, m) np array
48
+ :param C: (m, n) np array
49
+ :return: an (n,) np array containing the results
50
+ """
51
+
52
+ return ((A.T @ B) * C.T).sum(axis=1)
53
+
54
+
55
+ if __name__ == "__main__":
56
+ A = np.array([[1, 2], [3, 5]])
57
+ B = 3.0 * A + 4.2
58
+ C = np.array([[9, 4], [2, 1]])
59
+ x = np.array([[9], [1]])
60
+
61
+ from computils.performance_checking import compute_average_running_time
62
+ n_repeats = 1000
63
+ regular_quadratic_form = lambda M, z: z.T @ M @ z
64
+ average_time_regular = compute_average_running_time(regular_quadratic_form, args=(A, x), n_repeats=n_repeats)
65
+ average_time_fast_eval = compute_average_running_time(fast_quadratic_form, args=(A, x), n_repeats=n_repeats)
66
+
67
+ print("Regular / fast eval time = {:f}".format(average_time_regular/average_time_fast_eval))
68
+ pass
@@ -0,0 +1,240 @@
1
+ """ This module contains functionality for the numerical computation of derivatives using finite difference
2
+ based on the discussion in Numerical Recipes. """
3
+
4
+ import numpy as np
5
+ from typing import Callable, Final
6
+ from .globals import CUBE_ROOT_MACHINE_EPS, FOURTH_ROOT_MACHINE_EPS, FloatOrArray
7
+ from .type_utils import size as size_func
8
+
9
+ _LOWER_BOUND_CHARACTERISTIC_SCALE: Final = 0.001
10
+
11
+
12
+ def compute_derivative(func: Callable[[float], float],
13
+ x: FloatOrArray,
14
+ order: int,
15
+ args: tuple = (),
16
+ step_size: float = None) -> FloatOrArray:
17
+ """ Computes the first- or second-order derivative using finite differences for a given function at the point x.
18
+
19
+ Remarks: x is assumed to correspond to the first parameter of func.
20
+
21
+ :param func: a function f: R -> R, with x corresponding to its first parameter.
22
+ :param x: the point(s) of interest.
23
+ :param order: 1 or 2, the order of which to compute the derivative.
24
+ :param args: additional arguments to the function in the correct order.
25
+ :param step_size: a scalar > 0.
26
+ :return: the fd estimate(s) of the first-order derivative.
27
+ """
28
+
29
+ if order != 1 and order != 2:
30
+ raise RuntimeError("order must be 1 or 2.")
31
+
32
+ scalar_input = size_func(x) == 1
33
+ if scalar_input:
34
+ x = np.array([x])
35
+
36
+ fd_derivatives = np.full(fill_value=np.nan, shape=x.shape)
37
+ x_arg = np.full(fill_value=np.nan, shape=(1,))
38
+ for i in range(x.size):
39
+ x_arg[0] = x[i]
40
+ if order == 1:
41
+ fd_derivatives[i] = compute_gradient(func=func, x=x_arg, args=args, step_size=step_size)[0]
42
+ else: # order == 2:
43
+ fd_derivatives[i] = compute_hessian(func=func, x=x_arg, args=args, step_size=step_size)[0, 0]
44
+
45
+ if scalar_input:
46
+ return fd_derivatives[0]
47
+ else:
48
+ return fd_derivatives
49
+
50
+
51
+ def compute_gradient(func: Callable[..., float],
52
+ x: np.ndarray,
53
+ args: tuple = (),
54
+ step_size: float = None) -> np.ndarray:
55
+ """ Computes the gradient using finite differences for a given function at the point x.
56
+
57
+ Remarks: x is assumed to correspond to the first parameter of func.
58
+
59
+ :param func: a function f: R^n -> R, with x corresponding to its first parameter.
60
+ :param x: an (n,) np array corresponding to the point of interest.
61
+ :param args: additional arguments to the function in the correct order.
62
+ :param step_size: a scalar > 0.
63
+ :return: an (n, 1) array containing the numerical approximation of the gradient.
64
+ """
65
+
66
+ jacobian = compute_jacobian(func=func, x=x, args=args, step_size=step_size)
67
+ gradient = jacobian.T
68
+ return gradient
69
+
70
+
71
+ def compute_jacobian(func: Callable[[np.ndarray], np.ndarray],
72
+ x: np.ndarray,
73
+ args: tuple = (),
74
+ step_size: float = None) -> np.ndarray:
75
+ """ Computes the Jacobian using finite differences for a given function at the point x.
76
+
77
+ :param func: a function f: R^n -> R^m (i.e. returns an (m,) np array of function values), with x corresponding to
78
+ its first parameter.
79
+ :param x: an (n,) np array corresponding to the point of interest.
80
+ :param args: additional arguments to the function in the correct order.
81
+ :param step_size: a scalar > 0.
82
+ :return: an (m, n) array containing the numerical approximation of the Jacobian.
83
+ """
84
+
85
+ func = _get_func_given_args(func, args)
86
+
87
+ function_value = func(x)
88
+ if isinstance(function_value, np.ndarray):
89
+ m = func(x).size
90
+ else:
91
+ m = 1
92
+
93
+ n = x.size
94
+ jacobian = np.zeros((m, n))
95
+ step_sizes = get_step_sizes(x, derivative_order=1, step_size=step_size)
96
+
97
+ for i in range(n):
98
+ selection_vector = _get_selection_vector(n, index=i)
99
+
100
+ if m == 1:
101
+ jacobian[0, i] = _compute_1st_derivative_fd(func, x, step_sizes[i], selection_vector)
102
+ else:
103
+ jacobian[:, i] = _compute_1st_derivative_fd(func, x, step_sizes[i], selection_vector)
104
+
105
+ return jacobian
106
+
107
+
108
+ def compute_hessian(func: Callable[[np.ndarray], float],
109
+ x: np.ndarray,
110
+ args: tuple = (),
111
+ step_size: float = None) -> np.ndarray:
112
+ """ Computes the Hessian matrix using finite differences for a given function at the point x.
113
+
114
+ :param func: a function f: R^n -> R, with x corresponding to its first parameter..
115
+ :param x: an (n,) np array corresponding to the point of interest.
116
+ :param args: additional arguments to the function in the correct order.
117
+ :param step_size: a scalar > 0.
118
+ :return: an (n, n) array containing the numerical approximation of the hessian.
119
+ """
120
+
121
+ func = _get_func_given_args(func, args)
122
+
123
+ n = x.size
124
+ hessian = np.zeros((n, n))
125
+ step_sizes = get_step_sizes(x, derivative_order=2, step_size=step_size)
126
+
127
+ for i in range(n):
128
+ for j in range(i, n):
129
+ step_size = step_sizes[i, j]
130
+ selection_vector_i = _get_selection_vector(n, index=i)
131
+ selection_vector_j = _get_selection_vector(n, index=j)
132
+ hessian[i, j] = _compute_2nd_derivative_fd(func, x, step_size, selection_vector_i, selection_vector_j)
133
+
134
+ diagonal = np.diag(hessian).copy()
135
+ hessian += hessian.T
136
+ np.fill_diagonal(hessian, diagonal)
137
+
138
+ return hessian
139
+
140
+
141
+ def _get_func_given_args(func: Callable[..., FloatOrArray], args: tuple = ()) -> Callable[[np.ndarray], FloatOrArray]:
142
+ return lambda x: func(x, *args)
143
+
144
+
145
+ def get_step_sizes(x: np.ndarray,
146
+ derivative_order: int,
147
+ step_size: float = None) -> np.ndarray:
148
+ """ Returns an (x.size, 1) array for derivative_order 1, and an (x.size, x.size) array for derivative_order 2 """
149
+
150
+ if step_size is None:
151
+ optimal_relative_step_size = _get_optimal_relative_step_size(derivative_order)
152
+ abs_curvature_scale_estimates = _get_abs_curvature_scale_estimates(x, derivative_order)
153
+ optimal_step_sizes = optimal_relative_step_size * abs_curvature_scale_estimates
154
+
155
+ # trick from Numerical Recipes to ensure that x + h and x differ by an exactly-representable number
156
+ x_2dim = x.copy()
157
+ x_2dim.shape = (x_2dim.size, 1)
158
+ temp = optimal_step_sizes + x_2dim
159
+ optimal_step_sizes = temp - x_2dim
160
+
161
+ return optimal_step_sizes
162
+
163
+ elif step_size <= 0.0:
164
+ raise RuntimeError("step_size {:.2f} <= 0 not allowed..".format(step_size))
165
+ else:
166
+ if derivative_order == 1:
167
+ shape = (x.size, 1)
168
+ elif derivative_order == 2:
169
+ shape = (x.size, x.size)
170
+ else:
171
+ raise RuntimeError("derivative_order {:} not implemented.".format(derivative_order))
172
+
173
+ step_sizes = np.full(shape=shape, fill_value=step_size)
174
+ return step_sizes
175
+
176
+
177
+ def _get_abs_curvature_scale_estimates(x: np.ndarray,
178
+ derivative_order: int) -> np.ndarray:
179
+ """ Returns an (x.size, 1) array for derivative_order 1, and an (x.size, x.size) array for derivative_order 2 """
180
+
181
+ if derivative_order == 1:
182
+ abs_curvature_scale_estimates = np.clip(np.abs(x), a_min=_LOWER_BOUND_CHARACTERISTIC_SCALE, a_max=np.inf)
183
+ abs_curvature_scale_estimates.shape = (x.size, 1)
184
+ return abs_curvature_scale_estimates
185
+ elif derivative_order == 2:
186
+ abs_x = np.abs(x)
187
+ abs_x.shape = (abs_x.size, 1)
188
+ abs_curvature_scale_estimates = (abs_x + abs_x.T) / 2.0 # use the average argument values as estimates
189
+ return np.clip(abs_curvature_scale_estimates, a_min=_LOWER_BOUND_CHARACTERISTIC_SCALE, a_max=np.inf)
190
+ else:
191
+ raise RuntimeError("derivative_order {:} not implemented.".format(derivative_order))
192
+
193
+
194
+ def _get_optimal_relative_step_size(derivative_order: int) -> float:
195
+ if derivative_order == 1:
196
+ return CUBE_ROOT_MACHINE_EPS
197
+ elif derivative_order == 2:
198
+ return FOURTH_ROOT_MACHINE_EPS
199
+ else:
200
+ raise RuntimeError("derivative_order {:} not implemented.".format(derivative_order))
201
+
202
+
203
+ def _get_selection_vector(size: int,
204
+ index: int) -> np.ndarray:
205
+ selection_vector = np.zeros((size,))
206
+ selection_vector[index] = 1.0
207
+
208
+ return selection_vector
209
+
210
+
211
+ def _compute_1st_derivative_fd(func: Callable[[np.ndarray], FloatOrArray],
212
+ x: np.ndarray,
213
+ step_size: float,
214
+ selection_vector: np.ndarray) -> FloatOrArray:
215
+ """ Computes either a single 1st-order derivative, or an entire column of the Jacobian matrix. """
216
+
217
+ step_size_selection_vector = step_size * selection_vector
218
+ forward_value = func(x + step_size_selection_vector)
219
+ backward_value = func(x - step_size_selection_vector)
220
+
221
+ fd_approximation = (forward_value - backward_value) / (2.0 * step_size)
222
+ return fd_approximation
223
+
224
+
225
+ def _compute_2nd_derivative_fd(func: Callable[[np.ndarray], float],
226
+ x: np.ndarray,
227
+ step_size: float,
228
+ selection_vector_i: np.ndarray,
229
+ selection_vector_j: np.ndarray) -> float:
230
+ step_size_selection_vector_i = step_size * selection_vector_i
231
+ step_size_selection_vector_j = step_size * selection_vector_j
232
+
233
+ func_value_plus_i_plus_j = func(x + step_size_selection_vector_i + step_size_selection_vector_j)
234
+ func_value_plus_i_minus_j = func(x + step_size_selection_vector_i - step_size_selection_vector_j)
235
+ func_value_minus_i_plus_j = func(x - step_size_selection_vector_i + step_size_selection_vector_j)
236
+ func_value_minus_i_minus_j = func(x - step_size_selection_vector_i - step_size_selection_vector_j)
237
+
238
+ fd_approximation = (func_value_plus_i_plus_j - func_value_plus_i_minus_j - func_value_minus_i_plus_j +
239
+ func_value_minus_i_minus_j) / (4.0 * step_size ** 2)
240
+ return fd_approximation
@@ -0,0 +1,62 @@
1
+ """ This module implements functionality related to Gaussian elimination with scaled partial pivoting. """
2
+
3
+ import numpy as np
4
+ from numba import jit
5
+
6
+
7
+ def compute_inverse_using_gauss_elim_spp(square_matrix: np.ndarray) -> np.ndarray:
8
+ """ Uses Gaussian elimination with scaled partial pivoting to compute the inverse of the given square matrix. """
9
+
10
+ n_rows = square_matrix.shape[0]
11
+ b = np.eye(n_rows)
12
+ echelon_matrix = perform_gauss_elim_spp(a=square_matrix, b=b)
13
+ reduced_matrix = perform_back_substitution(echelon_matrix)
14
+ inverse_matrix = reduced_matrix[:, n_rows:]
15
+ return inverse_matrix
16
+
17
+
18
+ def perform_gauss_elim_spp(a: np.ndarray,
19
+ b: np.ndarray) -> np.ndarray:
20
+ """ Performs Gaussian elimination with scaled partial pivoting to put the matrix [a b] in echelon form.
21
+
22
+ :param a: an (n, n) array representing a square matrix.
23
+ :param b: an (n, n_b) array.
24
+ :return: echelon_matrix: an (n, n + n_b) matrix corresponding to [a b] in reduced echelon form, in which
25
+ the rows may have been swapped.
26
+ """
27
+
28
+ echelon_matrix = np.hstack((a, b))
29
+ n_rows = a.shape[0]
30
+
31
+ for j in range(n_rows - 1):
32
+ submatrix_a_right_to_pivot = echelon_matrix[j:, (j + 1):n_rows]
33
+ max_value_per_row = np.amax(np.abs(submatrix_a_right_to_pivot), axis=1)
34
+ r = np.divide(np.abs(echelon_matrix[j:, j]), max_value_per_row)
35
+ k = j + np.argmax(r) # k = row with largest r_k
36
+
37
+ if j != k:
38
+ echelon_matrix[[j, k]] = echelon_matrix[[k, j]] # swap rows
39
+
40
+ for i in range(j + 1, n_rows):
41
+ ratio = echelon_matrix[i, j] / echelon_matrix[j, j]
42
+ echelon_matrix[i, :] -= ratio * echelon_matrix[j, :]
43
+
44
+ return echelon_matrix
45
+
46
+
47
+ def perform_back_substitution(echelon_matrix: np.ndarray) -> np.ndarray:
48
+ """ Transforms a matrix in echelon form to reduced echelon form using back substitution.
49
+
50
+ :param echelon_matrix: a matrix [A B] in echelon form.
51
+ :return: reduced_matrix: the result of the matrix [A B] after back substitution.
52
+ """
53
+
54
+ reduced_matrix = echelon_matrix.copy()
55
+ n_rows = reduced_matrix.shape[0]
56
+ for j in reversed(range(n_rows)):
57
+ reduced_matrix[j, j:] /= reduced_matrix[j, j]
58
+
59
+ for i in range(j):
60
+ reduced_matrix[i, j:] -= reduced_matrix[i, j] * reduced_matrix[j, j:]
61
+
62
+ return reduced_matrix
@@ -0,0 +1,64 @@
1
+ """ This module collects all exposed types."""
2
+
3
+ import numpy as np
4
+ from enum import Enum
5
+ from abc import ABC, abstractmethod
6
+ from typing import final, Union
7
+
8
+ MACHINE_EPS: final = float(2 ** (-53)) # max relative error corresponding to 1/2 ULP
9
+ SQUARE_ROOT_MACHINE_EPS: final = np.sqrt(MACHINE_EPS)
10
+ CUBE_ROOT_MACHINE_EPS: final = np.cbrt(MACHINE_EPS)
11
+ FOURTH_ROOT_MACHINE_EPS: final = np.power(MACHINE_EPS, 1/4)
12
+
13
+ Integer: final = Union[int, np.int8, np.int16, np.int32, np.int64, np.uint8, np.uint16, np.uint32, np.uint64]
14
+ Float: final = Union[float, np.float16, np.float32, np.float64, np.float128]
15
+ Complex: final = Union[complex, np.complex64, np.complex128, np.complex256]
16
+ Scalar: final = Union[Integer, Float, Complex]
17
+
18
+ IntOrArray: final = Union[Integer, np.ndarray]
19
+ FloatOrArray: final = Union[Float, np.ndarray]
20
+ ComplexOrArray: final = Union[Complex, np.ndarray]
21
+ ScalarOrArray: final = Union[Scalar, np.ndarray]
22
+
23
+
24
+ # 128-bit integers generated using entropy gathered from the OS, for fixing the seed of NumPy generators.
25
+ SEED128_1: final = 137088887599416403785824428186011523708
26
+ SEED128_2: final = 230453062773196406322953725600125856954
27
+ SEED128_3: final = 334883455271001690768699206953238296696
28
+ SEED128_4: final = 69805789651723487751993596682833892465
29
+ SEED128_5: final = 107997836754270478432192384195731366045
30
+ SEED128_6: final = 61384398454038591198002507585631369346
31
+ SEED128_7: final = 165303562401480433055547884045914282112
32
+ SEED128_8: final = 215418768511176275020128984871223831370
33
+ SEED128_9: final = 247520618467519227348322182729719821192
34
+ SEED128_10: final = 260473913589332506348144405693894964574
35
+
36
+
37
+ class InterpolationType(Enum):
38
+ linear = 0
39
+ ncs = 1 # natural cubic spline
40
+ ccs = 2 # clamped cubic spline
41
+ pmc = 3 # piecewise monotone cubic [FC80]
42
+ pchip = 4 # [FB84]
43
+
44
+
45
+ class ExtrapolationType(Enum):
46
+ nan = 0
47
+ flat = 1
48
+
49
+
50
+ class Interpolator(ABC):
51
+ def __init__(self,
52
+ inter_type: InterpolationType,
53
+ extra_type: ExtrapolationType):
54
+
55
+ self.inter_type: InterpolationType = inter_type
56
+ self.extra_type: ExtrapolationType = extra_type
57
+
58
+ @abstractmethod
59
+ def __call__(self, x: ScalarOrArray) -> ScalarOrArray:
60
+ """
61
+
62
+ :param x:
63
+ :return:
64
+ """
@@ -0,0 +1,104 @@
1
+ """ This module implements various interpolation methods. """
2
+
3
+ import splines
4
+ from scipy.interpolate import CubicSpline, PchipInterpolator
5
+ from typing import Callable
6
+ from .type_utils import size
7
+ from .globals import *
8
+
9
+
10
+ class InternalInterpolator(Interpolator, ABC):
11
+ def __init__(self,
12
+ x: np.ndarray,
13
+ y: np.ndarray,
14
+ inter_type: InterpolationType,
15
+ extra_type: ExtrapolationType):
16
+ """
17
+
18
+ :param x: independent variables; data must be sorted in ascending order.
19
+ :param y: dependent variables corresponding to x.
20
+ :param inter_type:
21
+ :param extra_type:
22
+ """
23
+
24
+ super().__init__(inter_type=inter_type, extra_type=extra_type)
25
+
26
+ self._x: np.ndarray = x # independent variables
27
+ self._y: np.ndarray = y # dependent variables
28
+ self._interpolant: Callable[[ScalarOrArray], ScalarOrArray] = self._get_interpolant(x=x, y=y,
29
+ inter_type=inter_type)
30
+
31
+ def __call__(self, x: ScalarOrArray) -> ScalarOrArray:
32
+ """
33
+
34
+ :param x:
35
+ :return:
36
+ """
37
+
38
+ y = self._interpolant(x)
39
+ extra_masks = self._get_extrapolation_masks(x)
40
+ no_extrapolation = np.sum(extra_masks) == 0
41
+ if no_extrapolation:
42
+ return y
43
+ else:
44
+ y[extra_masks] = self._extrapolate(x[extra_masks])
45
+
46
+ return y
47
+
48
+ def _get_extrapolation_masks(self, x: ScalarOrArray) -> IntOrArray:
49
+ """ Returns masks for points in extrapolation region. """
50
+
51
+ return self._exceeds_lhs(x) | self._exceeds_rhs(x)
52
+
53
+ def _exceeds_lhs(self, x: ScalarOrArray) -> IntOrArray:
54
+ return x < self._x[0]
55
+
56
+ def _exceeds_rhs(self, x: ScalarOrArray) -> IntOrArray:
57
+ return x > self._x[-1]
58
+
59
+ def _extrapolate(self, x_extra: ScalarOrArray) -> ScalarOrArray:
60
+ y_extra = np.full(shape=(size(x_extra),), fill_value=np.nan)
61
+ if self.extra_type is ExtrapolationType.nan:
62
+ return y_extra
63
+
64
+ lhs_masks = self._exceeds_lhs(x_extra)
65
+ rhs_masks = self._exceeds_rhs(x_extra)
66
+ if self.extra_type is ExtrapolationType.flat:
67
+ y_extra[lhs_masks] = self._y[0]
68
+ y_extra[rhs_masks] = self._y[-1]
69
+
70
+ return y_extra
71
+
72
+ @staticmethod
73
+ def _get_interpolant(x: np.ndarray,
74
+ y: np.ndarray,
75
+ inter_type: InterpolationType) -> Callable[[ScalarOrArray], ScalarOrArray]:
76
+
77
+ if inter_type is InterpolationType.linear:
78
+ interpolant = lambda z: np.interp(x=z, xp=x, fp=y)
79
+ return interpolant
80
+ elif inter_type is InterpolationType.ncs:
81
+ return CubicSpline(x=x, y=y, bc_type='natural')
82
+ elif inter_type is InterpolationType.ccs:
83
+ return CubicSpline(x=x, y=y, bc_type='clamped')
84
+ elif inter_type is InterpolationType.pmc:
85
+ return PmcSplineFunctor(x=x, y=y)
86
+ elif inter_type is InterpolationType.pchip:
87
+ return PchipInterpolator(x=x, y=y)
88
+ else:
89
+ raise RuntimeError(f"Unhandled inter_type {inter_type.name}.")
90
+
91
+
92
+ class PmcSplineFunctor:
93
+ def __init__(self,
94
+ x: np.ndarray,
95
+ y: np.ndarray):
96
+
97
+ self.x: np.ndarray = x
98
+ self._spline = splines.PiecewiseMonotoneCubic(values=y, grid=x)
99
+
100
+ def __call__(self, x: np.ndarray):
101
+ inter_masks = (x >= self.x[0]) & (x <= self.x[-1])
102
+ y_eval = np.full(shape=(size(x),), fill_value=np.nan)
103
+ y_eval[inter_masks] = self._spline.evaluate(x[inter_masks])
104
+ return y_eval
@@ -0,0 +1,23 @@
1
+ """ This module contains functionality related to matrix computations. """
2
+
3
+ import numpy as np
4
+ from numpy.linalg import solve
5
+
6
+
7
+ def robust_inverse(square_matrix: np.ndarray) -> np.ndarray:
8
+ """ Computes the inverse of the square matrix.
9
+
10
+ :param square_matrix: (n, n) array corresponding to an invertible matrix.
11
+ :return: inverse_matrix: (n, n) array.
12
+ """
13
+
14
+ if is_diagonal(square_matrix):
15
+ main_diagonal_inverse = np.divide(1.0, np.diagonal(square_matrix))
16
+ return np.diag(main_diagonal_inverse)
17
+ else:
18
+ inverse_matrix = solve(square_matrix, np.eye(square_matrix.shape[0]))
19
+ return inverse_matrix
20
+
21
+
22
+ def is_diagonal(matrix: np.ndarray) -> bool:
23
+ return np.count_nonzero(matrix - np.diag(np.diagonal(matrix))) == 0
@@ -0,0 +1,54 @@
1
+ """ This module contains transformations and their inverse functions for imposing parameter restrictions. """
2
+
3
+ import numpy as np
4
+
5
+
6
+ def impose_lower_bound(transformed_parameter: float, lower_bound: float) -> float:
7
+ return np.exp(transformed_parameter) + lower_bound
8
+
9
+
10
+ def inverse_impose_lower_bound(parameter: float, lower_bound: float) -> float:
11
+ return np.log(parameter - lower_bound)
12
+
13
+
14
+ def impose_upper_bound(transformed_parameter: float, upper_bound: float) -> float:
15
+ return -np.exp(transformed_parameter) + upper_bound
16
+
17
+
18
+ def inverse_impose_upper_bound(parameter: float, upper_bound: float) -> float:
19
+ return np.log(upper_bound - parameter)
20
+
21
+
22
+ def impose_bounds(transformed_parameter: float, lower_bound: float, upper_bound: float) -> float:
23
+ return lower_bound + (upper_bound - lower_bound)/(1.0 + np.exp(-transformed_parameter))
24
+
25
+
26
+ def inverse_impose_bounds(parameter: float, lower_bound: float, upper_bound: float) -> float:
27
+ z = (parameter - lower_bound)/(upper_bound - lower_bound)
28
+ return np.log(z) - np.log(1.0 - z)
29
+
30
+
31
+ def impose_upper_bound_sum(transformed_params: np.ndarray, upper_bound: float) -> np.ndarray:
32
+ """ Imposes an upper bound on the sum of params, such that all params are in (0, upper_bound).
33
+
34
+ :param transformed_params: (n_params,) array of transformed parameters, real numbers
35
+ :param upper_bound: real number
36
+ :return: constrained_params: (n_params,) array of parameters with constraints
37
+ """
38
+
39
+ exp_trans_params = np.exp(transformed_params)
40
+ constrained_params = upper_bound * np.divide(exp_trans_params, np.sum(exp_trans_params) + 1.0)
41
+ return constrained_params
42
+
43
+
44
+ def inverse_impose_upper_bound_sum(params: np.ndarray, upper_bound: float) -> np.ndarray:
45
+ """ Inverse transformation function of impose_upper_bound_sum.
46
+
47
+ :param params: (n_params,) array of params that are all in (0, upper_bound)
48
+ :param upper_bound: real number
49
+ :return: unconstrained_params
50
+ """
51
+
52
+ sum_params = np.sum(params)
53
+ unconstrained_params = np.log(params / (upper_bound - sum_params))
54
+ return unconstrained_params
@@ -0,0 +1,31 @@
1
+ """ This module provides functionality for checking the running time performance of code. """
2
+
3
+ from time import time
4
+ from typing import Callable
5
+
6
+
7
+ def compute_average_running_time(callback_func: Callable, args: tuple = (),
8
+ n_repeats: int = 10) -> float:
9
+ """ Computes the average running time of a function evaluation for given number of repeats.
10
+
11
+ :param callback_func: the function to be evaluated
12
+ :param args: arguments to the callback
13
+ :param n_repeats: > 1, number of repeats of the function evaluation
14
+ :return: average_running_time_in_seconds
15
+ """
16
+
17
+ t = time()
18
+
19
+ for i in range(n_repeats):
20
+ callback_func(*args)
21
+
22
+ running_time_in_seconds = time() - t
23
+ average_running_time_in_seconds = running_time_in_seconds / n_repeats
24
+ return average_running_time_in_seconds
25
+
26
+
27
+ if __name__ == "__main__":
28
+ import numpy as np
29
+ n_reps = 100
30
+ print("Average running time = {:.4f}s (repeats = {:d}).".format(
31
+ compute_average_running_time(np.exp, (np.zeros((10 ** 7,)),), n_reps), n_reps))
@@ -0,0 +1,102 @@
1
+ """ This module implements various sorting algorithms based on Python's bisect module. It is based on the implementation
2
+ suggested in https://stackoverflow.com/questions/6628744/search-for-before-and-after-values-in-a-long-sorted-list
3
+ """
4
+
5
+ import bisect
6
+ from typing import Sequence, Any, Optional
7
+
8
+
9
+ def _check_args(a: Sequence,
10
+ x: Any):
11
+ """ Checks the arguments a and x and raises a RuntimeError for invalid input. """
12
+
13
+ if len(a) == 0:
14
+ raise RuntimeError("a is empty.")
15
+
16
+
17
+ def index_eq(a: Sequence,
18
+ x: Any) -> Optional[int]:
19
+ """ Returns the index of the leftmost value in a that is exactly equal to x.
20
+
21
+ :param a:
22
+ :param x: an object with '<' and '==' operators.
23
+ :return: the index, or None if the x is not in a.
24
+ """
25
+
26
+ _check_args(a=a, x=x)
27
+
28
+ i = bisect.bisect_left(a, x)
29
+ if i != len(a) and a[i] == x:
30
+ return i
31
+ else:
32
+ return None
33
+
34
+
35
+ def find_lt(a: Sequence,
36
+ x: Any) -> Any:
37
+ """ Returns the rightmost value in a that is less than x.
38
+
39
+ :param a:
40
+ :param x: an object with '<' and '==' operators.
41
+ :return:
42
+ """
43
+
44
+ _check_args(a=a, x=x)
45
+
46
+ i = bisect.bisect_left(a, x)
47
+ if i:
48
+ return a[i-1]
49
+ raise ValueError
50
+
51
+
52
+ def find_le(a: Sequence,
53
+ x: Any) -> Any:
54
+ """ Returns the rightmost value in a that is less than or equal to x.
55
+
56
+ :param a:
57
+ :param x: an object with '<' and '==' operators.
58
+ :return:
59
+ """
60
+
61
+ _check_args(a=a, x=x)
62
+
63
+ i = bisect.bisect_right(a, x)
64
+ if i:
65
+ return a[i-1]
66
+ raise ValueError
67
+
68
+
69
+ def find_gt(a: Sequence,
70
+ x: Any) -> Any:
71
+ """ Returns the leftmost value in a that is greater than x.
72
+
73
+ :param a:
74
+ :param x: an object with '<' and '==' operators.
75
+ :return:
76
+ """
77
+
78
+ _check_args(a=a, x=x)
79
+
80
+ i = bisect.bisect_right(a, x)
81
+ if i != len(a):
82
+ return a[i]
83
+ raise ValueError
84
+
85
+
86
+ def find_ge(a: Sequence,
87
+ x: Any) -> Any:
88
+ """ Returns the leftmost value in a that is greater than or equal to x.
89
+
90
+ Remark: raises a value error if
91
+
92
+ :param a:
93
+ :param x: an object with '<' and '==' operators.
94
+ :return:
95
+ """
96
+
97
+ _check_args(a=a, x=x)
98
+
99
+ i = bisect.bisect_left(a, x)
100
+ if i != len(a):
101
+ return a[i]
102
+ raise ValueError
@@ -0,0 +1,11 @@
1
+ """ Implements helping functions for the introduced types. """
2
+
3
+ import numpy as np
4
+ from .globals import ScalarOrArray
5
+
6
+
7
+ def size(x: ScalarOrArray) -> int:
8
+ if isinstance(x, np.ndarray):
9
+ return x.size
10
+ else:
11
+ return 1
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.1
2
+ Name: computils
3
+ Version: 0.0.1
4
+ Summary: A package collecting functionality for various numerical computations (numerical differentiation, interpolation, optimization, sorting, ).
5
+ Author-email: Karim Moussa <research@k-moussa.com>
6
+ Project-URL: Homepage, https://github.com/k-moussa/computils
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.7
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+
14
+ # computils
15
+ A package collecting utilities and other convenient functionality for various numerical computations:
16
+ * Numerical differentiation: based on finite differences.
17
+ * Interpolation: provide a common interface for several useful interpolators from the excellent 'splines' and 'scipy.interpolate' packages.
18
+ * Numerical optimization: transformation functions to impose constraints using unconstrained optimization algorithms.
19
+ * Fast or robust python implementations of certain matrix operations.
20
+ * Sorting, timing, and other utilities.
@@ -0,0 +1,23 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/computils/__init__.py
5
+ src/computils/factory.py
6
+ src/computils/fast_eval.py
7
+ src/computils/finite_difference.py
8
+ src/computils/gaussian_elim_spp.py
9
+ src/computils/globals.py
10
+ src/computils/interpolation.py
11
+ src/computils/matrix_computations.py
12
+ src/computils/parameter_transformations.py
13
+ src/computils/performance_checking.py
14
+ src/computils/sorting_algorithms.py
15
+ src/computils/type_utils.py
16
+ src/computils.egg-info/PKG-INFO
17
+ src/computils.egg-info/SOURCES.txt
18
+ src/computils.egg-info/dependency_links.txt
19
+ src/computils.egg-info/top_level.txt
20
+ src/tests/matrix_computations_tests.py
21
+ src/tests/numerical_derivatives_tests.py
22
+ src/tests/parameter_transformation_tests.py
23
+ src/tests/test_utils.py
@@ -0,0 +1,2 @@
1
+ computils
2
+ tests
@@ -0,0 +1,25 @@
1
+ """ This module tests functionality related to matrix computations. """
2
+
3
+ import unittest
4
+ import numpy as np
5
+ from numpy.linalg import inv
6
+ from scipy.stats import wishart
7
+ from test_utils import compute_max_abs_diff
8
+ from computils import SEED128_1
9
+ from computils.gaussian_elim_spp import compute_inverse_using_gauss_elim_spp
10
+
11
+
12
+ class MatrixComputationsTests(unittest.TestCase):
13
+
14
+ def test_inverse_gauss_elim_spp(self):
15
+ rng = np.random.default_rng(SEED128_1)
16
+ n = 50
17
+ df = n + 1.0
18
+ scale_matrix = np.eye(n)
19
+
20
+ for i in range(100):
21
+ matrix = wishart.rvs(df=df, scale=scale_matrix, random_state=rng)
22
+ inverse_matrix = inv(matrix)
23
+ inverse_matrix_gespp = compute_inverse_using_gauss_elim_spp(matrix)
24
+ max_diff = compute_max_abs_diff(inverse_matrix, inverse_matrix_gespp)
25
+ self.assertAlmostEqual(0.0, max_diff, delta=10**-8)
@@ -0,0 +1,129 @@
1
+ """ This module tests the computation of numerical derivatives. """
2
+
3
+ import unittest
4
+ import numpy as np
5
+ from computils.finite_difference import compute_gradient, compute_jacobian, compute_hessian
6
+
7
+
8
+ def func_r2_to_r3(x: np.ndarray, scaling_factor: float = 1.0) -> np.ndarray:
9
+ """ A test function f: R^2 -> R^3
10
+
11
+ :param x: a (2,) np array of argument values.
12
+ :param scaling_factor: real number (nonzero).
13
+ :return: a (3,) array containing the function values
14
+ """
15
+
16
+ function_values = np.zeros((3,))
17
+ function_values[0] = 2.0 * x[0]
18
+ function_values[1] = func_r2_to_r(x)
19
+ function_values[2] = -3.0 * x[1]
20
+
21
+ return scaling_factor * function_values
22
+
23
+
24
+ def analytical_jacobian_test_func_r2_to_r3(x: np.ndarray) -> np.ndarray:
25
+ """ Analytical Jacobian of the test function f: R^2 -> R^3
26
+
27
+ :param x: a (2,) np array of argument values.
28
+ :return: jacobian: a (3, 2) array of first-order derivatives.
29
+ """
30
+
31
+ jacobian = np.zeros((3, 2))
32
+ jacobian[0, 0] = 2.0
33
+ jacobian[1, :] = analytical_gradient_test_func_r2_to_r(x).flatten()
34
+ jacobian[2, 1] = -3.0
35
+ return jacobian
36
+
37
+
38
+ def func_r2_to_r(x: np.ndarray, scaling_factor: float = 1.0) -> float:
39
+ """ A test function f: R^2 -> R
40
+
41
+ :param x: a (2,) array of argument values.
42
+ :param scaling_factor: real number (nonzero).
43
+ :return: the function value (scalar).
44
+ """
45
+
46
+ return scaling_factor * (2.0 * x[0] ** 2 + 4.0 * x[1] ** 3 + 6.0 * x[0] * x[1])
47
+
48
+
49
+ def analytical_gradient_test_func_r2_to_r(x: np.ndarray) -> np.ndarray:
50
+ """ Analytical gradient of the test function f: R^2 -> R
51
+
52
+ :param x: a (2,) array of argument values.
53
+ :return: gradient: a (2, 1) np array of first-order derivatives.
54
+ """
55
+
56
+ gradient = np.zeros((2, 1))
57
+ gradient[0, 0] = 4.0 * x[0] + 6.0 * x[1]
58
+ gradient[1, 0] = 12.0 * x[1] ** 2 + 6.0 * x[0]
59
+ return gradient
60
+
61
+
62
+ def analytical_hessian_test_func_r2_to_r(x: np.ndarray) -> np.ndarray:
63
+ """ Analytical hessian of the test function f: R^2 -> R
64
+
65
+ :param x: a (2,) array of argument values.
66
+ :return: hessian: a (2, 1) np array of first-order derivatives.
67
+ """
68
+
69
+ hessian = np.zeros((2, 2))
70
+ hessian[0, 0] = 4.0
71
+ hessian[0, 1] = hessian[1, 0] = 6.0
72
+ hessian[1, 1] = 24.0 * x[1]
73
+ return hessian
74
+
75
+
76
+ class FiniteDifferenceTests(unittest.TestCase):
77
+ @classmethod
78
+ def setUpClass(cls):
79
+ cls.x_values = [n * np.array([0.5, 1.0]) for n in range(-1, 10)]
80
+ cls.scaling_factor = 0.9
81
+
82
+ def test_gradient(self):
83
+ for x in self.x_values:
84
+ for include_args in (True, False):
85
+ analytical_gradient = analytical_gradient_test_func_r2_to_r(x)
86
+
87
+ if include_args:
88
+ analytical_gradient *= self.scaling_factor
89
+ numerical_gradient = compute_gradient(func_r2_to_r, x, args=(self.scaling_factor,))
90
+ else:
91
+ numerical_gradient = compute_gradient(func_r2_to_r, x)
92
+
93
+ diffs = numerical_gradient - analytical_gradient
94
+ max_diff = np.amax(np.abs(diffs))
95
+ self.assertAlmostEqual(0.0, max_diff, delta=10**-8)
96
+
97
+ def test_jacobian(self):
98
+ for x in self.x_values:
99
+ for include_args in (True, False):
100
+ analytical_jacobian = analytical_jacobian_test_func_r2_to_r3(x)
101
+
102
+ if include_args:
103
+ analytical_jacobian *= self.scaling_factor
104
+ numerical_jacobian = compute_jacobian(func_r2_to_r3, x, args=(self.scaling_factor,))
105
+ else:
106
+ numerical_jacobian = compute_jacobian(func_r2_to_r3, x)
107
+
108
+ diffs = numerical_jacobian - analytical_jacobian
109
+ max_diff = np.amax(np.abs(diffs))
110
+ self.assertAlmostEqual(0.0, max_diff, delta=10**-8)
111
+
112
+ def test_hessian(self):
113
+ for x in self.x_values:
114
+ for include_args in (True, False):
115
+ analytical_hessian = analytical_hessian_test_func_r2_to_r(x)
116
+
117
+ if include_args:
118
+ analytical_hessian *= self.scaling_factor
119
+ numerical_hessian = compute_hessian(func_r2_to_r, x, args=(self.scaling_factor,))
120
+ else:
121
+ numerical_hessian = compute_hessian(func_r2_to_r, x)
122
+
123
+ diffs = numerical_hessian - analytical_hessian
124
+ max_diff = np.amax(np.abs(diffs))
125
+ self.assertAlmostEqual(0.0, max_diff, delta=10**-6)
126
+
127
+
128
+ if __name__ == '__main__':
129
+ unittest.main()
@@ -0,0 +1,122 @@
1
+ """ This module implements tests for the parameter transformation functions. """
2
+
3
+ import unittest
4
+ import numpy as np
5
+ import computils.parameter_transformations as pt
6
+
7
+
8
+ class ParameterTransformationTests(unittest.TestCase):
9
+ def test_impose_lower_bound(self):
10
+ n_values = 100
11
+ lower_bounds = np.linspace(start=-10.0, stop=10.0, num=n_values)
12
+
13
+ for lower_bound in lower_bounds:
14
+ expected_value = 1.0 + lower_bound
15
+ actual_value = pt.impose_lower_bound(transformed_parameter=0.0, lower_bound=lower_bound)
16
+
17
+ self.assertAlmostEqual(expected_value, actual_value, delta=10 ** (-10))
18
+
19
+ def test_consistency_lower_bound_transformations(self):
20
+ """ Tests consistency of the lower bound transformations by checking whether their
21
+ composition yields the identity function. """
22
+
23
+ n_values = 100
24
+ lower_bounds = np.linspace(start=-10.0, stop=10.0, num=n_values)
25
+ transformed_values = np.linspace(start=-10.0, stop=10.0, num=n_values)
26
+
27
+ for lower_bound in lower_bounds:
28
+ for value in transformed_values:
29
+ actual_value = pt.inverse_impose_lower_bound(
30
+ pt.impose_lower_bound(value, lower_bound), lower_bound)
31
+
32
+ self.assertAlmostEqual(value, actual_value, delta=10 ** (-10))
33
+
34
+ def test_impose_upper_bound(self):
35
+ n_values = 100
36
+ upper_bounds = np.linspace(start=-10.0, stop=10.0, num=n_values)
37
+
38
+ for upper_bound in upper_bounds:
39
+ expected_value = -1.0 + upper_bound
40
+ actual_value = pt.impose_upper_bound(transformed_parameter=0.0, upper_bound=upper_bound)
41
+
42
+ self.assertAlmostEqual(expected_value, actual_value, delta=10 ** (-10))
43
+
44
+ def test_consistency_upper_bound_transformations(self):
45
+ """ Tests consistency of the upper bound transformations by checking whether their
46
+ composition yields the identity function. """
47
+
48
+ n_values = 100
49
+ upper_bounds = np.linspace(start=-10.0, stop=10.0, num=n_values)
50
+ transformed_values = np.linspace(start=-10.0, stop=10.0, num=n_values)
51
+
52
+ for upper_bound in upper_bounds:
53
+ for value in transformed_values:
54
+ actual_value = pt.inverse_impose_upper_bound(
55
+ pt.impose_upper_bound(value, upper_bound), upper_bound)
56
+
57
+ self.assertAlmostEqual(value, actual_value, delta=10 ** (-10))
58
+
59
+ def test_impose_bounds(self):
60
+ n_values = 100
61
+ lower_bounds = np.linspace(start=-10.0, stop=10.0, num=n_values)
62
+ upper_bounds = np.linspace(start=-10.0, stop=10.0, num=n_values)
63
+
64
+ for lower_bound in lower_bounds:
65
+ for upper_bound in upper_bounds:
66
+ expected_value = (lower_bound + upper_bound) / 2.0
67
+ actual_value = pt.impose_bounds(
68
+ transformed_parameter=0.0, lower_bound=lower_bound, upper_bound=upper_bound)
69
+
70
+ self.assertAlmostEqual(expected_value, actual_value, delta=10 ** (-10))
71
+
72
+ def test_consistency_bounds_transformations(self):
73
+ """ Tests consistency of the double bound transformations by checking whether their
74
+ composition yields the identity function. """
75
+
76
+ n_values = 100
77
+ lower_bounds = np.linspace(start=-10.0, stop=10.0, num=n_values)
78
+ transformed_values = np.linspace(start=-10.0, stop=10.0, num=n_values)
79
+
80
+ for lower_bound in lower_bounds:
81
+ upper_bound = lower_bound + 1.0
82
+
83
+ for value in transformed_values:
84
+ actual_value = pt.inverse_impose_bounds(
85
+ pt.impose_bounds(value, lower_bound, upper_bound), lower_bound, upper_bound)
86
+
87
+ self.assertAlmostEqual(value, actual_value, delta=10 ** (-10))
88
+
89
+ def test_impose_upper_bound_sum(self):
90
+ n_params = 3
91
+ transformed_params = np.zeros((n_params,))
92
+ n_values = 100
93
+ upper_bounds = np.linspace(start=-10.0, stop=10.0, num=n_values)
94
+
95
+ for upper_bound in upper_bounds:
96
+ expected_param_value = upper_bound / (n_params + 1.0)
97
+ actual_params = pt.impose_upper_bound_sum(transformed_params=transformed_params, upper_bound=upper_bound)
98
+
99
+ for param in actual_params:
100
+ self.assertAlmostEqual(expected_param_value, param, delta=10 ** (-10))
101
+
102
+ def test_consistency_upper_bound_sum_transformations(self):
103
+ """ Tests consistency of the upper bound sum transformations by checking whether their
104
+ composition yields the identity function. """
105
+
106
+ n_values = 100
107
+ upper_bounds = np.linspace(start=-10.0, stop=10.0, num=n_values)
108
+ transformed_values = np.random.normal(size=(n_values, 2))
109
+
110
+ for upper_bound in upper_bounds:
111
+ for i in range(n_values):
112
+ transformed_params = transformed_values[i]
113
+ result = pt.inverse_impose_upper_bound_sum(
114
+ pt.impose_upper_bound_sum(transformed_params, upper_bound), upper_bound)
115
+
116
+ diffs = result - transformed_params
117
+ max_abs_diff = np.amax(np.abs(diffs))
118
+ self.assertAlmostEqual(0.0, max_abs_diff, delta=10 ** (-10))
119
+
120
+
121
+ if __name__ == '__main__':
122
+ unittest.main()
@@ -0,0 +1,12 @@
1
+ "This module collects functionality for testing. "
2
+
3
+ import numpy as np
4
+ from computils import FloatOrArray
5
+
6
+
7
+ def compute_max_abs_diff(expected: np.ndarray, actual: np.ndarray) -> float:
8
+ return float(np.amax(compute_abs_diff(expected, actual)))
9
+
10
+
11
+ def compute_abs_diff(expected: FloatOrArray, actual: FloatOrArray) -> FloatOrArray:
12
+ return np.abs(actual - expected)