derivkit 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- derivkit/__init__.py +22 -0
- derivkit/calculus/__init__.py +17 -0
- derivkit/calculus/calculus_core.py +152 -0
- derivkit/calculus/gradient.py +97 -0
- derivkit/calculus/hessian.py +528 -0
- derivkit/calculus/hyper_hessian.py +296 -0
- derivkit/calculus/jacobian.py +156 -0
- derivkit/calculus_kit.py +128 -0
- derivkit/derivative_kit.py +315 -0
- derivkit/derivatives/__init__.py +6 -0
- derivkit/derivatives/adaptive/__init__.py +5 -0
- derivkit/derivatives/adaptive/adaptive_fit.py +238 -0
- derivkit/derivatives/adaptive/batch_eval.py +179 -0
- derivkit/derivatives/adaptive/diagnostics.py +325 -0
- derivkit/derivatives/adaptive/grid.py +333 -0
- derivkit/derivatives/adaptive/polyfit_utils.py +513 -0
- derivkit/derivatives/adaptive/spacing.py +66 -0
- derivkit/derivatives/adaptive/transforms.py +245 -0
- derivkit/derivatives/autodiff/__init__.py +1 -0
- derivkit/derivatives/autodiff/jax_autodiff.py +95 -0
- derivkit/derivatives/autodiff/jax_core.py +217 -0
- derivkit/derivatives/autodiff/jax_utils.py +146 -0
- derivkit/derivatives/finite/__init__.py +5 -0
- derivkit/derivatives/finite/batch_eval.py +91 -0
- derivkit/derivatives/finite/core.py +84 -0
- derivkit/derivatives/finite/extrapolators.py +511 -0
- derivkit/derivatives/finite/finite_difference.py +247 -0
- derivkit/derivatives/finite/stencil.py +206 -0
- derivkit/derivatives/fornberg.py +245 -0
- derivkit/derivatives/local_polynomial_derivative/__init__.py +1 -0
- derivkit/derivatives/local_polynomial_derivative/diagnostics.py +90 -0
- derivkit/derivatives/local_polynomial_derivative/fit.py +199 -0
- derivkit/derivatives/local_polynomial_derivative/local_poly_config.py +95 -0
- derivkit/derivatives/local_polynomial_derivative/local_polynomial_derivative.py +205 -0
- derivkit/derivatives/local_polynomial_derivative/sampling.py +72 -0
- derivkit/derivatives/tabulated_model/__init__.py +1 -0
- derivkit/derivatives/tabulated_model/one_d.py +247 -0
- derivkit/forecast_kit.py +783 -0
- derivkit/forecasting/__init__.py +1 -0
- derivkit/forecasting/dali.py +78 -0
- derivkit/forecasting/expansions.py +486 -0
- derivkit/forecasting/fisher.py +298 -0
- derivkit/forecasting/fisher_gaussian.py +171 -0
- derivkit/forecasting/fisher_xy.py +357 -0
- derivkit/forecasting/forecast_core.py +313 -0
- derivkit/forecasting/getdist_dali_samples.py +429 -0
- derivkit/forecasting/getdist_fisher_samples.py +235 -0
- derivkit/forecasting/laplace.py +259 -0
- derivkit/forecasting/priors_core.py +860 -0
- derivkit/forecasting/sampling_utils.py +388 -0
- derivkit/likelihood_kit.py +114 -0
- derivkit/likelihoods/__init__.py +1 -0
- derivkit/likelihoods/gaussian.py +136 -0
- derivkit/likelihoods/poisson.py +176 -0
- derivkit/utils/__init__.py +13 -0
- derivkit/utils/concurrency.py +213 -0
- derivkit/utils/extrapolation.py +254 -0
- derivkit/utils/linalg.py +513 -0
- derivkit/utils/logger.py +26 -0
- derivkit/utils/numerics.py +262 -0
- derivkit/utils/sandbox.py +74 -0
- derivkit/utils/types.py +15 -0
- derivkit/utils/validate.py +811 -0
- derivkit-1.0.0.dist-info/METADATA +50 -0
- derivkit-1.0.0.dist-info/RECORD +68 -0
- derivkit-1.0.0.dist-info/WHEEL +5 -0
- derivkit-1.0.0.dist-info/licenses/LICENSE +21 -0
- derivkit-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Poissonian likelihoods function module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from scipy.stats import poisson
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"build_poissonian_likelihood",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_poissonian_likelihood(
|
|
14
|
+
data: float | np.ndarray[float],
|
|
15
|
+
model_parameters: float | np.ndarray[float],
|
|
16
|
+
return_log: bool = True,
|
|
17
|
+
) -> tuple[np.ndarray[float], np.ndarray[float]]:
|
|
18
|
+
"""Constructs the Poissonian likelihoods function.
|
|
19
|
+
|
|
20
|
+
The shape of the data products depend on the shape of ``model_parameters``.
|
|
21
|
+
The assumption is that ``model_parameters`` contains the expectation value
|
|
22
|
+
of some quantity which is either uniform for the entire distribution or is
|
|
23
|
+
distributed across a grid of bins. It is uniform for the entire distribution
|
|
24
|
+
if it is a scalar.
|
|
25
|
+
|
|
26
|
+
The function will try to reshape ``data`` to align with ``model_parameters``.
|
|
27
|
+
If ``model_parameters`` is a scalar, then ``data`` will be flattened. Otherwise,
|
|
28
|
+
the grid can contain any number of axes, but currently the number of axes
|
|
29
|
+
is hardcoded to 2. Supplying a higher-dimensional array to
|
|
30
|
+
``model_parameters`` may produce unexpected results.
|
|
31
|
+
|
|
32
|
+
This hardcoded limit means that, while it is possible to supply
|
|
33
|
+
``model_parameters`` along a 1D grid, the output shape will always be a
|
|
34
|
+
2D row-major array. See Examples for more details.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
data: an array representing the given data values.
|
|
38
|
+
model_parameters: an array representing the means of the data samples.
|
|
39
|
+
return_log: when set to ``True``, returns the log-likelihoods instead of
|
|
40
|
+
the probability mass function.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
A tuple of arrays containing (in order):
|
|
44
|
+
|
|
45
|
+
- the data, reshaped to align with the model parameters.
|
|
46
|
+
- the values of the Poissonian probability mass function computed
|
|
47
|
+
from the data and model parameters.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
ValueError: If any of the model_parameters are negative or non-finite,
|
|
51
|
+
or the data points cannot be reshaped to align with
|
|
52
|
+
model_parameters.
|
|
53
|
+
|
|
54
|
+
Examples:
|
|
55
|
+
Scalar mean + scalar data:
|
|
56
|
+
|
|
57
|
+
>>> import numpy as np
|
|
58
|
+
>>> from scipy.stats import poisson
|
|
59
|
+
>>> from derivkit.likelihoods.poisson import build_poissonian_likelihood
|
|
60
|
+
>>> x, y = build_poissonian_likelihood(2, 1.4, return_log=False)
|
|
61
|
+
>>> x.shape, y.shape
|
|
62
|
+
((1,), (1,))
|
|
63
|
+
>>> x[0].item()
|
|
64
|
+
2
|
|
65
|
+
>>> np.allclose(y[0], poisson.pmf(2, 1.4))
|
|
66
|
+
True
|
|
67
|
+
|
|
68
|
+
Vector data + scalar mean (data are flattened):
|
|
69
|
+
|
|
70
|
+
>>> data = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
|
|
71
|
+
>>> model_parameters = 2.4
|
|
72
|
+
>>> x, y = build_poissonian_likelihood(
|
|
73
|
+
... data, model_parameters, return_log=False
|
|
74
|
+
... )
|
|
75
|
+
>>> x.shape, y.shape
|
|
76
|
+
((10,), (10,))
|
|
77
|
+
>>> np.array_equal(x, data)
|
|
78
|
+
True
|
|
79
|
+
>>> np.allclose(y, poisson.pmf(data, 2.4))
|
|
80
|
+
True
|
|
81
|
+
|
|
82
|
+
Shape follows ``model_parameters``:
|
|
83
|
+
|
|
84
|
+
>>> data = np.array([1, 2])
|
|
85
|
+
>>> model_parameters = np.array([3])
|
|
86
|
+
>>> x, y = build_poissonian_likelihood(
|
|
87
|
+
... data, model_parameters, return_log=False
|
|
88
|
+
... )
|
|
89
|
+
>>> x.shape, y.shape
|
|
90
|
+
((2, 1), (2, 1))
|
|
91
|
+
>>> np.array_equal(x[:, 0], data)
|
|
92
|
+
True
|
|
93
|
+
>>> np.allclose(y[:, 0], poisson.pmf(data, 3))
|
|
94
|
+
True
|
|
95
|
+
|
|
96
|
+
1D grid of bins produces a row-major 2D output:
|
|
97
|
+
|
|
98
|
+
>>> model_parameters = np.array([0.1, 0.2, 0.3, 0.4, 0.5, 0.6])
|
|
99
|
+
>>> data = np.array([1, 2, 3, 4, 5, 6])
|
|
100
|
+
>>> x, y = build_poissonian_likelihood(
|
|
101
|
+
... data, model_parameters, return_log=False
|
|
102
|
+
... )
|
|
103
|
+
>>> x.shape, y.shape
|
|
104
|
+
((1, 6), (1, 6))
|
|
105
|
+
>>> np.array_equal(x[0], data)
|
|
106
|
+
True
|
|
107
|
+
>>> np.allclose(y[0], poisson.pmf(data, model_parameters))
|
|
108
|
+
True
|
|
109
|
+
|
|
110
|
+
2D grid:
|
|
111
|
+
|
|
112
|
+
>>> data = np.array([[1, 2, 3], [4, 5, 6]])
|
|
113
|
+
>>> model_parameters = np.array([[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]])
|
|
114
|
+
>>> x, y = build_poissonian_likelihood(
|
|
115
|
+
... data, model_parameters, return_log=False
|
|
116
|
+
... )
|
|
117
|
+
>>> x.shape, y.shape
|
|
118
|
+
((1, 2, 3), (1, 2, 3))
|
|
119
|
+
>>> np.array_equal(x[0], data)
|
|
120
|
+
True
|
|
121
|
+
>>> np.allclose(y[0], poisson.pmf(data, model_parameters))
|
|
122
|
+
True
|
|
123
|
+
|
|
124
|
+
Stacked data on the same grid:
|
|
125
|
+
|
|
126
|
+
>>> val1 = np.array([[1, 2, 3], [4, 5, 6]])
|
|
127
|
+
>>> val2 = np.array([[7, 8, 9], [10, 11, 12]])
|
|
128
|
+
>>> data = np.array([val1, val2])
|
|
129
|
+
>>> model_parameters = np.array([[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]])
|
|
130
|
+
>>> x, y = build_poissonian_likelihood(
|
|
131
|
+
... data, model_parameters, return_log=False
|
|
132
|
+
... )
|
|
133
|
+
>>> x.shape, y.shape
|
|
134
|
+
((2, 2, 3), (2, 2, 3))
|
|
135
|
+
>>> np.array_equal(x[0], val1) and np.array_equal(x[1], val2)
|
|
136
|
+
True
|
|
137
|
+
>>> np.allclose(y[0], poisson.pmf(val1, model_parameters))
|
|
138
|
+
True
|
|
139
|
+
>>> np.allclose(y[1], poisson.pmf(val2, model_parameters))
|
|
140
|
+
True
|
|
141
|
+
|
|
142
|
+
Same result when supplying flattened data:
|
|
143
|
+
|
|
144
|
+
>>> data_flat = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
|
|
145
|
+
>>> x2, y2 = build_poissonian_likelihood(
|
|
146
|
+
... data_flat, model_parameters, return_log=False
|
|
147
|
+
... )
|
|
148
|
+
>>> np.array_equal(x2, x) and np.allclose(y2, y)
|
|
149
|
+
True
|
|
150
|
+
"""
|
|
151
|
+
values_to_reshape = np.asarray(data)
|
|
152
|
+
parameters = np.asarray(model_parameters)
|
|
153
|
+
|
|
154
|
+
if np.any(values_to_reshape < 0):
|
|
155
|
+
raise ValueError("values of data must be non-negative.")
|
|
156
|
+
if np.any(~np.isfinite(values_to_reshape)):
|
|
157
|
+
raise ValueError("values of data must be finite.")
|
|
158
|
+
if np.any(parameters < 0):
|
|
159
|
+
raise ValueError("values of model_parameters must be non-negative.")
|
|
160
|
+
if np.any(~np.isfinite(parameters)):
|
|
161
|
+
raise ValueError("values of model_parameters must be finite.")
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
counts = values_to_reshape.reshape(-1, *parameters.shape[-2:])
|
|
165
|
+
except ValueError:
|
|
166
|
+
raise ValueError(
|
|
167
|
+
"data cannot be reshaped to align with model_parameters: "
|
|
168
|
+
f"data.shape={values_to_reshape.shape} is incompatible with "
|
|
169
|
+
f"model_parameters.shape={parameters.shape}."
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
probabilities = poisson.logpmf(counts, parameters) \
|
|
173
|
+
if return_log \
|
|
174
|
+
else poisson.pmf(counts, parameters)
|
|
175
|
+
|
|
176
|
+
return counts, probabilities
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Concurrency management for derivative computations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextvars
|
|
6
|
+
import os
|
|
7
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from typing import Any, Callable, Iterator, Sequence, Tuple
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"set_default_inner_derivative_workers",
|
|
13
|
+
"set_inner_derivative_workers",
|
|
14
|
+
"resolve_inner_from_outer",
|
|
15
|
+
"parallel_execute",
|
|
16
|
+
"_inner_workers_var",
|
|
17
|
+
"normalize_workers",
|
|
18
|
+
"resolve_workers",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Context-var and default
|
|
23
|
+
_inner_workers_var: contextvars.ContextVar[int | None] = contextvars.ContextVar(
|
|
24
|
+
"derivkit_inner_workers", default=None
|
|
25
|
+
)
|
|
26
|
+
_DEFAULT_INNER_WORKERS: int | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def set_default_inner_derivative_workers(n: int | None) -> None:
|
|
30
|
+
"""Sets the module-wide default for inner derivative workers.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
n: Number of inner derivative workers, or None for automatic policy.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
None
|
|
37
|
+
"""
|
|
38
|
+
global _DEFAULT_INNER_WORKERS
|
|
39
|
+
_DEFAULT_INNER_WORKERS = None if n is None else int(n)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@contextmanager
|
|
43
|
+
def set_inner_derivative_workers(n: int | None) -> Iterator[int | None]:
|
|
44
|
+
"""Temporarily sets the number of inner derivative workers.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
n: Number of inner derivative workers, or ``None`` for automatic policy.
|
|
48
|
+
|
|
49
|
+
Yields:
|
|
50
|
+
int | None: The previous worker setting (restored on exit).
|
|
51
|
+
"""
|
|
52
|
+
prev = _inner_workers_var.get()
|
|
53
|
+
token = _inner_workers_var.set(None if n is None else int(n))
|
|
54
|
+
try:
|
|
55
|
+
yield prev
|
|
56
|
+
finally:
|
|
57
|
+
_inner_workers_var.reset(token)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _int_env(name: str) -> int | None:
|
|
61
|
+
"""Reads a positive integer from an environment variable, or None if unset/invalid.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
name: Environment variable name.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Positive integer value, or None.
|
|
68
|
+
"""
|
|
69
|
+
v = os.getenv(name)
|
|
70
|
+
if not v:
|
|
71
|
+
return None
|
|
72
|
+
try:
|
|
73
|
+
i = int(v)
|
|
74
|
+
return i if i > 0 else None
|
|
75
|
+
except ValueError:
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
def _detect_hw_threads() -> int:
|
|
79
|
+
"""Detects the number of hardware threads, capped by relevant environment variables.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Number of hardware threads (at least 1).
|
|
83
|
+
"""
|
|
84
|
+
hints = [
|
|
85
|
+
_int_env("OMP_NUM_THREADS"),
|
|
86
|
+
_int_env("MKL_NUM_THREADS"),
|
|
87
|
+
_int_env("OPENBLAS_NUM_THREADS"),
|
|
88
|
+
_int_env("VECLIB_MAXIMUM_THREADS"),
|
|
89
|
+
_int_env("NUMEXPR_NUM_THREADS"),
|
|
90
|
+
]
|
|
91
|
+
env_cap = min([h for h in hints if h is not None], default=None)
|
|
92
|
+
hw = os.cpu_count() or 1
|
|
93
|
+
return max(1, min(hw, env_cap) if env_cap else hw)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def resolve_inner_from_outer(w_params: int) -> int | None:
|
|
97
|
+
"""Resolves the number of inner derivative workers based on outer workers and defaults.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
w_params: Number of outer derivative workers.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Number of inner derivative workers, or None for automatic policy.
|
|
104
|
+
"""
|
|
105
|
+
w = _inner_workers_var.get()
|
|
106
|
+
if w is not None:
|
|
107
|
+
return w
|
|
108
|
+
if _DEFAULT_INNER_WORKERS is not None:
|
|
109
|
+
return _DEFAULT_INNER_WORKERS
|
|
110
|
+
cores = _detect_hw_threads()
|
|
111
|
+
if w_params > 1:
|
|
112
|
+
return min(4, max(1, cores // w_params))
|
|
113
|
+
return min(4, cores)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def parallel_execute(
|
|
117
|
+
worker: Callable[..., Any],
|
|
118
|
+
arg_tuples: Sequence[Tuple[Any, ...]],
|
|
119
|
+
*,
|
|
120
|
+
outer_workers: int = 1,
|
|
121
|
+
inner_workers: int | None = None,
|
|
122
|
+
backend: str = "threads",
|
|
123
|
+
) -> list[Any]:
|
|
124
|
+
"""Applies a function to groups of arguments in parallel.
|
|
125
|
+
|
|
126
|
+
Inner worker setting is applied to the context, so calls inside worker
|
|
127
|
+
will see the resolved inner worker count.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
worker: Function applied to each entry in ``arg_tuples`` (called as ``worker(*args)``).
|
|
131
|
+
arg_tuples: Argument tuples; each tuple is expanded into one ``worker(*args)`` call.
|
|
132
|
+
outer_workers: Parallelism level for outer execution.
|
|
133
|
+
inner_workers: Inner derivative worker setting to propagate via contextvar.
|
|
134
|
+
backend: Parallel backend. Currently supported: "threads".
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
List of worker return values.
|
|
138
|
+
"""
|
|
139
|
+
backend_l = str(backend).lower()
|
|
140
|
+
if backend_l not in {"threads"}:
|
|
141
|
+
raise NotImplementedError(
|
|
142
|
+
f"parallel_execute backend={backend!r} not supported yet."
|
|
143
|
+
f" Use backend='threads'."
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
with set_inner_derivative_workers(inner_workers):
|
|
147
|
+
if outer_workers > 1:
|
|
148
|
+
with ThreadPoolExecutor(max_workers=outer_workers) as ex:
|
|
149
|
+
futures = []
|
|
150
|
+
for args in arg_tuples:
|
|
151
|
+
# Each task gets its own copy of the current context
|
|
152
|
+
ctx = contextvars.copy_context()
|
|
153
|
+
futures.append(ex.submit(ctx.run, worker, *args))
|
|
154
|
+
return [f.result() for f in futures]
|
|
155
|
+
else:
|
|
156
|
+
return [worker(*args) for args in arg_tuples]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def normalize_workers(
|
|
160
|
+
n_workers: Any
|
|
161
|
+
) -> int:
|
|
162
|
+
"""Ensures n_workers is a positive integer, defaulting to 1.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
n_workers: Input number of workers (can be None, float, negative, etc.)
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
int: A positive integer number of workers (at least 1).
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
None: Invalid inputs are coerced to 1.
|
|
172
|
+
"""
|
|
173
|
+
try:
|
|
174
|
+
n = int(n_workers)
|
|
175
|
+
except (TypeError, ValueError):
|
|
176
|
+
n = 1
|
|
177
|
+
return 1 if n < 1 else n
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def resolve_workers(
|
|
181
|
+
n_workers: Any,
|
|
182
|
+
dk_kwargs: dict[str, Any],
|
|
183
|
+
) -> tuple[int, int | None, dict[str, Any]]:
|
|
184
|
+
"""Decides how parallel work is split between outer calculus routines and the inner derivative engine.
|
|
185
|
+
|
|
186
|
+
Outer workers parallelize across independent derivative tasks (e.g. parameters,
|
|
187
|
+
output components, Hessian entries). Inner workers control parallelism inside
|
|
188
|
+
each derivative evaluation (within DerivativeKit).
|
|
189
|
+
|
|
190
|
+
If both levels spawn workers simultaneously, nested parallelism can cause
|
|
191
|
+
oversubscription. By default, the inner worker count is derived from the
|
|
192
|
+
outer worker count to avoid that. You can override this by passing
|
|
193
|
+
``inner_workers=<int>`` via ``dk_kwargs``.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
n_workers: Number of outer workers. If ``None``, defaults to 1.
|
|
197
|
+
dk_kwargs: Keyword arguments forwarded to DerivativeKit.differentiate.
|
|
198
|
+
May include ``inner_workers`` to override the default policy.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
(outer_workers, inner_workers, dk_kwargs_cleaned), where ``dk_kwargs_cleaned``
|
|
202
|
+
has any ``inner_workers`` entry removed.
|
|
203
|
+
"""
|
|
204
|
+
dk_kwargs_cleaned = dict(dk_kwargs)
|
|
205
|
+
inner_override = dk_kwargs_cleaned.pop("inner_workers", None)
|
|
206
|
+
|
|
207
|
+
outer = normalize_workers(n_workers)
|
|
208
|
+
if inner_override is None:
|
|
209
|
+
inner = resolve_inner_from_outer(outer)
|
|
210
|
+
else:
|
|
211
|
+
inner = normalize_workers(inner_override)
|
|
212
|
+
|
|
213
|
+
return outer, inner, dk_kwargs_cleaned
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""Extrapolation methods for numerical approximations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Sequence
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from numpy.typing import NDArray
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"richardson_extrapolate",
|
|
12
|
+
"ridders_extrapolate",
|
|
13
|
+
"gauss_richardson_extrapolate",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def richardson_extrapolate(
|
|
18
|
+
base_values: Sequence[NDArray[np.float64] | float],
|
|
19
|
+
p: int,
|
|
20
|
+
r: float = 2.0,
|
|
21
|
+
) -> NDArray[np.float64] | float:
|
|
22
|
+
"""Computes Richardson extrapolation on a sequence of approximations.
|
|
23
|
+
|
|
24
|
+
Richardson extrapolation improves the accuracy of a sequence of
|
|
25
|
+
numerical approximations that converge with a known leading-order error
|
|
26
|
+
term. Given a sequence of approximations computed with decreasing step sizes,
|
|
27
|
+
this method combines them to eliminate the leading error term, yielding
|
|
28
|
+
a more accurate estimate of the true value.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
base_values:
|
|
32
|
+
Sequence of approximations at different step sizes.
|
|
33
|
+
The step sizes are assumed to decrease by a factor of ``r``
|
|
34
|
+
between successive entries.
|
|
35
|
+
p:
|
|
36
|
+
The order of the leading error term in the approximations.
|
|
37
|
+
r:
|
|
38
|
+
The step-size reduction factor between successive entries
|
|
39
|
+
(default is ``2.0``).
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
The extrapolated value with improved accuracy.
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
ValueError: If ``base_values`` has fewer than two entries.
|
|
46
|
+
"""
|
|
47
|
+
# Work on float arrays for both scalar and vector cases
|
|
48
|
+
n = len(base_values)
|
|
49
|
+
if n < 2:
|
|
50
|
+
raise ValueError("richardson_extrapolate requires at least two base values.")
|
|
51
|
+
|
|
52
|
+
vals = [np.asarray(v, dtype=float) for v in base_values]
|
|
53
|
+
|
|
54
|
+
for j in range(1, n):
|
|
55
|
+
factor = r ** (p * j)
|
|
56
|
+
for k in range(n - 1, j - 1, -1):
|
|
57
|
+
vals[k] = (factor * vals[k] - vals[k - 1]) / (factor - 1.0)
|
|
58
|
+
|
|
59
|
+
result = vals[-1]
|
|
60
|
+
return float(result) if result.ndim == 0 else result
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def ridders_extrapolate(
|
|
64
|
+
base_values: Sequence[NDArray[np.float64] | float],
|
|
65
|
+
r: float = 2.0,
|
|
66
|
+
*,
|
|
67
|
+
extrapolator = richardson_extrapolate,
|
|
68
|
+
p: int = 2,
|
|
69
|
+
) -> tuple[NDArray[np.float64] | float, float]:
|
|
70
|
+
"""Computes a Ridders-style extrapolation on a sequence of approximations.
|
|
71
|
+
|
|
72
|
+
This builds the usual Ridders diagonal assuming a central finite-difference
|
|
73
|
+
scheme (leading error is approximately O(h^2)) by repeatedly extrapolating
|
|
74
|
+
prefixes of ``base_values``. By default it uses
|
|
75
|
+
:func:`derivkit.utils.extrapolation.richardson_extrapolate`
|
|
76
|
+
with ``p=2``, but a different extrapolator can be passed if needed.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
base_values:
|
|
80
|
+
Sequence of derivative approximations at step sizes
|
|
81
|
+
h, h/r, h/r^2, ... (all same shape: scalar, vector, or tensor).
|
|
82
|
+
r:
|
|
83
|
+
Step-size reduction factor (default ``2.0``).
|
|
84
|
+
extrapolator:
|
|
85
|
+
Function implementing the extrapolation step. Must have the
|
|
86
|
+
signature ``extrapolator(base_values, p, r) -> array_like``.
|
|
87
|
+
Defaults to
|
|
88
|
+
:func:`derivkit.utils.extrapolation.richardson_extrapolate`.
|
|
89
|
+
p:
|
|
90
|
+
Leading error order passed to ``extrapolator`` (default ``2``).
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
A tuple ``(best_value, error_estimate)`` where:
|
|
94
|
+
|
|
95
|
+
* ``best_value`` is the extrapolated estimate chosen from the
|
|
96
|
+
diagonal entries.
|
|
97
|
+
* ``error_estimate`` is a heuristic scalar error scale given by the
|
|
98
|
+
minimum difference between consecutive diagonal elements.
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
ValueError:
|
|
102
|
+
If fewer than two base values are provided.
|
|
103
|
+
"""
|
|
104
|
+
n = len(base_values)
|
|
105
|
+
if n < 2:
|
|
106
|
+
raise ValueError("ridders_extrapolate requires at least two base values.")
|
|
107
|
+
|
|
108
|
+
diag: list[NDArray[np.float64]] = []
|
|
109
|
+
err_estimates: list[float] = []
|
|
110
|
+
|
|
111
|
+
for j in range(n):
|
|
112
|
+
if j == 0:
|
|
113
|
+
d_j = np.asarray(base_values[0], dtype=float)
|
|
114
|
+
else:
|
|
115
|
+
# Use the chosen extrapolator on the first (j+1) base values
|
|
116
|
+
d_j = np.asarray(
|
|
117
|
+
extrapolator(base_values[: j + 1], p=p, r=r),
|
|
118
|
+
dtype=float,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
diag.append(d_j)
|
|
122
|
+
|
|
123
|
+
if j == 0:
|
|
124
|
+
err_estimates.append(np.inf)
|
|
125
|
+
else:
|
|
126
|
+
diff = np.asarray(diag[j] - diag[j - 1], dtype=float)
|
|
127
|
+
err_estimates.append(float(np.max(np.abs(diff))))
|
|
128
|
+
|
|
129
|
+
# Pick the diagonal element with the smallest estimated error
|
|
130
|
+
best_idx = int(np.argmin(err_estimates))
|
|
131
|
+
best_val = diag[best_idx]
|
|
132
|
+
best_err = err_estimates[best_idx]
|
|
133
|
+
|
|
134
|
+
if best_val.ndim == 0:
|
|
135
|
+
return float(best_val), float(best_err)
|
|
136
|
+
return best_val, float(best_err)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _rbf_kernel_1d(x: NDArray[np.float64],
|
|
140
|
+
y: NDArray[np.float64],
|
|
141
|
+
length_scale: float) -> NDArray[np.float64]:
|
|
142
|
+
"""Compute the RBF kernel matrix between 1D inputs x and y.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
x: 1D array of shape (n,).
|
|
146
|
+
y: 1D array of shape (m,).
|
|
147
|
+
length_scale: Length scale parameter for the RBF kernel.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Kernel matrix of shape (n, m).
|
|
151
|
+
"""
|
|
152
|
+
x = np.atleast_1d(x).astype(float)
|
|
153
|
+
y = np.atleast_1d(y).astype(float)
|
|
154
|
+
diff2 = (x[:, None] - y[None, :]) ** 2
|
|
155
|
+
return np.exp(-0.5 * diff2 / (length_scale**2))
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def gauss_richardson_extrapolate(
|
|
159
|
+
base_values: Sequence[NDArray[np.float64] | float],
|
|
160
|
+
h_values: Sequence[float],
|
|
161
|
+
p: int,
|
|
162
|
+
jitter: float = 1e-10,
|
|
163
|
+
) -> tuple[NDArray[np.float64] | float, NDArray[np.float64] | float]:
|
|
164
|
+
"""Gauss–Richardson extrapolation for a sequence of approximations f(h_i).
|
|
165
|
+
|
|
166
|
+
This method uses a Gaussian-process model with a radial-basis-function (RBF)
|
|
167
|
+
kernel to perform Richardson extrapolation, providing both an improved estimate
|
|
168
|
+
of the true value at h=0 and an uncertainty estimate. For more details, see arXiv:2401.07562.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
base_values: Sequence of approximations at different step sizes h_i.
|
|
172
|
+
h_values: Corresponding step sizes (must be positive and same length as base_values).
|
|
173
|
+
p: The order of the leading error term in the approximations.
|
|
174
|
+
jitter: Small positive value added to the diagonal of the kernel matrix for numerical stability.
|
|
175
|
+
Defaults to ``1e-10``.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
A tuple (extrapolated_value, error_estimate) where:
|
|
179
|
+
|
|
180
|
+
- extrapolated_value is the Gauss–Richardson extrapolated estimate at h=0.
|
|
181
|
+
- error_estimate is a heuristic uncertainty estimate for the extrapolated value.
|
|
182
|
+
|
|
183
|
+
Raises:
|
|
184
|
+
ValueError:
|
|
185
|
+
If h_values and base_values have different lengths or if any h_value is non-positive.
|
|
186
|
+
"""
|
|
187
|
+
h = np.asarray(h_values, dtype=float).ravel()
|
|
188
|
+
if len(base_values) != h.size:
|
|
189
|
+
raise ValueError("base_values and h_values must have the same length.")
|
|
190
|
+
if np.any(h <= 0):
|
|
191
|
+
raise ValueError("All h_values must be > 0.")
|
|
192
|
+
|
|
193
|
+
y = np.stack([np.asarray(v, dtype=float) for v in base_values], axis=0)
|
|
194
|
+
n = h.size
|
|
195
|
+
|
|
196
|
+
# Error bound b(h) = h^p
|
|
197
|
+
b = h**p
|
|
198
|
+
|
|
199
|
+
# crude length scale from spacing
|
|
200
|
+
h_sorted = np.sort(h)
|
|
201
|
+
# then we compute the differences between consecutive sorted h values
|
|
202
|
+
diffs = np.diff(h_sorted)
|
|
203
|
+
# if there are any positive differences, take the median of those
|
|
204
|
+
if np.any(diffs > 0):
|
|
205
|
+
char = np.median(diffs[diffs > 0])
|
|
206
|
+
else:
|
|
207
|
+
char = max(h.max() - h.min(), 1e-12)
|
|
208
|
+
|
|
209
|
+
ell = char
|
|
210
|
+
|
|
211
|
+
# we then build the kernel matrix kb
|
|
212
|
+
ke = _rbf_kernel_1d(h, h, ell)
|
|
213
|
+
kb = (b[:, None] * b[None, :]) * ke
|
|
214
|
+
kb += jitter * np.eye(n)
|
|
215
|
+
|
|
216
|
+
# we then precompute the matrix-vector product kb^{-1} 1
|
|
217
|
+
one = np.ones(n)
|
|
218
|
+
kb_inv_1 = np.linalg.solve(kb, one)
|
|
219
|
+
|
|
220
|
+
flat = y.reshape(n, -1)
|
|
221
|
+
means = []
|
|
222
|
+
errs = []
|
|
223
|
+
|
|
224
|
+
denom = float(one @ kb_inv_1)
|
|
225
|
+
for j in range(flat.shape[1]):
|
|
226
|
+
col = flat[:, j]
|
|
227
|
+
|
|
228
|
+
# reuse kb_inv_1 or recompute:
|
|
229
|
+
kb_inv_y = np.linalg.solve(kb, col)
|
|
230
|
+
|
|
231
|
+
num = float(one @ kb_inv_y)
|
|
232
|
+
mean0 = num / denom # μ̂
|
|
233
|
+
|
|
234
|
+
# Residuals
|
|
235
|
+
resid = col - mean0 * one
|
|
236
|
+
kb_inv_resid = np.linalg.solve(kb, resid)
|
|
237
|
+
|
|
238
|
+
# Noise variance estimate
|
|
239
|
+
sigma2 = float(resid @ kb_inv_resid) / max(n - 1, 1)
|
|
240
|
+
|
|
241
|
+
# Variance at h=0
|
|
242
|
+
var0 = sigma2 / denom if denom > 0 else 0.0
|
|
243
|
+
var0 = max(var0, 0.0)
|
|
244
|
+
std0 = float(np.sqrt(var0))
|
|
245
|
+
|
|
246
|
+
means.append(mean0)
|
|
247
|
+
errs.append(std0)
|
|
248
|
+
|
|
249
|
+
means_arr = np.array(means).reshape(y.shape[1:])
|
|
250
|
+
errs_arr = np.array(errs).reshape(y.shape[1:])
|
|
251
|
+
|
|
252
|
+
if means_arr.ndim == 0:
|
|
253
|
+
return float(means_arr), float(errs_arr)
|
|
254
|
+
return means_arr, errs_arr
|