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.
Files changed (68) hide show
  1. derivkit/__init__.py +22 -0
  2. derivkit/calculus/__init__.py +17 -0
  3. derivkit/calculus/calculus_core.py +152 -0
  4. derivkit/calculus/gradient.py +97 -0
  5. derivkit/calculus/hessian.py +528 -0
  6. derivkit/calculus/hyper_hessian.py +296 -0
  7. derivkit/calculus/jacobian.py +156 -0
  8. derivkit/calculus_kit.py +128 -0
  9. derivkit/derivative_kit.py +315 -0
  10. derivkit/derivatives/__init__.py +6 -0
  11. derivkit/derivatives/adaptive/__init__.py +5 -0
  12. derivkit/derivatives/adaptive/adaptive_fit.py +238 -0
  13. derivkit/derivatives/adaptive/batch_eval.py +179 -0
  14. derivkit/derivatives/adaptive/diagnostics.py +325 -0
  15. derivkit/derivatives/adaptive/grid.py +333 -0
  16. derivkit/derivatives/adaptive/polyfit_utils.py +513 -0
  17. derivkit/derivatives/adaptive/spacing.py +66 -0
  18. derivkit/derivatives/adaptive/transforms.py +245 -0
  19. derivkit/derivatives/autodiff/__init__.py +1 -0
  20. derivkit/derivatives/autodiff/jax_autodiff.py +95 -0
  21. derivkit/derivatives/autodiff/jax_core.py +217 -0
  22. derivkit/derivatives/autodiff/jax_utils.py +146 -0
  23. derivkit/derivatives/finite/__init__.py +5 -0
  24. derivkit/derivatives/finite/batch_eval.py +91 -0
  25. derivkit/derivatives/finite/core.py +84 -0
  26. derivkit/derivatives/finite/extrapolators.py +511 -0
  27. derivkit/derivatives/finite/finite_difference.py +247 -0
  28. derivkit/derivatives/finite/stencil.py +206 -0
  29. derivkit/derivatives/fornberg.py +245 -0
  30. derivkit/derivatives/local_polynomial_derivative/__init__.py +1 -0
  31. derivkit/derivatives/local_polynomial_derivative/diagnostics.py +90 -0
  32. derivkit/derivatives/local_polynomial_derivative/fit.py +199 -0
  33. derivkit/derivatives/local_polynomial_derivative/local_poly_config.py +95 -0
  34. derivkit/derivatives/local_polynomial_derivative/local_polynomial_derivative.py +205 -0
  35. derivkit/derivatives/local_polynomial_derivative/sampling.py +72 -0
  36. derivkit/derivatives/tabulated_model/__init__.py +1 -0
  37. derivkit/derivatives/tabulated_model/one_d.py +247 -0
  38. derivkit/forecast_kit.py +783 -0
  39. derivkit/forecasting/__init__.py +1 -0
  40. derivkit/forecasting/dali.py +78 -0
  41. derivkit/forecasting/expansions.py +486 -0
  42. derivkit/forecasting/fisher.py +298 -0
  43. derivkit/forecasting/fisher_gaussian.py +171 -0
  44. derivkit/forecasting/fisher_xy.py +357 -0
  45. derivkit/forecasting/forecast_core.py +313 -0
  46. derivkit/forecasting/getdist_dali_samples.py +429 -0
  47. derivkit/forecasting/getdist_fisher_samples.py +235 -0
  48. derivkit/forecasting/laplace.py +259 -0
  49. derivkit/forecasting/priors_core.py +860 -0
  50. derivkit/forecasting/sampling_utils.py +388 -0
  51. derivkit/likelihood_kit.py +114 -0
  52. derivkit/likelihoods/__init__.py +1 -0
  53. derivkit/likelihoods/gaussian.py +136 -0
  54. derivkit/likelihoods/poisson.py +176 -0
  55. derivkit/utils/__init__.py +13 -0
  56. derivkit/utils/concurrency.py +213 -0
  57. derivkit/utils/extrapolation.py +254 -0
  58. derivkit/utils/linalg.py +513 -0
  59. derivkit/utils/logger.py +26 -0
  60. derivkit/utils/numerics.py +262 -0
  61. derivkit/utils/sandbox.py +74 -0
  62. derivkit/utils/types.py +15 -0
  63. derivkit/utils/validate.py +811 -0
  64. derivkit-1.0.0.dist-info/METADATA +50 -0
  65. derivkit-1.0.0.dist-info/RECORD +68 -0
  66. derivkit-1.0.0.dist-info/WHEEL +5 -0
  67. derivkit-1.0.0.dist-info/licenses/LICENSE +21 -0
  68. derivkit-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,298 @@
1
+ """Fisher forecasting utilities."""
2
+
3
+ from typing import Any, Callable
4
+
5
+ import numpy as np
6
+ from numpy.typing import ArrayLike, DTypeLike, NDArray
7
+
8
+ from derivkit.calculus_kit import CalculusKit
9
+ from derivkit.forecasting.forecast_core import get_forecast_tensors
10
+ from derivkit.utils.concurrency import normalize_workers
11
+ from derivkit.utils.linalg import solve_or_pinv
12
+ from derivkit.utils.validate import validate_covariance_matrix_shape
13
+
14
+ __all__ = [
15
+ "build_fisher_matrix",
16
+ "build_fisher_bias",
17
+ "build_delta_nu",
18
+ ]
19
+
20
+
21
+ def build_fisher_matrix(
22
+ function: Callable[[ArrayLike], float | NDArray[np.floating]],
23
+ theta0: ArrayLike,
24
+ cov: ArrayLike,
25
+ *,
26
+ method: str | None = None,
27
+ n_workers: int = 1,
28
+ **dk_kwargs: Any,
29
+ ) -> NDArray[np.floating]:
30
+ """Computes the Fisher information matrix for a given model and covariance.
31
+
32
+ Args:
33
+ function: The scalar or vector-valued model function. It should accept
34
+ a 1D array-like of parameter values and return either a scalar or
35
+ an array of observables.
36
+ theta0: 1D array-like of fiducial parameters (single expansion point).
37
+ This helper currently assumes a single expansion point; if you need
38
+ multiple expansion points with different covariances, call this
39
+ function in a loop or work directly with
40
+ :class:`derivkit.forecast_kit.ForecastKit`.
41
+ cov: Covariance matrix of the observables. Must be square with shape
42
+ ``(n_observables, n_observables)``.
43
+ method: Derivative method name or alias (e.g., ``"adaptive"``,
44
+ ``"finite"``).
45
+ If ``None``, the :class:`derivkit.derivative_kit.DerivativeKit`
46
+ default is used.
47
+ n_workers: Number of workers for per-parameter parallelisation.
48
+ Default is ``1`` (serial).
49
+ **dk_kwargs: Additional keyword arguments forwarded to
50
+ :meth:`derivkit.derivative_kit.DerivativeKit.differentiate`.
51
+
52
+ Returns:
53
+ Fisher matrix with shape ``(n_parameters, n_parameters)``.
54
+ """
55
+ out = get_forecast_tensors(
56
+ function=function,
57
+ theta0=theta0,
58
+ cov=cov,
59
+ forecast_order=1,
60
+ method=method,
61
+ n_workers=n_workers,
62
+ **dk_kwargs,
63
+ )
64
+ # Accept both the dict form: out[1] == (F,) and the
65
+ # direct array form: out == F
66
+ if isinstance(out, dict):
67
+ try:
68
+ multiplet = out[1]
69
+ except KeyError as e:
70
+ raise ValueError(
71
+ "Expected Fisher output at key 1 in forecast tensors.") from e
72
+ if not isinstance(multiplet, tuple) or len(multiplet) < 1:
73
+ raise ValueError("Expected out[1] to be a tuple like (F,).")
74
+ fisher = np.asarray(multiplet[0], dtype=float)
75
+ else:
76
+ fisher = np.asarray(out, dtype=float)
77
+
78
+ if fisher.ndim != 2 or fisher.shape[0] != fisher.shape[1]:
79
+ raise ValueError(
80
+ f"Fisher matrix must be square; got shape {fisher.shape}.")
81
+
82
+ return fisher
83
+
84
+
85
+ def build_fisher_bias(
86
+ function: Callable[[ArrayLike], float | NDArray[np.floating]],
87
+ theta0: ArrayLike,
88
+ cov: ArrayLike,
89
+ *,
90
+ fisher_matrix: NDArray[np.floating],
91
+ delta_nu: NDArray[np.floating],
92
+ n_workers: int = 1,
93
+ method: str | None = None,
94
+ rcond: float = 1e-12,
95
+ **dk_kwargs: Any,
96
+ ) -> tuple[NDArray[np.floating], NDArray[np.floating]]:
97
+ r"""Estimates parameter bias using the stored model, expansion point, and covariance.
98
+
99
+ This function takes a model, an expansion point, a covariance matrix,
100
+ a Fisher matrix, and a data-vector difference ``delta_nu`` and maps that
101
+ difference into parameter space. A common use case is the classic
102
+ “Fisher bias” setup, where one asks how a systematic-induced change in
103
+ the data would shift inferred parameters.
104
+
105
+ Internally, the function evaluates the model response at the expansion
106
+ point and uses the covariance and Fisher matrix to compute both the
107
+ parameter-space bias vector and the corresponding shifts. See
108
+ https://arxiv.org/abs/0710.5171 for details.
109
+
110
+ Args:
111
+ function: The scalar or vector-valued function to
112
+ differentiate. It should accept a list or array of parameter
113
+ values as input and return either a scalar or a
114
+ :class:`np.ndarray` of observable values.
115
+ theta0: 1D array-like of fiducial parameters (single expansion point).
116
+ This helper currently assumes a single expansion point; if you need
117
+ multiple expansion points with different covariances, call this
118
+ function in a loop or work directly with
119
+ :class:`derivkit.forecast_kit.ForecastKit`.
120
+ cov: The covariance matrix of the observables. Must be a square
121
+ matrix with shape ``(n_observables, n_observables)``, where
122
+ ``n_observables`` is the number of observables returned by the
123
+ function.
124
+ fisher_matrix: Square matrix describing information about the parameters.
125
+ Its shape must be ``(p, p)``, where ``p`` is the number of parameters.
126
+ delta_nu: Difference between a biased and an unbiased data vector,
127
+ for example :math:`\\Delta\nu = \nu_{\\mathrm{with\\,sys}} - \nu_{\\mathrm{without\\,sys}}`.
128
+ Accepts a 1D array of length n or a 2D array that will be flattened in
129
+ row-major order (“C”) to length n, where n is the number of observables.
130
+ If supplied as a 1D array, it must already follow the same row-major (“C”)
131
+ flattening convention used throughout the package.
132
+ n_workers: Number of workers used by the internal derivative routine when
133
+ forming the Jacobian.
134
+ method: Method name or alias (e.g., ``"adaptive"``, ``"finite"``).
135
+ If ``None``, the :class:`derivkit.derivative_kit.DerivativeKit`
136
+ default (``"adaptive"``) is used.
137
+ rcond: Regularization cutoff for pseudoinverse. Default is ``1e-12``.
138
+ **dk_kwargs: Additional keyword arguments passed to
139
+ :meth:`derivkit.derivative_kit.DerivativeKit.differentiate`.
140
+
141
+ Returns:
142
+ A tuple ``(bias_vec, delta_theta)`` of 1D arrays with length ``p``,
143
+ where ``bias_vec`` is the parameter-space bias vector
144
+ and ``delta_theta`` are the corresponding parameter shifts.
145
+
146
+ Raises:
147
+ ValueError: If input shapes are inconsistent with the stored model, covariance,
148
+ or the Fisher matrix dimensions.
149
+ FloatingPointError: If the difference vector contains at least one ``NaN``.
150
+ """
151
+ n_workers = normalize_workers(n_workers)
152
+
153
+ theta0 = np.atleast_1d(theta0)
154
+ cov = validate_covariance_matrix_shape(cov)
155
+
156
+ n_parameters = theta0.shape[0]
157
+ n_observables = cov.shape[0]
158
+
159
+ fisher_matrix = np.asarray(fisher_matrix, dtype=float)
160
+ if fisher_matrix.ndim != 2 or fisher_matrix.shape[0] != fisher_matrix.shape[1]:
161
+ raise ValueError(f"fisher_matrix must be square; got shape {fisher_matrix.shape}.")
162
+
163
+ # Compute the Jacobian with shape (n_obs, n_params), so that rows correspond
164
+ # to observables and columns to parameters. This convention is used
165
+ # throughout the forecasting utilities and is assumed by the Fisher/bias
166
+ # algebra below.
167
+ ckit = CalculusKit(function, theta0)
168
+ j_matrix = np.asarray(
169
+ ckit.jacobian(
170
+ method=method,
171
+ n_workers=n_workers,
172
+ **dk_kwargs,
173
+ )
174
+ )
175
+
176
+ n_obs, n_params = n_observables, n_parameters
177
+ if j_matrix.shape != (n_obs, n_params):
178
+ raise ValueError(
179
+ f"build_jacobian must return shape (n_obs, n_params)=({n_obs},{n_params}); "
180
+ f"got {j_matrix.shape}."
181
+ )
182
+
183
+ if cov.shape != (j_matrix.shape[0], j_matrix.shape[0]):
184
+ raise ValueError(
185
+ f"covariance shape {cov.shape} must be (n, n) = "
186
+ f"{(j_matrix.shape[0], j_matrix.shape[0])} from the Jacobian."
187
+ )
188
+ if fisher_matrix.shape != (j_matrix.shape[1], j_matrix.shape[1]):
189
+ raise ValueError(
190
+ f"fisher_matrix shape {fisher_matrix.shape} must be (p, p) = "
191
+ f"{(j_matrix.shape[1], j_matrix.shape[1])} from the Jacobian."
192
+ )
193
+
194
+ # Make delta_nu a 1D array of length n; 2D inputs are flattened in row-major ("C") order.
195
+ delta_nu = np.asarray(delta_nu, dtype=float)
196
+ if delta_nu.ndim == 2:
197
+ delta_nu = delta_nu.ravel(order="C")
198
+ if delta_nu.ndim != 1 or delta_nu.size != n_obs:
199
+ raise ValueError(f"delta_nu must have length n={n_obs}; got shape {delta_nu.shape}.")
200
+ if not np.isfinite(delta_nu).all():
201
+ raise FloatingPointError("Non-finite values found in delta_nu.")
202
+
203
+ # GLS weighting by the inverse covariance:
204
+ # If C is diagonal, compute invcov * delta_nu by elementwise division (fast).
205
+ # Otherwise solve with a symmetric solver; on ill-conditioning/failure,
206
+ # fall back to a pseudoinverse and emit a warning.
207
+ off = cov.copy()
208
+ np.fill_diagonal(off, 0.0)
209
+ is_diag = not np.any(off)
210
+
211
+ if is_diag:
212
+ diag = np.diag(cov)
213
+ if np.all(diag > 0):
214
+ cinv_delta = delta_nu / diag
215
+ else:
216
+ cinv_delta = solve_or_pinv(
217
+ cov, delta_nu, rcond=rcond, assume_symmetric=True, warn_context="covariance solve"
218
+ )
219
+ else:
220
+ cinv_delta = solve_or_pinv(
221
+ cov, delta_nu, rcond=rcond, assume_symmetric=True, warn_context="covariance solve"
222
+ )
223
+
224
+ bias_vec = j_matrix.T @ cinv_delta
225
+ delta_theta = solve_or_pinv(
226
+ fisher_matrix, bias_vec, rcond=rcond, assume_symmetric=True, warn_context="Fisher solve"
227
+ )
228
+
229
+ return bias_vec, delta_theta
230
+
231
+
232
+ def build_delta_nu(
233
+ cov: ArrayLike,
234
+ *,
235
+ data_biased: NDArray[np.floating],
236
+ data_unbiased: NDArray[np.floating],
237
+ dtype: DTypeLike = np.float64,
238
+ ) -> NDArray[np.floating]:
239
+ """Computes the difference between two data vectors.
240
+
241
+ This helper is used in Fisher-bias calculations and any other workflow
242
+ where two data vectors are compared: it takes a pair of vectors (for example,
243
+ a version with a systematic and one without) and returns their difference as a
244
+ 1D array whose length matches the number of observables implied by ``cov``.
245
+ It works with both 1D inputs and 2D arrays (for example, correlation-by-ell)
246
+ and flattens 2D inputs using NumPy's row-major ("C") order, which is the
247
+ standard convention throughout the DerivKit package.
248
+
249
+ Args:
250
+ cov: The covariance matrix of the observables. Should be a square
251
+ matrix with shape ``(n_observables, n_observables)``, where
252
+ ``n_observables`` is the number of observables returned by the
253
+ function.
254
+ data_biased: Data vector that includes the systematic effect. Can be
255
+ 1D or 2D. If 1D, it must follow the NumPy's row-major (“C”)
256
+ flattening convention used throughout the package.
257
+ data_unbiased: Reference data vector without the systematic. Can be
258
+ 1D or 2D. If 1D, it must follow the NumPy's row-major (“C”)
259
+ flattening convention used throughout the package.
260
+ dtype: NumPy dtype for the output array (defaults to ``np.float64``,
261
+ i.e. NumPy's default floating type).
262
+
263
+ Returns:
264
+ A 1D NumPy array of length ``n_observables`` representing the mismatch
265
+ between the two input data vectors. This is simply the element-wise
266
+ difference between the input with systematic and the input without systematic,
267
+ flattened if necessary to match the expected observable ordering.
268
+
269
+ Raises:
270
+ ValueError: If input shapes differ, inputs are not 1D/2D, or the flattened
271
+ length does not match ``n_observables``.
272
+ FloatingPointError: If non-finite values are detected in the result.
273
+ """
274
+ cov = validate_covariance_matrix_shape(cov)
275
+ n_observables = cov.shape[0]
276
+
277
+ a = np.asarray(data_biased, dtype=dtype)
278
+ b = np.asarray(data_unbiased, dtype=dtype)
279
+
280
+ if a.shape != b.shape:
281
+ raise ValueError(f"Shapes must match: got {a.shape} vs {b.shape}.")
282
+
283
+ if a.ndim == 1:
284
+ delta_nu = a - b
285
+ elif a.ndim == 2:
286
+ delta_nu = (a - b).ravel(order="C")
287
+ else:
288
+ raise ValueError(f"Only 1D or 2D inputs are supported; got ndim={a.ndim}.")
289
+
290
+ if delta_nu.size != n_observables:
291
+ raise ValueError(
292
+ f"Flattened length {delta_nu.size} != expected n_observables {n_observables}."
293
+ )
294
+
295
+ if not np.isfinite(delta_nu).all():
296
+ raise FloatingPointError("Non-finite values found in delta vector.")
297
+
298
+ return delta_nu
@@ -0,0 +1,171 @@
1
+ """Generalized Fisher matrix construction for parameter-dependent mean and covariance."""
2
+
3
+ from typing import Any, Callable
4
+
5
+ import numpy as np
6
+ from numpy.typing import NDArray
7
+
8
+ from derivkit.calculus_kit import CalculusKit
9
+ from derivkit.forecasting.forecast_core import get_forecast_tensors
10
+ from derivkit.utils.concurrency import normalize_workers
11
+ from derivkit.utils.linalg import solve_or_pinv
12
+ from derivkit.utils.validate import (
13
+ flatten_matrix_c_order,
14
+ resolve_covariance_input,
15
+ validate_covariance_matrix_shape,
16
+ )
17
+
18
+ __all__ = [
19
+ "build_gaussian_fisher_matrix"
20
+ ]
21
+
22
+
23
+ def build_gaussian_fisher_matrix(
24
+ theta0: NDArray[np.float64],
25
+ cov: NDArray[np.float64]
26
+ | Callable[[NDArray[np.float64]], NDArray[np.float64]],
27
+ function: Callable[[NDArray[np.float64]], float | NDArray[np.float64]],
28
+ *,
29
+ method: str | None = None,
30
+ n_workers: int = 1,
31
+ rcond: float = 1e-12,
32
+ symmetrize_dcov: bool = True,
33
+ **dk_kwargs: Any,
34
+ ) -> NDArray[np.float64]:
35
+ """Computes the Gaussian Fisher matrix.
36
+
37
+ This implements the standard Fisher matrix for a Gaussian likelihoods with
38
+ parameter-dependent mean and covariance (see e.g. Eq. (2) of arXiv:1404.2854).
39
+
40
+ For Gaussian-distributed data ``d`` with mean ``mu(theta)`` and covariance ``C(theta)``,
41
+ the generalized Fisher matrix evaluated at `theta0` is::
42
+
43
+ F_ij = mu_i^T C^{-1} mu_j + 0.5 * Tr[C^{-1} C_i C^{-1} C_j].
44
+
45
+ Args:
46
+ function: Callable returning the model mean ``mu(theta)`` as a scalar (only if
47
+ ``n_obs == 1``) or 1D array of observables with shape ``(n_obs,)``.
48
+ cov: Covariance matrix. Provide either a fixed covariance array or
49
+ a callable covariance function. Supported forms are:
50
+
51
+ - ``cov=C0``: fixed covariance matrix ``C(theta_0)`` with shape
52
+ ``(n_obs, n_obs)``. Here ``n_obs`` is the number of observables.
53
+ In this case the covariance-derivative Fisher term will not be computed.
54
+ - ``cov=cov_fn``: callable ``cov_fn(theta)`` returning the covariance
55
+ matrix ``C(theta)`` evaluated at the parameter vector ``theta``,
56
+ with shape ``(n_obs, n_obs)``.
57
+
58
+ The callable form is evaluated at ``theta0`` to determine ``n_obs`` and (unless
59
+ ``C0`` is provided) to define ``C0 = C(theta0)``.
60
+ theta0: Fiducial parameter vector where the Fisher matrix is evaluated.
61
+ method: Derivative method name or alias (e.g., ``"adaptive"``, ``"finite"``).
62
+ If ``None``, the :class:`derivkit.derivative_kit.DerivativeKit` default is used.
63
+ n_workers: Number of workers for per-parameter parallelisation. Default is ``1`` (serial).
64
+ rcond: Regularization cutoff for pseudoinverse fallback.
65
+ symmetrize_dcov: If ``True``, symmetrize each covariance derivative via
66
+ ``0.5 * (C_i + C_i.T)``. Default is ``True``.
67
+ **dk_kwargs: Additional keyword arguments passed to
68
+ :meth:`derivkit.derivative_kit.DerivativeKit.differentiate`.
69
+
70
+ Returns:
71
+ Fisher matrix with shape ``(p, p)`` where ``p`` is the number of parameters.
72
+
73
+ Raises:
74
+ ValueError: If ``function(theta0)`` does not match the implied observable dimension.
75
+ """
76
+ n_workers = normalize_workers(n_workers)
77
+ theta0 = np.atleast_1d(theta0).astype(np.float64)
78
+ n_parameters = int(theta0.size)
79
+
80
+ cov0, cov_fn = resolve_covariance_input(
81
+ cov, theta0=theta0, validate=validate_covariance_matrix_shape
82
+ )
83
+ n_observables = int(cov0.shape[0])
84
+
85
+ # Validate that function(theta0) matches the
86
+ # observable dimension implied by cov0.
87
+ _mu0 = np.asarray(function(theta0), dtype=np.float64)
88
+ if _mu0.ndim == 0:
89
+ # Scalar mean is only valid for a single observable.
90
+ if n_observables != 1:
91
+ raise ValueError(
92
+ "function(theta0) returned a scalar, "
93
+ f"but cov implies n_observables={n_observables}. "
94
+ "Return a 1D mean vector with length n_observables."
95
+ )
96
+
97
+ elif _mu0.ndim != 1 or _mu0.shape[0] != n_observables:
98
+ raise ValueError(
99
+ f"function(theta0) must return shape ({n_observables},); "
100
+ f"got {_mu0.shape}."
101
+ )
102
+
103
+ # Term with derivatives of covariance matrices:
104
+ # (1/2) Tr[C^{-1} C_{,i} C^{-1} C_{,j}]
105
+ fisher_cov = np.zeros((n_parameters, n_parameters), dtype=np.float64)
106
+ if cov_fn is not None:
107
+
108
+ def cov_flat_function(th: NDArray[np.float64]) -> NDArray[np.float64]:
109
+ """Flattened covariance function for derivative computation."""
110
+ return flatten_matrix_c_order(cov_fn,
111
+ th,
112
+ n_observables=n_observables)
113
+
114
+ cov_ckit = CalculusKit(cov_flat_function, theta0)
115
+
116
+ dcov_flat = np.asarray(
117
+ cov_ckit.jacobian(method=method, n_workers=n_workers, **dk_kwargs),
118
+ dtype=np.float64,
119
+ )
120
+
121
+ expected_shape = (n_observables * n_observables, n_parameters)
122
+ if dcov_flat.shape != expected_shape:
123
+ raise ValueError(
124
+ f"dcov_flat must have shape {expected_shape}; "
125
+ "got {dcov_flat.shape}."
126
+ )
127
+
128
+ dcov = dcov_flat.T.reshape(n_parameters,
129
+ n_observables,
130
+ n_observables,
131
+ order="C")
132
+ if symmetrize_dcov:
133
+ dcov = 0.5 * (dcov + np.swapaxes(dcov, -1, -2))
134
+
135
+ covinv_dcov = np.empty_like(dcov) # (p, n_obs, n_obs)
136
+ for i in range(n_parameters):
137
+ covinv_dcov[i] = solve_or_pinv(
138
+ cov0,
139
+ dcov[i],
140
+ rcond=rcond,
141
+ assume_symmetric=True,
142
+ warn_context=f"C^{-1} dC solve (i={i})",
143
+ )
144
+ fisher_cov = 0.5 * np.einsum("iab,jba->ij", covinv_dcov, covinv_dcov)
145
+
146
+ # Term with derivatives of model mean functions:
147
+ # mu_{,i}^T C^{-1} mu_{,j}
148
+ out = get_forecast_tensors(
149
+ function=function,
150
+ theta0=theta0,
151
+ cov=cov0,
152
+ forecast_order=1,
153
+ method=method,
154
+ n_workers=n_workers,
155
+ **dk_kwargs,
156
+ )
157
+
158
+ if isinstance(out, dict):
159
+ # introduced-at-order dict: {1: (F,), ...}
160
+ fisher_mean = out[1][0]
161
+ elif isinstance(out, tuple):
162
+ # single multiplet: (F,)
163
+ fisher_mean = out[0]
164
+ else:
165
+ # already an array-like Fisher matrix
166
+ fisher_mean = out
167
+
168
+ fisher_mean = np.asarray(fisher_mean, dtype=np.float64)
169
+
170
+ fisher = fisher_mean + fisher_cov
171
+ return 0.5 * (fisher + fisher.T)