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,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)
|