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.
- computils-0.0.1/LICENSE +21 -0
- computils-0.0.1/PKG-INFO +20 -0
- computils-0.0.1/README.md +7 -0
- computils-0.0.1/pyproject.toml +28 -0
- computils-0.0.1/setup.cfg +4 -0
- computils-0.0.1/src/computils/__init__.py +22 -0
- computils-0.0.1/src/computils/factory.py +12 -0
- computils-0.0.1/src/computils/fast_eval.py +68 -0
- computils-0.0.1/src/computils/finite_difference.py +240 -0
- computils-0.0.1/src/computils/gaussian_elim_spp.py +62 -0
- computils-0.0.1/src/computils/globals.py +64 -0
- computils-0.0.1/src/computils/interpolation.py +104 -0
- computils-0.0.1/src/computils/matrix_computations.py +23 -0
- computils-0.0.1/src/computils/parameter_transformations.py +54 -0
- computils-0.0.1/src/computils/performance_checking.py +31 -0
- computils-0.0.1/src/computils/sorting_algorithms.py +102 -0
- computils-0.0.1/src/computils/type_utils.py +11 -0
- computils-0.0.1/src/computils.egg-info/PKG-INFO +20 -0
- computils-0.0.1/src/computils.egg-info/SOURCES.txt +23 -0
- computils-0.0.1/src/computils.egg-info/dependency_links.txt +1 -0
- computils-0.0.1/src/computils.egg-info/top_level.txt +2 -0
- computils-0.0.1/src/tests/matrix_computations_tests.py +25 -0
- computils-0.0.1/src/tests/numerical_derivatives_tests.py +129 -0
- computils-0.0.1/src/tests/parameter_transformation_tests.py +122 -0
- computils-0.0.1/src/tests/test_utils.py +12 -0
computils-0.0.1/LICENSE
ADDED
|
@@ -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.
|
computils-0.0.1/PKG-INFO
ADDED
|
@@ -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,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,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 @@
|
|
|
1
|
+
|
|
@@ -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)
|