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,262 @@
|
|
|
1
|
+
"""Numerical utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Callable, Sequence
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from numpy.typing import ArrayLike, NDArray
|
|
9
|
+
|
|
10
|
+
from derivkit.utils.logger import derivkit_logger
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"central_difference_error_estimate",
|
|
14
|
+
"relative_error",
|
|
15
|
+
"evaluate_logprior",
|
|
16
|
+
"is_in_bounds",
|
|
17
|
+
"apply_hard_bounds",
|
|
18
|
+
"sum_terms",
|
|
19
|
+
"get_index_value",
|
|
20
|
+
"logsumexp_1d",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def central_difference_error_estimate(step_size: float, order: int = 1) -> float:
|
|
25
|
+
"""Computes a general heuristic size of the first omitted term in central-difference stencils.
|
|
26
|
+
|
|
27
|
+
Uses the general pattern h^2 / ((order + 1) * (order + 2)) as a
|
|
28
|
+
rule-of-thumb O(h^2) truncation-error scale.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
step_size: Grid spacing.
|
|
32
|
+
order: Derivative order (positive integer).
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Estimated truncation error scale.
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
ValueError: If order is not a positive integer.
|
|
39
|
+
"""
|
|
40
|
+
if order < 1:
|
|
41
|
+
raise ValueError("order must be a positive integer.")
|
|
42
|
+
|
|
43
|
+
# if order higher than 4 we do not support it, but we can still compute the estimate
|
|
44
|
+
if order > 4:
|
|
45
|
+
derivkit_logger.warning(
|
|
46
|
+
"central_difference_error_estimate called with order > 4,"
|
|
47
|
+
" which is not supported by finite_difference module.",
|
|
48
|
+
)
|
|
49
|
+
return step_size**2 / ((order + 1) * (order + 2))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def relative_error(a: np.ndarray, b: np.ndarray) -> float:
|
|
53
|
+
"""Computes the relative error metric between a and b.
|
|
54
|
+
|
|
55
|
+
This metric is defined as the maximum over all components of a and b of
|
|
56
|
+
the absolute difference divided by the maximum of 1.0 and the absolute values of
|
|
57
|
+
a and b.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
a: First array-like input.
|
|
61
|
+
b: Second array-like input.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
The relative error metric as a float.
|
|
65
|
+
"""
|
|
66
|
+
a = np.asarray(a, dtype=np.float64)
|
|
67
|
+
b = np.asarray(b, dtype=np.float64)
|
|
68
|
+
denom = np.maximum(1.0, np.maximum(np.abs(a), np.abs(b)))
|
|
69
|
+
return float(np.max(np.abs(a - b) / denom))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def evaluate_logprior(
|
|
73
|
+
theta: ArrayLike,
|
|
74
|
+
logprior: Callable[[NDArray[np.floating]], np.floating] | None,
|
|
75
|
+
) -> np.floating:
|
|
76
|
+
"""Evaluates a user-supplied log-prior callable at ``theta``.
|
|
77
|
+
|
|
78
|
+
If ``logprior`` is ``None``, a flat prior is assumed and this returns ``0.0``.
|
|
79
|
+
If a callable is provided, its output is interpreted as a log-density defined
|
|
80
|
+
up to an additive constant. Any non-finite output (e.g., ``-np.inf`` or
|
|
81
|
+
``np.nan``) is treated as zero probability and mapped to ``-np.inf``.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
theta: Parameter vector at which to evaluate the prior.
|
|
85
|
+
logprior: Callable returning the log-prior density, or ``None`` to indicate
|
|
86
|
+
a flat prior.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Log-prior value at ``theta`` (finite or ``-np.inf``).
|
|
90
|
+
"""
|
|
91
|
+
if logprior is None:
|
|
92
|
+
return np.float64(0.0)
|
|
93
|
+
v = np.float64(logprior(np.asarray(theta, dtype=np.float64)))
|
|
94
|
+
return v if np.isfinite(v) else np.float64(-np.inf)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def is_in_bounds(
|
|
98
|
+
theta: NDArray[np.floating],
|
|
99
|
+
bounds: Sequence[tuple[np.floating | None, np.floating | None]] | None,
|
|
100
|
+
) -> bool:
|
|
101
|
+
"""Checks whether a parameter vector lies within specified bounds.
|
|
102
|
+
|
|
103
|
+
If ``bounds`` is ``None``, this returns True by convention so callers can write
|
|
104
|
+
``if is_in_bounds(theta, bounds): ...`` without special-casing the absence of bounds.
|
|
105
|
+
|
|
106
|
+
Bounds are interpreted component-wise. For each parameter, either side may
|
|
107
|
+
be unbounded by setting that limit to ``None``.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
theta: Parameter vector to test.
|
|
111
|
+
bounds: Optional sequence of ``(lower, upper)`` pairs, one per parameter.
|
|
112
|
+
Use ``None`` for an unconstrained lower or upper limit.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
A boolean indicating whether all parameters satisfy their bounds
|
|
116
|
+
(or ``True`` if bounds is ``None``).
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
ValueError: If ``bounds`` is provided and its length does not match ``theta``.
|
|
120
|
+
"""
|
|
121
|
+
if bounds is None:
|
|
122
|
+
return True
|
|
123
|
+
if len(bounds) != theta.size:
|
|
124
|
+
raise ValueError(f"bounds length {len(bounds)} != theta length {theta.size}")
|
|
125
|
+
|
|
126
|
+
result = True
|
|
127
|
+
for t, (lo, hi) in zip(theta, bounds):
|
|
128
|
+
if (lo is not None and t < lo) or (hi is not None and t > hi):
|
|
129
|
+
result = False
|
|
130
|
+
break
|
|
131
|
+
return result
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def apply_hard_bounds(
|
|
135
|
+
term: Callable[[NDArray[np.floating]], np.floating],
|
|
136
|
+
*,
|
|
137
|
+
bounds: Sequence[tuple[np.floating | None, np.floating | None]] | None = None,
|
|
138
|
+
) -> Callable[[NDArray[np.floating]], np.floating]:
|
|
139
|
+
"""Returns a bounded version of a log-density contribution.
|
|
140
|
+
|
|
141
|
+
A ``term`` is a callable that returns a scalar log-density contribution
|
|
142
|
+
and support refers to the region where the density is non-zero.
|
|
143
|
+
|
|
144
|
+
This helper enforces a top-hat support region defined by ``bounds``. If
|
|
145
|
+
``theta`` lies outside the allowed region, the result is ``-np.inf`` to denote
|
|
146
|
+
zero probability. If ``theta`` lies inside the region, the provided term is
|
|
147
|
+
evaluated and any non-finite output is treated as ``-np.inf``.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
term: Callable returning a log-density contribution.
|
|
151
|
+
bounds: Optional sequence of ``(lower, upper)`` pairs defining the
|
|
152
|
+
allowed support region.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
A callable with the same signature as ``term`` that enforces the given
|
|
156
|
+
bounds, or ``term`` itself if no bounds are provided.
|
|
157
|
+
"""
|
|
158
|
+
if bounds is None:
|
|
159
|
+
return term
|
|
160
|
+
|
|
161
|
+
def bounded(theta: NDArray[np.floating]) -> np.floating:
|
|
162
|
+
"""Evaluates the bounded log-density term."""
|
|
163
|
+
th = np.asarray(theta, dtype=np.float64)
|
|
164
|
+
v = np.float64(-np.inf)
|
|
165
|
+
if is_in_bounds(th, bounds):
|
|
166
|
+
v = np.float64(term(th))
|
|
167
|
+
return v if np.isfinite(v) else np.float64(-np.inf)
|
|
168
|
+
|
|
169
|
+
return bounded
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def sum_terms(
|
|
173
|
+
*terms: Callable[[NDArray[np.floating]], np.floating]
|
|
174
|
+
) -> Callable[[NDArray[np.floating]], np.floating]:
|
|
175
|
+
"""Constructs a composite log term by summing multiple contributions.
|
|
176
|
+
|
|
177
|
+
The returned callable evaluates each provided term at the same parameter
|
|
178
|
+
vector and adds the results. If any term is non-finite, the composite term
|
|
179
|
+
evaluates to ``-np.inf``, corresponding to zero probability under the combined
|
|
180
|
+
density.
|
|
181
|
+
|
|
182
|
+
A ``term`` is a callable that returns a scalar log-density contribution
|
|
183
|
+
and support refers to the region where the density is non-zero.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
*terms: One or more callables, each returning a log-density contribution.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
A callable that returns the sum of the provided log terms at ``theta``.
|
|
190
|
+
|
|
191
|
+
Raises:
|
|
192
|
+
ValueError: If no terms are provided.
|
|
193
|
+
"""
|
|
194
|
+
if len(terms) == 0:
|
|
195
|
+
raise ValueError("sum_terms requires at least one term")
|
|
196
|
+
|
|
197
|
+
def summed(theta: NDArray[np.floating]) -> np.floating:
|
|
198
|
+
"""Evaluates the summed log-density terms."""
|
|
199
|
+
th = np.asarray(theta, dtype=np.float64)
|
|
200
|
+
total = np.float64(0.0)
|
|
201
|
+
for f in terms:
|
|
202
|
+
v = np.float64(f(th))
|
|
203
|
+
if not np.isfinite(v):
|
|
204
|
+
return np.float64(-np.inf)
|
|
205
|
+
total = np.float64(total + v)
|
|
206
|
+
return total
|
|
207
|
+
|
|
208
|
+
return summed
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def get_index_value(theta: ArrayLike, index: int, *, name: str = "theta") -> float:
|
|
212
|
+
"""Extracts a single parameter value from a 1D parameter vector.
|
|
213
|
+
|
|
214
|
+
This helper enforces that ``theta`` is one-dimensional and raises a clear,
|
|
215
|
+
user-facing error if the requested index is out of bounds. It is intended
|
|
216
|
+
for simple prior or likelihoods components that act on a single parameter.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
theta: 1D parameter vector.
|
|
220
|
+
index: Index to extract.
|
|
221
|
+
name: Name used in error messages.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Value at the given index as float.
|
|
225
|
+
|
|
226
|
+
Raises:
|
|
227
|
+
ValueError: If ``theta`` is not 1D.
|
|
228
|
+
IndexError: If ``index`` is out of bounds.
|
|
229
|
+
"""
|
|
230
|
+
th = np.asarray(theta, dtype=np.float64)
|
|
231
|
+
if th.ndim != 1:
|
|
232
|
+
raise ValueError(f"{name} must be 1D, got shape {th.shape}")
|
|
233
|
+
|
|
234
|
+
j = int(index)
|
|
235
|
+
if j < 0 or j >= th.size:
|
|
236
|
+
raise IndexError(f"{name} index {j} out of bounds for length {th.size}")
|
|
237
|
+
return float(th[j])
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def logsumexp_1d(x: ArrayLike) -> float:
|
|
241
|
+
"""Computes log(sum(exp(x))) for a 1D array using the max-shift identity.
|
|
242
|
+
|
|
243
|
+
This implements the common max-shift trick used to reduce overflow/underflow.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
x: 1D array-like values.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Value of log(sum(exp(x))) as a float.
|
|
250
|
+
|
|
251
|
+
Raises:
|
|
252
|
+
ValueError: If x is not 1D.
|
|
253
|
+
"""
|
|
254
|
+
arr = np.asarray(x, dtype=np.float64)
|
|
255
|
+
if arr.ndim != 1:
|
|
256
|
+
raise ValueError(f"logsumexp_1d expects a 1D array, got shape {arr.shape}")
|
|
257
|
+
|
|
258
|
+
m = float(np.max(arr))
|
|
259
|
+
if not np.isfinite(m):
|
|
260
|
+
return m
|
|
261
|
+
|
|
262
|
+
return float(m + np.log(np.sum(np.exp(arr - m))))
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Sandbox utilities for experimentation and testing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"get_partial_function",
|
|
11
|
+
"generate_test_function",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_partial_function(
|
|
16
|
+
full_function: Callable,
|
|
17
|
+
variable_index: int,
|
|
18
|
+
fixed_values: list | np.ndarray,
|
|
19
|
+
) -> Callable:
|
|
20
|
+
"""Returns a single-variable version of a multivariate function.
|
|
21
|
+
|
|
22
|
+
A single parameter must be specified by index. All others parameters
|
|
23
|
+
are held fixed.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
full_function: A function that takes a list of n_parameters
|
|
27
|
+
parameters and returns a vector of n_observables observables.
|
|
28
|
+
variable_index: The index of the parameter to treat as the variable.
|
|
29
|
+
fixed_values: The list of parameter values to use as fixed inputs for
|
|
30
|
+
all parameters except the one being varied.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
callable: A function of a single variable, suitable for use in
|
|
34
|
+
differentiation.
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
ValueError: If ``fixed_values`` is not 1D or if `variable_index`` is out of bounds.
|
|
38
|
+
TypeError: If ``variable_index`` is not an integer.
|
|
39
|
+
IndexError: If ``variable_index`` is out of bounds for the size of ``fixed_values``.
|
|
40
|
+
"""
|
|
41
|
+
fixed_arr = np.asarray(fixed_values, dtype=float)
|
|
42
|
+
if fixed_arr.ndim != 1:
|
|
43
|
+
raise ValueError(
|
|
44
|
+
f"fixed_values must be 1D; got shape {fixed_arr.shape}."
|
|
45
|
+
)
|
|
46
|
+
if not isinstance(variable_index, (int, np.integer)):
|
|
47
|
+
raise TypeError(
|
|
48
|
+
f"variable_index must be an integer; got {type(variable_index).__name__}."
|
|
49
|
+
)
|
|
50
|
+
if variable_index < 0 or variable_index >= fixed_arr.size:
|
|
51
|
+
raise IndexError(
|
|
52
|
+
f"variable_index {variable_index} out of bounds for size {fixed_arr.size}."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def partial_function(x):
|
|
56
|
+
params = fixed_arr.copy()
|
|
57
|
+
params[variable_index] = x
|
|
58
|
+
return np.atleast_1d(full_function(params))
|
|
59
|
+
|
|
60
|
+
return partial_function
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def generate_test_function(name: str = "sin"):
|
|
64
|
+
"""Return (f, f', f'') tuple for a named test function.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
name: One of {"sin"}; more may be added.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Tuple of callables (f, df, d2f) for testing.
|
|
71
|
+
"""
|
|
72
|
+
if name == "sin":
|
|
73
|
+
return lambda x: np.sin(x), lambda x: np.cos(x), lambda x: -np.sin(x)
|
|
74
|
+
raise ValueError(f"Unknown test function: {name!r}")
|
derivkit/utils/types.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Shared typing aliases for DerivKit."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Sequence, TypeAlias
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from numpy.typing import NDArray
|
|
9
|
+
|
|
10
|
+
Float: TypeAlias = np.floating
|
|
11
|
+
Array: TypeAlias = NDArray[np.floating]
|
|
12
|
+
FloatArray: TypeAlias = NDArray[np.float64]
|
|
13
|
+
|
|
14
|
+
ArrayLike1D: TypeAlias = Sequence[float] | NDArray[np.floating]
|
|
15
|
+
ArrayLike2D: TypeAlias = Sequence[Sequence[float]] | NDArray[np.floating]
|