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,296 @@
|
|
|
1
|
+
"""Construct third-derivative tensors ("hyper-Hessians") for scalar- or tensor-valued functions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Sequence
|
|
6
|
+
from functools import partial
|
|
7
|
+
from itertools import permutations
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
from numpy.typing import ArrayLike, NDArray
|
|
12
|
+
|
|
13
|
+
from derivkit.calculus.calculus_core import (
|
|
14
|
+
component_scalar_eval,
|
|
15
|
+
dispatch_tensor_output,
|
|
16
|
+
)
|
|
17
|
+
from derivkit.derivative_kit import DerivativeKit
|
|
18
|
+
from derivkit.utils.concurrency import (
|
|
19
|
+
parallel_execute,
|
|
20
|
+
resolve_inner_from_outer,
|
|
21
|
+
)
|
|
22
|
+
from derivkit.utils.sandbox import get_partial_function
|
|
23
|
+
from derivkit.utils.validate import ensure_finite
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"build_hyper_hessian",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def build_hyper_hessian(
|
|
31
|
+
function: Callable[[ArrayLike], float | np.ndarray],
|
|
32
|
+
theta0: NDArray[np.float64] | Sequence[float],
|
|
33
|
+
*,
|
|
34
|
+
method: str | None = None,
|
|
35
|
+
n_workers: int = 1,
|
|
36
|
+
**dk_kwargs: Any,
|
|
37
|
+
) -> NDArray[np.float64]:
|
|
38
|
+
"""Returns the third-derivative tensor ("hyper-Hessian") of a function.
|
|
39
|
+
|
|
40
|
+
This function computes all third-order partial derivatives of a scalar- or
|
|
41
|
+
vector-valued function with respect to its parameters, evaluated at a single
|
|
42
|
+
point in parameter space. The resulting tensor generalizes the Hessian to
|
|
43
|
+
third order and is useful for higher-order Taylor expansions, non-Gaussian
|
|
44
|
+
approximations, and sensitivity analyses beyond quadratic order.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
function: Function to differentiate.
|
|
48
|
+
theta0: 1D Parameter vector where the derivatives are evaluated.
|
|
49
|
+
method: Derivative method name or alias. If ``None``,
|
|
50
|
+
the :class:`derivkit.DerivativeKit` default is used.
|
|
51
|
+
n_workers: Outer parallelism across output components (tensor outputs only).
|
|
52
|
+
**dk_kwargs: Extra keyword args forwarded to :meth:`derivkit.DerivativeKit.differentiate`.
|
|
53
|
+
You may pass ``inner_workers=<int>`` here to override inner parallelism.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Third-derivative tensor. For scalar outputs, the result has shape
|
|
57
|
+
``(p, p, p)``, where ``p`` is the number of parameters. For tensor-valued
|
|
58
|
+
outputs with shape ``out_shape``, the result has shape
|
|
59
|
+
``(*out_shape, p, p, p)``.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
ValueError: If ``theta0`` is empty.
|
|
63
|
+
FloatingPointError: If non-finite values are encountered.
|
|
64
|
+
"""
|
|
65
|
+
theta = np.asarray(theta0, dtype=np.float64).reshape(-1)
|
|
66
|
+
if theta.size == 0:
|
|
67
|
+
raise ValueError("theta0 must be a non-empty 1D array.")
|
|
68
|
+
|
|
69
|
+
y0 = np.asarray(function(theta))
|
|
70
|
+
ensure_finite(y0, msg="Non-finite values in model output at theta0.")
|
|
71
|
+
|
|
72
|
+
inner_override = dk_kwargs.pop("inner_workers", None)
|
|
73
|
+
outer_workers = int(n_workers) if n_workers is not None else 1
|
|
74
|
+
inner_workers = (
|
|
75
|
+
int(inner_override)
|
|
76
|
+
if inner_override is not None
|
|
77
|
+
else resolve_inner_from_outer(outer_workers)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if y0.ndim == 0:
|
|
81
|
+
out = _build_hyper_hessian_scalar(
|
|
82
|
+
function=function,
|
|
83
|
+
theta=theta,
|
|
84
|
+
method=method,
|
|
85
|
+
inner_workers=inner_workers,
|
|
86
|
+
outer_workers=outer_workers,
|
|
87
|
+
**dk_kwargs,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
ensure_finite(out, msg="Non-finite values encountered in hyper-Hessian.")
|
|
91
|
+
return out
|
|
92
|
+
|
|
93
|
+
# Tensor output: compute per-component scalar hyper-Hessian and reshape it back.
|
|
94
|
+
return dispatch_tensor_output(
|
|
95
|
+
function=function,
|
|
96
|
+
theta=theta,
|
|
97
|
+
method=method,
|
|
98
|
+
outer_workers=outer_workers,
|
|
99
|
+
inner_workers=inner_workers,
|
|
100
|
+
dk_kwargs=dk_kwargs,
|
|
101
|
+
build_component=_compute_component_hyper_hessian,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _build_hyper_hessian_scalar(
|
|
106
|
+
function: Callable[[ArrayLike], float | np.ndarray],
|
|
107
|
+
theta: NDArray[np.float64],
|
|
108
|
+
method: str | None,
|
|
109
|
+
inner_workers: int | None,
|
|
110
|
+
outer_workers: int,
|
|
111
|
+
**dk_kwargs: Any,
|
|
112
|
+
) -> NDArray[np.float64]:
|
|
113
|
+
"""Returns a hyper-Hessian for a scalar-valued function.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
function: Scalar-valued function to differentiate.
|
|
117
|
+
theta: 1D parameter vector where the derivatives are evaluated..
|
|
118
|
+
method: Derivative method name or alias. If ``None``,
|
|
119
|
+
the :class:`derivkit.DerivativeKit` default is used.
|
|
120
|
+
inner_workers: Number of inner workers for :class:`derivkit.DerivativeKit` calls.
|
|
121
|
+
outer_workers: Number of outer workers for parallelism over entries.
|
|
122
|
+
**dk_kwargs: Extra keyword args forwarded to :meth:`derivkit.DerivativeKit.differentiate`.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Hyper-Hessian array with shape ``(p, p, p)`` with ``p`` the number of parameters.
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
TypeError: If ``function`` does not return a scalar.
|
|
129
|
+
"""
|
|
130
|
+
probe = np.asarray(function(theta), dtype=np.float64)
|
|
131
|
+
if probe.ndim != 0:
|
|
132
|
+
raise TypeError(
|
|
133
|
+
"Scalar hyper-Hessian path expects a scalar-valued function; "
|
|
134
|
+
f"got output with shape {probe.shape}."
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
p = int(theta.size)
|
|
138
|
+
iw = int(inner_workers or 1)
|
|
139
|
+
|
|
140
|
+
# Compute only unique entries i<=j<=k, then symmetrize.
|
|
141
|
+
triplets: list[tuple[int, int, int]] = [
|
|
142
|
+
(i, j, k) for i in range(p) for j in range(i, p) for k in range(j, p)
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
def entry_worker(i: int, j: int, k: int) -> float:
|
|
146
|
+
"""Worker to compute one hyper-Hessian entry.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
i: First parameter index.
|
|
150
|
+
j: Second parameter index.
|
|
151
|
+
k: Third parameter index.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Value of the third order derivative of the function at theta0.
|
|
155
|
+
"""
|
|
156
|
+
return _third_derivative_entry(
|
|
157
|
+
function=function,
|
|
158
|
+
theta0=theta,
|
|
159
|
+
i=i,
|
|
160
|
+
j=j,
|
|
161
|
+
k=k,
|
|
162
|
+
method=method,
|
|
163
|
+
n_workers=iw,
|
|
164
|
+
dk_kwargs=dk_kwargs,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
vals = parallel_execute(
|
|
168
|
+
entry_worker,
|
|
169
|
+
arg_tuples=triplets,
|
|
170
|
+
outer_workers=outer_workers,
|
|
171
|
+
inner_workers=iw,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
hess = np.zeros((p, p, p), dtype=np.float64)
|
|
175
|
+
for (i, j, k), v in zip(triplets, vals, strict=True):
|
|
176
|
+
v = float(np.asarray(v).item())
|
|
177
|
+
for a, b, c in set(permutations((i, j, k), 3)):
|
|
178
|
+
hess[a, b, c] = v
|
|
179
|
+
|
|
180
|
+
ensure_finite(hess, msg="Non-finite values encountered in hyper-Hessian.")
|
|
181
|
+
return hess
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _third_derivative_entry(
|
|
185
|
+
*,
|
|
186
|
+
function: Callable[[ArrayLike], float | np.ndarray],
|
|
187
|
+
theta0: NDArray[np.float64],
|
|
188
|
+
i: int,
|
|
189
|
+
j: int,
|
|
190
|
+
k: int,
|
|
191
|
+
method: str | None,
|
|
192
|
+
n_workers: int,
|
|
193
|
+
dk_kwargs: dict[str, Any],
|
|
194
|
+
) -> float:
|
|
195
|
+
"""Computes the third order derivative of ``function`` at ``theta0`` with respect to parameters ``i``, ``j``, ``k``.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
function: Scalar-valued function to differentiate.
|
|
199
|
+
theta0: 1D parameter vector at which the derivative is evaluated.
|
|
200
|
+
i: First parameter index.
|
|
201
|
+
j: Second parameter index.
|
|
202
|
+
k: Third parameter index.
|
|
203
|
+
method: Derivative method name or alias. If ``None``,
|
|
204
|
+
the :class:`derivkit.DerivativeKit` default is used.
|
|
205
|
+
n_workers: Number of workers for :class:`derivkit.DerivativeKit` calls.
|
|
206
|
+
dk_kwargs: Extra keyword args forwarded to :meth:`derivkit.DerivativeKit.differentiate`.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Value of the third order derivative of the function at ``theta0``.
|
|
210
|
+
"""
|
|
211
|
+
i, j, k = int(i), int(j), int(k)
|
|
212
|
+
ii, jj, kk = sorted((i, j, k))
|
|
213
|
+
|
|
214
|
+
if ii == jj == kk:
|
|
215
|
+
f1 = get_partial_function(function, ii, theta0)
|
|
216
|
+
kit = DerivativeKit(f1, float(theta0[ii]))
|
|
217
|
+
val = kit.differentiate(order=3, method=method, n_workers=n_workers, **dk_kwargs)
|
|
218
|
+
return float(np.asarray(val).item())
|
|
219
|
+
|
|
220
|
+
def g_func(t: float) -> float:
|
|
221
|
+
"""Function for derivative with respect to parameter kk.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
t: Value of parameter kk.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
Second derivative with respect to ii and jj at theta0 with kk=t.
|
|
228
|
+
|
|
229
|
+
"""
|
|
230
|
+
th = theta0.copy()
|
|
231
|
+
th[kk] = float(t)
|
|
232
|
+
|
|
233
|
+
if ii == jj:
|
|
234
|
+
f1 = get_partial_function(function, ii, th)
|
|
235
|
+
kit2 = DerivativeKit(f1, float(th[ii]))
|
|
236
|
+
v2 = kit2.differentiate(order=2, method=method, n_workers=n_workers, **dk_kwargs)
|
|
237
|
+
return float(np.asarray(v2).item())
|
|
238
|
+
|
|
239
|
+
# mixed second derivative via nested 1D partials
|
|
240
|
+
def h_func(yj: float) -> float:
|
|
241
|
+
"""Function for derivative with respect to parameter jj.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
yj: Value of parameter jj.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
First derivative with respect to ii at theta0 with jj=yj and kk fixed.
|
|
248
|
+
"""
|
|
249
|
+
th2 = th.copy()
|
|
250
|
+
th2[jj] = float(yj)
|
|
251
|
+
f_ii = get_partial_function(function, ii, th2)
|
|
252
|
+
kit1 = DerivativeKit(f_ii, float(th2[ii]))
|
|
253
|
+
v1 = kit1.differentiate(order=1, method=method, n_workers=n_workers, **dk_kwargs)
|
|
254
|
+
return float(np.asarray(v1).item())
|
|
255
|
+
|
|
256
|
+
kitm = DerivativeKit(h_func, float(th[jj]))
|
|
257
|
+
vm = kitm.differentiate(order=1, method=method, n_workers=n_workers, **dk_kwargs)
|
|
258
|
+
return float(np.asarray(vm).item())
|
|
259
|
+
|
|
260
|
+
kit3 = DerivativeKit(g_func, float(theta0[kk]))
|
|
261
|
+
v3 = kit3.differentiate(order=1, method=method, n_workers=n_workers, **dk_kwargs)
|
|
262
|
+
return float(np.asarray(v3).item())
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _compute_component_hyper_hessian(
|
|
266
|
+
idx: int,
|
|
267
|
+
theta: NDArray[np.float64],
|
|
268
|
+
method: str | None,
|
|
269
|
+
inner_workers: int | None,
|
|
270
|
+
dk_kwargs: dict[str, Any],
|
|
271
|
+
function: Callable[[ArrayLike], float | np.ndarray],
|
|
272
|
+
) -> NDArray[np.float64]:
|
|
273
|
+
"""Computes the hyper-Hessian for one output component of a tensor-valued function.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
idx: Index of the output component to differentiate.
|
|
277
|
+
theta: Parameter vector where the derivatives are evaluated.
|
|
278
|
+
method: Derivative method name or alias. If ``None``,
|
|
279
|
+
the :class:`derivkit.DerivativeKit` default is used.
|
|
280
|
+
inner_workers: Number of inner workers for :class:`derivkit.DerivativeKit` calls.
|
|
281
|
+
dk_kwargs: Extra keyword args forwarded to :meth:`derivkit.DerivativeKit.differentiate`.
|
|
282
|
+
function: Original tensor-valued function.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
Hyper-Hessian array with shape ``(p, p, p)`` with ``p`` the number of parameters.
|
|
286
|
+
"""
|
|
287
|
+
g = partial(component_scalar_eval, function=function, idx=int(idx))
|
|
288
|
+
# Use scalar builder with outer_workers=1 (parallel computation is already over components outside)
|
|
289
|
+
return _build_hyper_hessian_scalar(
|
|
290
|
+
g,
|
|
291
|
+
theta,
|
|
292
|
+
method=method,
|
|
293
|
+
inner_workers=inner_workers,
|
|
294
|
+
outer_workers=1,
|
|
295
|
+
**dk_kwargs,
|
|
296
|
+
)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Contains functions used to construct the Jacobian matrix."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from functools import partial
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from numpy.typing import ArrayLike, NDArray
|
|
9
|
+
|
|
10
|
+
from derivkit.derivative_kit import DerivativeKit
|
|
11
|
+
from derivkit.utils.concurrency import (
|
|
12
|
+
parallel_execute,
|
|
13
|
+
resolve_inner_from_outer,
|
|
14
|
+
)
|
|
15
|
+
from derivkit.utils.sandbox import get_partial_function
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def build_jacobian(
|
|
19
|
+
function: Callable[[ArrayLike], ArrayLike | float],
|
|
20
|
+
theta0: ArrayLike,
|
|
21
|
+
method: str | None = None,
|
|
22
|
+
n_workers: int | None = 1,
|
|
23
|
+
**dk_kwargs: Any,
|
|
24
|
+
) -> NDArray[np.floating]:
|
|
25
|
+
"""Computes the Jacobian of a vector-valued function.
|
|
26
|
+
|
|
27
|
+
Each column in the Jacobian is the derivative with respect to one parameter.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
function: The vector-valued function to be differentiated.
|
|
31
|
+
It should accept a list or array of parameter values as input and
|
|
32
|
+
return an array of observable values.
|
|
33
|
+
theta0: The parameter vector at which the jacobian is evaluated.
|
|
34
|
+
method: Method name or alias (e.g., ``"adaptive"``, ``"finite"``).
|
|
35
|
+
If ``None``, the :class:`derivkit.derivative_kit.DerivativeKit`
|
|
36
|
+
default (``"adaptive"``) is used.
|
|
37
|
+
n_workers: Number of workers used to parallelize across
|
|
38
|
+
parameters. If None or 1, no parallelization is used.
|
|
39
|
+
If greater than 1, this many threads will be used to compute
|
|
40
|
+
derivatives with respect to different parameters in parallel.
|
|
41
|
+
**dk_kwargs: Additional keyword arguments passed to
|
|
42
|
+
:meth:`derivkit.derivative_kit.DerivativeKit.differentiate`.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
A 2D array representing the jacobian. Each column corresponds to
|
|
46
|
+
the derivative with respect to one parameter.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
FloatingPointError: If non-finite values are encountered.
|
|
50
|
+
ValueError: If ``theta0`` is an empty array.
|
|
51
|
+
TypeError: If ``function`` does not return a vector value.
|
|
52
|
+
"""
|
|
53
|
+
# Validate inputs and evaluate baseline output
|
|
54
|
+
theta = np.asarray(theta0, dtype=float).ravel()
|
|
55
|
+
if theta.size == 0:
|
|
56
|
+
raise ValueError("theta0 must be a non-empty 1D array.")
|
|
57
|
+
|
|
58
|
+
y0 = np.asarray(function(theta), dtype=float)
|
|
59
|
+
if y0.ndim != 1:
|
|
60
|
+
raise TypeError(
|
|
61
|
+
f"build_jacobian expects f: R^n -> R^m with 1-D vector output; got shape {y0.shape}"
|
|
62
|
+
)
|
|
63
|
+
if not np.isfinite(y0).all():
|
|
64
|
+
raise FloatingPointError("Non-finite values in model output at theta0.")
|
|
65
|
+
|
|
66
|
+
m = int(y0.size)
|
|
67
|
+
n = int(theta.size)
|
|
68
|
+
|
|
69
|
+
# Resolve parallelism policy
|
|
70
|
+
try:
|
|
71
|
+
outer_workers = max(1, int(n_workers or 1))
|
|
72
|
+
except (TypeError, ValueError):
|
|
73
|
+
outer_workers = 1
|
|
74
|
+
inner_workers = resolve_inner_from_outer(outer_workers)
|
|
75
|
+
|
|
76
|
+
# Prepare worker
|
|
77
|
+
worker = partial(
|
|
78
|
+
_column_derivative,
|
|
79
|
+
function=function,
|
|
80
|
+
theta0=theta,
|
|
81
|
+
method=method,
|
|
82
|
+
inner_workers=inner_workers,
|
|
83
|
+
expected_m=m,
|
|
84
|
+
**dk_kwargs,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Parallelize across parameters
|
|
88
|
+
cols = parallel_execute(
|
|
89
|
+
worker,
|
|
90
|
+
arg_tuples=[(j,) for j in range(n)],
|
|
91
|
+
outer_workers=outer_workers,
|
|
92
|
+
inner_workers=inner_workers, # passed for context; we also pass explicitly to worker
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Stack columns → (m, n)
|
|
96
|
+
jac = np.column_stack([np.asarray(c, dtype=float).reshape(m) for c in cols])
|
|
97
|
+
return jac
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _column_derivative(
|
|
101
|
+
j: int,
|
|
102
|
+
function: Callable[[ArrayLike], ArrayLike | float],
|
|
103
|
+
theta0: ArrayLike,
|
|
104
|
+
method: str | None,
|
|
105
|
+
inner_workers: int | None,
|
|
106
|
+
expected_m: int,
|
|
107
|
+
**dk_kwargs: Any,
|
|
108
|
+
) -> NDArray[np.floating]:
|
|
109
|
+
"""Derivative of function with respect to parameter j.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
j: Index of the parameter to differentiate with respect to.
|
|
113
|
+
function: The vector-valued function to be differentiated.
|
|
114
|
+
theta0: The parameter vector at which the jacobian is evaluated.
|
|
115
|
+
method: Method name or alias (e.g., ``"adaptive"``, ``"finite"``).
|
|
116
|
+
If ``None``, the :class:`derivkit.derivative_kit.DerivativeKit`
|
|
117
|
+
default (``"adaptive"``) is used.
|
|
118
|
+
inner_workers: Number of workers used by
|
|
119
|
+
:meth:`derivkit.derivative_kit.DerivativeKit.differentiate`.
|
|
120
|
+
**dk_kwargs: Additional keyword arguments passed to
|
|
121
|
+
:meth:`derivkit.derivative_kit.DerivativeKit.differentiate`.
|
|
122
|
+
expected_m: Expected length of the derivative vector.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
A 1D array representing the derivative with respect to parameter j.
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
TypeError: If the derivative does not have the expected length.
|
|
129
|
+
FloatingPointError: If non-finite values are encountered.
|
|
130
|
+
"""
|
|
131
|
+
theta_x = np.asarray(theta0, dtype=float).ravel().copy()
|
|
132
|
+
|
|
133
|
+
# Single-variable view: f_j(y) where theta_x[j] = y, others fixed
|
|
134
|
+
f_j = get_partial_function(function, j, theta_x)
|
|
135
|
+
|
|
136
|
+
# Normalize method aliases for the new DK API
|
|
137
|
+
# (let DK handle validation; we only map common shorthands)
|
|
138
|
+
method_norm = None
|
|
139
|
+
if method is not None:
|
|
140
|
+
m = method.lower()
|
|
141
|
+
alias = {"auto": "adaptive", "fd": "finite"}
|
|
142
|
+
method_norm = alias.get(m, m)
|
|
143
|
+
|
|
144
|
+
# Differentiate via new unified API (passes method through)
|
|
145
|
+
kit = DerivativeKit(f_j, theta_x[j])
|
|
146
|
+
g = kit.differentiate(method=method_norm, order=1, n_workers=inner_workers, **dk_kwargs)
|
|
147
|
+
|
|
148
|
+
g = np.atleast_1d(np.asarray(g, dtype=float)).reshape(-1)
|
|
149
|
+
if g.size != expected_m:
|
|
150
|
+
raise TypeError(
|
|
151
|
+
f"Expected derivative of length {expected_m} but got {g.size} for parameter index {j}."
|
|
152
|
+
)
|
|
153
|
+
if not np.isfinite(g).all():
|
|
154
|
+
raise FloatingPointError(f"Non-finite derivative for parameter index {j}.")
|
|
155
|
+
|
|
156
|
+
return g
|
derivkit/calculus_kit.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Provides the CalculusKit class.
|
|
2
|
+
|
|
3
|
+
A wrapper around the calculus helpers that exposes the gradient, Jacobian, and Hessian functions.
|
|
4
|
+
|
|
5
|
+
Typical usage examples:
|
|
6
|
+
|
|
7
|
+
>>> import numpy as np
|
|
8
|
+
>>> from derivkit.calculus_kit import CalculusKit
|
|
9
|
+
>>>
|
|
10
|
+
>>> def sin_function(x):
|
|
11
|
+
... # scalar-valued function: f(x) = sin(x[0])
|
|
12
|
+
... return np.sin(x[0])
|
|
13
|
+
>>>
|
|
14
|
+
>>> def identity_function(x):
|
|
15
|
+
... # vector-valued function: f(x) = x
|
|
16
|
+
... return np.asarray(x, dtype=float)
|
|
17
|
+
>>>
|
|
18
|
+
>>> calc = CalculusKit(sin_function, x0=np.array([0.5]))
|
|
19
|
+
>>> grad = calc.gradient()
|
|
20
|
+
>>> hess = calc.hessian()
|
|
21
|
+
>>>
|
|
22
|
+
>>> jac = CalculusKit(identity_function, x0=np.array([1.0, 2.0])).jacobian()
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from collections.abc import Callable
|
|
26
|
+
from typing import Sequence
|
|
27
|
+
|
|
28
|
+
import numpy as np
|
|
29
|
+
from numpy.typing import NDArray
|
|
30
|
+
|
|
31
|
+
from derivkit.calculus import (
|
|
32
|
+
build_gradient,
|
|
33
|
+
build_hessian,
|
|
34
|
+
build_hessian_diag,
|
|
35
|
+
build_hyper_hessian,
|
|
36
|
+
build_jacobian,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class CalculusKit:
|
|
41
|
+
"""Provides access to gradient, Jacobian, and Hessian tensors."""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
function: Callable[[Sequence[float] | np.ndarray], float | NDArray[np.floating]],
|
|
46
|
+
x0: Sequence[float] | np.ndarray,
|
|
47
|
+
):
|
|
48
|
+
"""Initialises class with function and expansion point.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
function: The function to be differentiated. Accepts a 1D
|
|
52
|
+
array-like. Must return either a scalar (for gradient/Hessian)
|
|
53
|
+
or a 1D array (for Jacobian).
|
|
54
|
+
x0: Point at which to evaluate derivatives (shape ``(p,)``) for
|
|
55
|
+
``p`` input parameters.
|
|
56
|
+
"""
|
|
57
|
+
self.function = function
|
|
58
|
+
self.x0 = np.asarray(x0, dtype=float)
|
|
59
|
+
|
|
60
|
+
def gradient(self, *args, **kwargs) -> NDArray[np.floating]:
|
|
61
|
+
"""Returns the gradient of a scalar-valued function.
|
|
62
|
+
|
|
63
|
+
This is a wrapper around :func:`derivkit.calculus.build_gradient`,
|
|
64
|
+
with the ``function`` and ``theta0`` arguments fixed to the values
|
|
65
|
+
provided at initialization of :class:`CalculusKit`. Any additional
|
|
66
|
+
positional or keyword arguments are forwarded to
|
|
67
|
+
:func:`derivkit.calculus.build_gradient`.
|
|
68
|
+
|
|
69
|
+
Refer to the documentation of
|
|
70
|
+
:func:`derivkit.calculus.build_gradient` for available options.
|
|
71
|
+
"""
|
|
72
|
+
return build_gradient(self.function, self.x0, *args, **kwargs)
|
|
73
|
+
|
|
74
|
+
def jacobian(self, *args, **kwargs) -> NDArray[np.floating]:
|
|
75
|
+
"""Returns the Jacobian of a vector-valued function.
|
|
76
|
+
|
|
77
|
+
This is a wrapper around :func:`derivkit.calculus.build_jacobian`,
|
|
78
|
+
with the ``function`` and ``theta0`` arguments fixed to the values
|
|
79
|
+
provided at initialization of :class:`CalculusKit`. Any additional
|
|
80
|
+
positional or keyword arguments are forwarded to
|
|
81
|
+
:func:`derivkit.calculus.build_jacobian`.
|
|
82
|
+
|
|
83
|
+
Refer to the documentation of
|
|
84
|
+
:func:`derivkit.calculus.build_jacobian` for available options.
|
|
85
|
+
"""
|
|
86
|
+
return build_jacobian(self.function, self.x0, *args, **kwargs)
|
|
87
|
+
|
|
88
|
+
def hessian(self, *args, **kwargs) -> NDArray[np.floating]:
|
|
89
|
+
"""Returns the Hessian of a scalar-valued function.
|
|
90
|
+
|
|
91
|
+
This is a wrapper around :func:`derivkit.calculus.build_hessian`,
|
|
92
|
+
with the ``function`` and ``theta0`` arguments fixed to the values
|
|
93
|
+
provided at initialization of :class:`CalculusKit`. Any additional
|
|
94
|
+
positional or keyword arguments are forwarded to
|
|
95
|
+
:func:`derivkit.calculus.build_hessian`.
|
|
96
|
+
|
|
97
|
+
Refer to the documentation of
|
|
98
|
+
:func:`derivkit.calculus.build_hessian` for available options.
|
|
99
|
+
"""
|
|
100
|
+
return build_hessian(self.function, self.x0, *args, **kwargs)
|
|
101
|
+
|
|
102
|
+
def hessian_diag(self, *args, **kwargs) -> NDArray[np.floating]:
|
|
103
|
+
"""Returns the diagonal of the Hessian of a scalar-valued function.
|
|
104
|
+
|
|
105
|
+
This is a wrapper around :func:`derivkit.calculus.build_hessian_diag`,
|
|
106
|
+
with the ``function`` and ``theta0`` arguments fixed to the values
|
|
107
|
+
provided at initialization of :class:`CalculusKit`. Any additional
|
|
108
|
+
positional or keyword arguments are forwarded to
|
|
109
|
+
:func:`derivkit.calculus.build_hessian_diag`.
|
|
110
|
+
|
|
111
|
+
Refer to the documentation of
|
|
112
|
+
:func:`derivkit.calculus.build_hessian_diag` for available options.
|
|
113
|
+
"""
|
|
114
|
+
return build_hessian_diag(self.function, self.x0, *args, **kwargs)
|
|
115
|
+
|
|
116
|
+
def hyper_hessian(self, *args, **kwargs) -> NDArray[np.floating]:
|
|
117
|
+
"""Returns the third-derivative tensor of a function.
|
|
118
|
+
|
|
119
|
+
This is a wrapper around :func:`derivkit.calculus.build_hyper_hessian`,
|
|
120
|
+
with the ``function`` and ``theta0`` arguments fixed to the values
|
|
121
|
+
provided at initialization of :class:`CalculusKit`. Any additional
|
|
122
|
+
positional or keyword arguments are forwarded to
|
|
123
|
+
:func:`derivkit.calculus.build_hyper_hessian`.
|
|
124
|
+
|
|
125
|
+
Refer to the documentation of
|
|
126
|
+
:func:`derivkit.calculus.build_hyper_hessian` for available options.
|
|
127
|
+
"""
|
|
128
|
+
return build_hyper_hessian(self.function, self.x0, *args, **kwargs)
|