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,429 @@
|
|
|
1
|
+
"""Provides GetDist sampling helpers for DALI approximate posteriors.
|
|
2
|
+
|
|
3
|
+
This module converts DALI-expanded posteriors into GetDist-compatible
|
|
4
|
+
:class:`getdist.MCSamples` for plotting and analysis.
|
|
5
|
+
|
|
6
|
+
Two backends are provided:
|
|
7
|
+
|
|
8
|
+
- Importance sampling using a Fisher-Gaussian kernel centered on ``theta0``.
|
|
9
|
+
- ``emcee`` ensemble MCMC targeting the same DALI log-posterior.
|
|
10
|
+
|
|
11
|
+
The target log-posterior is evaluated with
|
|
12
|
+
:func:`derivkit.forecasting.expansions.build_logposterior_dali`, optionally
|
|
13
|
+
including user-defined priors and parameter support bounds.
|
|
14
|
+
|
|
15
|
+
Note: GetDist's ``loglikes`` field stores ``-log(posterior)``, not ``-log(likelihoods)``.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from functools import partial
|
|
21
|
+
from typing import Any, Callable, Sequence
|
|
22
|
+
|
|
23
|
+
import emcee
|
|
24
|
+
import numpy as np
|
|
25
|
+
from getdist import MCSamples
|
|
26
|
+
from numpy.typing import NDArray
|
|
27
|
+
|
|
28
|
+
from derivkit.forecasting.expansions import build_logposterior_dali
|
|
29
|
+
from derivkit.forecasting.priors_core import build_prior
|
|
30
|
+
from derivkit.forecasting.sampling_utils import (
|
|
31
|
+
apply_parameter_bounds,
|
|
32
|
+
init_walkers_from_fisher,
|
|
33
|
+
kernel_samples_from_fisher,
|
|
34
|
+
log_gaussian_kernel,
|
|
35
|
+
)
|
|
36
|
+
from derivkit.utils.validate import validate_dali_shape
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"dali_to_getdist_importance",
|
|
40
|
+
"dali_to_getdist_emcee",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _resolve_support_bounds(
|
|
45
|
+
*,
|
|
46
|
+
n_params: int,
|
|
47
|
+
prior_bounds: Sequence[tuple[float | None, float | None]] | None,
|
|
48
|
+
sampler_bounds: Sequence[tuple[float | None, float | None]] | None,
|
|
49
|
+
) -> Sequence[tuple[float | None, float | None]] | None:
|
|
50
|
+
"""Computes the effective parameter-support bounds used by the sampler.
|
|
51
|
+
|
|
52
|
+
This function combines two optional sources of support constraints:
|
|
53
|
+
|
|
54
|
+
- ``prior_bounds``, which truncate the prior (and therefore the posterior support).
|
|
55
|
+
- ``sampler_bounds``, restrict the sampling domain used for rejection/initialization.
|
|
56
|
+
|
|
57
|
+
When both are provided, the returned bounds are their intersection, computed
|
|
58
|
+
component-wise for each parameter. Each bound is a ``(low, high)`` pair where
|
|
59
|
+
either endpoint may be ``None`` to indicate no constraint.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
n_params: Number of parameters (expected length of any provided bounds sequence).
|
|
63
|
+
prior_bounds: Optional per-parameter prior truncation bounds.
|
|
64
|
+
sampler_bounds: Optional per-parameter sampling-domain bounds.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
The effective per-parameter bounds to use for support filtering, or ``None``
|
|
68
|
+
if neither set of bounds is provided.
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
ValueError: If a provided bounds sequence does not have length ``n_params``,
|
|
72
|
+
or if the intersection of ``prior_bounds`` and ``sampler_bounds`` is empty
|
|
73
|
+
for any parameter (i.e., the resulting lower bound exceeds the upper bound).
|
|
74
|
+
"""
|
|
75
|
+
if sampler_bounds is not None and len(sampler_bounds) != n_params:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
f"sampler_bounds must have length p={n_params}; got len(sampler_bounds)={len(sampler_bounds)}."
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if prior_bounds is not None and len(prior_bounds) != n_params:
|
|
81
|
+
raise ValueError(
|
|
82
|
+
f"prior_bounds must have length p={n_params}; got len(prior_bounds)={len(prior_bounds)}."
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if sampler_bounds is None:
|
|
86
|
+
return prior_bounds
|
|
87
|
+
if prior_bounds is None:
|
|
88
|
+
return sampler_bounds
|
|
89
|
+
|
|
90
|
+
bounds = []
|
|
91
|
+
for i, ((hlo, hhi), (plo, phi)) in enumerate(zip(sampler_bounds, prior_bounds, strict=True)):
|
|
92
|
+
lo = None if (hlo is None and plo is None) else max(x for x in (hlo, plo) if x is not None)
|
|
93
|
+
hi = None if (hhi is None and phi is None) else min(x for x in (hhi, phi) if x is not None)
|
|
94
|
+
|
|
95
|
+
if lo is not None and hi is not None and lo > hi:
|
|
96
|
+
raise ValueError(
|
|
97
|
+
f"Empty support for parameter index {i}: "
|
|
98
|
+
f"intersection gives ({lo}, {hi}). "
|
|
99
|
+
"Check prior_bounds and sampler_bounds."
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
bounds.append((lo, hi))
|
|
103
|
+
|
|
104
|
+
return bounds
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def dali_to_getdist_importance(
|
|
108
|
+
theta0: NDArray[np.floating],
|
|
109
|
+
dali: dict[int, tuple[NDArray[np.floating], ...]],
|
|
110
|
+
*,
|
|
111
|
+
forecast_order: int | None = 2,
|
|
112
|
+
names: Sequence[str],
|
|
113
|
+
labels: Sequence[str],
|
|
114
|
+
n_samples: int = 50_000,
|
|
115
|
+
kernel_scale: float = 1.5,
|
|
116
|
+
seed: int | None = None,
|
|
117
|
+
prior_terms: Sequence[tuple[str, dict[str, Any]] | dict[str, Any]] | None = None,
|
|
118
|
+
prior_bounds: Sequence[tuple[float | None, float | None]] | None = None,
|
|
119
|
+
logprior: Callable[[NDArray[np.floating]], float] | None = None,
|
|
120
|
+
sampler_bounds: Sequence[tuple[float | None, float | None]] | None = None,
|
|
121
|
+
label: str = "DALI (importance)",
|
|
122
|
+
) -> MCSamples:
|
|
123
|
+
"""Returns :class:`getdist.MCSamples` for a DALI posterior via importance sampling.
|
|
124
|
+
|
|
125
|
+
The target log-posterior is evaluated with
|
|
126
|
+
:func:`derivkit.forecasting.expansions.build_logposterior_dali`. Samples
|
|
127
|
+
are drawn from a Fisher–Gaussian kernel centered on ``theta0`` and
|
|
128
|
+
reweighted by the difference between the target log-posterior and the
|
|
129
|
+
kernel log-density.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
theta0: Fiducial parameter vector with shape ``(p,)`` for ``p`` parameters.
|
|
133
|
+
dali: Dictionary returned by :func:`derivkit.forecasting.build_dali`.
|
|
134
|
+
forecast_order: Maximum order of the DALI expansion to include
|
|
135
|
+
(e.g., 2 for doublet, 3 for triplet).
|
|
136
|
+
names: Parameter names used to label the returned samples (length ``p``).
|
|
137
|
+
labels: LaTeX-formatted parameter labels used to label the returned samples (length ``p``).
|
|
138
|
+
n_samples: Number of importance samples to draw.
|
|
139
|
+
kernel_scale: Scale factor applied to the Fisher covariance for the kernel.
|
|
140
|
+
seed: Random seed for kernel sampling.
|
|
141
|
+
prior_terms: Optional prior term specifications used to build a prior via
|
|
142
|
+
:func:`derivkit.forecasting.priors.core.build_prior`. Mutually exclusive with ``logprior``.
|
|
143
|
+
Can be combined with ``prior_bounds`` to truncate prior support.
|
|
144
|
+
prior_bounds: Optional per-parameter bounds passed to
|
|
145
|
+
:func:`derivkit.forecasting.priors.core.build_prior` to truncate the prior support.
|
|
146
|
+
If provided with no ``prior_terms``, this corresponds to a bounded-uniform (top-hat) prior.
|
|
147
|
+
A bounded-uniform prior is proper (normalizable), unlike an unbounded flat prior which is improper.
|
|
148
|
+
Mutually exclusive with ``logprior``.
|
|
149
|
+
logprior: Optional custom log-prior ``logprior(theta)``. Mutually exclusive with
|
|
150
|
+
``prior_terms``/``prior_bounds``. If none of these are provided, a flat (typically improper)
|
|
151
|
+
prior is used.
|
|
152
|
+
sampler_bounds: Optional per-parameter sampling-domain bounds used for early rejection
|
|
153
|
+
and walker initialization.
|
|
154
|
+
label: Label attached to the returned samples output (e.g., used by GetDist in plot legends/titles).
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
:class:`getdist.MCSamples` containing the importance ``weights``.
|
|
158
|
+
:attr:`getdist.MCSamples.loglikes` stores ``-log(posterior) (likelihoods x prior)``
|
|
159
|
+
up to an additive constant, consistent with GetDist's convention.
|
|
160
|
+
|
|
161
|
+
Raises:
|
|
162
|
+
ValueError: If shapes are inconsistent, mutually exclusive options are provided,
|
|
163
|
+
or the effective support bounds are invalid (e.g., an empty intersection).
|
|
164
|
+
RuntimeError: If all samples are rejected by bounds or prior support.
|
|
165
|
+
"""
|
|
166
|
+
fiducial = np.asarray(theta0, dtype=float).reshape(-1)
|
|
167
|
+
|
|
168
|
+
if not isinstance(dali, dict):
|
|
169
|
+
raise TypeError(
|
|
170
|
+
"dali must be the dict form returned by build_dali.")
|
|
171
|
+
|
|
172
|
+
validate_dali_shape(fiducial, dali)
|
|
173
|
+
|
|
174
|
+
# Fisher for the Gaussian proposal kernel comes from dali[1]
|
|
175
|
+
if 1 not in dali.keys():
|
|
176
|
+
raise ValueError("dali must contain key 1 with dali[1] == (F,).")
|
|
177
|
+
|
|
178
|
+
fisher_matrix = np.asarray(dali[1][0], dtype=float)
|
|
179
|
+
|
|
180
|
+
n_params = int(fiducial.size)
|
|
181
|
+
n_names = len(names)
|
|
182
|
+
n_labels = len(labels)
|
|
183
|
+
if n_names != n_params:
|
|
184
|
+
raise ValueError(f"names must have length p={n_params}; got len(names)={n_names}.")
|
|
185
|
+
if n_labels != n_params:
|
|
186
|
+
raise ValueError(f"labels must have length p={n_params}; got len(labels)={n_labels}.")
|
|
187
|
+
|
|
188
|
+
if logprior is not None and (prior_terms is not None or prior_bounds is not None):
|
|
189
|
+
raise ValueError(
|
|
190
|
+
"Ambiguous prior specification: pass either `logprior` or (`prior_terms` and/or `prior_bounds`), not both."
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
support_bounds = _resolve_support_bounds(
|
|
194
|
+
n_params=n_params,
|
|
195
|
+
prior_bounds=prior_bounds,
|
|
196
|
+
sampler_bounds=sampler_bounds,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Computing the prior once prevents rebuilding inside logposterior_dali
|
|
200
|
+
logprior_fn = logprior
|
|
201
|
+
if logprior_fn is None and (
|
|
202
|
+
prior_terms is not None or prior_bounds is not None):
|
|
203
|
+
logprior_fn = build_prior(terms=prior_terms, bounds=prior_bounds)
|
|
204
|
+
|
|
205
|
+
kernel_samples = kernel_samples_from_fisher(
|
|
206
|
+
fiducial,
|
|
207
|
+
fisher_matrix,
|
|
208
|
+
n_samples=int(n_samples),
|
|
209
|
+
kernel_scale=float(kernel_scale),
|
|
210
|
+
seed=seed,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
kernel_samples = apply_parameter_bounds(kernel_samples, support_bounds)
|
|
214
|
+
if kernel_samples.shape[0] == 0:
|
|
215
|
+
raise RuntimeError("All kernel samples rejected by bounds.")
|
|
216
|
+
|
|
217
|
+
target_logpost = np.array(
|
|
218
|
+
[
|
|
219
|
+
build_logposterior_dali(
|
|
220
|
+
theta,
|
|
221
|
+
fiducial,
|
|
222
|
+
dali,
|
|
223
|
+
forecast_order=forecast_order,
|
|
224
|
+
logprior=logprior_fn,
|
|
225
|
+
)
|
|
226
|
+
for theta in kernel_samples
|
|
227
|
+
],
|
|
228
|
+
dtype=float,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
keep = np.isfinite(target_logpost)
|
|
232
|
+
kernel_samples = kernel_samples[keep]
|
|
233
|
+
target_logpost = target_logpost[keep]
|
|
234
|
+
if kernel_samples.shape[0] == 0:
|
|
235
|
+
raise RuntimeError(
|
|
236
|
+
"All kernel samples were rejected: the target log-posterior "
|
|
237
|
+
"was -inf for every sample "
|
|
238
|
+
"(outside prior/support bounds or invalid model evaluation)."
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
kernel_logpdf = log_gaussian_kernel(
|
|
242
|
+
kernel_samples,
|
|
243
|
+
fiducial,
|
|
244
|
+
fisher_matrix,
|
|
245
|
+
kernel_scale=float(kernel_scale),
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
log_weights = target_logpost - kernel_logpdf
|
|
249
|
+
log_weights -= np.max(log_weights)
|
|
250
|
+
weights = np.exp(log_weights)
|
|
251
|
+
|
|
252
|
+
loglikes = -target_logpost
|
|
253
|
+
|
|
254
|
+
ranges = None
|
|
255
|
+
if support_bounds is not None:
|
|
256
|
+
ranges = {n: [lo, hi] for n, (lo, hi) in zip(names, support_bounds, strict=True)}
|
|
257
|
+
|
|
258
|
+
return MCSamples(
|
|
259
|
+
samples=kernel_samples,
|
|
260
|
+
weights=weights,
|
|
261
|
+
loglikes=loglikes,
|
|
262
|
+
names=list(names),
|
|
263
|
+
labels=list(labels),
|
|
264
|
+
ranges=ranges,
|
|
265
|
+
label=label,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def dali_to_getdist_emcee(
|
|
270
|
+
theta0: NDArray[np.floating],
|
|
271
|
+
dali: dict[int, tuple[NDArray[np.floating], ...]],
|
|
272
|
+
*,
|
|
273
|
+
forecast_order: int | None = 2,
|
|
274
|
+
names: Sequence[str],
|
|
275
|
+
labels: Sequence[str],
|
|
276
|
+
n_steps: int = 10_000,
|
|
277
|
+
burn: int = 2_000,
|
|
278
|
+
thin: int = 2,
|
|
279
|
+
n_walkers: int | None = None,
|
|
280
|
+
init_scale: float = 0.5,
|
|
281
|
+
seed: int | None = None,
|
|
282
|
+
prior_terms: Sequence[tuple[str, dict[str, Any]] | dict[str, Any]] | None = None,
|
|
283
|
+
prior_bounds: Sequence[tuple[float | None, float | None]] | None = None,
|
|
284
|
+
logprior: Callable[[NDArray[np.floating]], float] | None = None,
|
|
285
|
+
sampler_bounds: Sequence[tuple[float | None, float | None]] | None = None,
|
|
286
|
+
label: str = "DALI (emcee)",
|
|
287
|
+
) -> MCSamples:
|
|
288
|
+
"""Returns :class:`getdist.MCSamples` from ``emcee`` sampling of a DALI posterior.
|
|
289
|
+
|
|
290
|
+
The target log-posterior is evaluated with
|
|
291
|
+
:func:`derivkit.forecasting.expansions.build_logposterior_dali`.
|
|
292
|
+
Walkers are initialized from a Fisher–Gaussian cloud around ``theta0`` and
|
|
293
|
+
evolved with :class:`emcee.EnsembleSampler`. Optional priors and support
|
|
294
|
+
bounds are applied through the target log-posterior.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
theta0: Fiducial parameter vector with shape ``(p,)`` with ``p`` parameters.
|
|
298
|
+
dali: Dictionary returned by :func:`derivkit.forecasting.build_dali`.
|
|
299
|
+
forecast_order: Maximum order of the DALI expansion to include (e.g., 2 for doublet, 3 for triplet).
|
|
300
|
+
names: Parameter names used to label the returned samples (length ``p``).
|
|
301
|
+
labels: LaTeX-formatted parameter labels used to label the returned samples (length ``p``).
|
|
302
|
+
n_steps: Total number of MCMC steps.
|
|
303
|
+
burn: Number of initial MCMC steps discarded as burn-in, allowing the chain to
|
|
304
|
+
forget its initial starting positions before samples are retained.
|
|
305
|
+
thin: Thinning factor applied after burn-in; only every ``thin``-th sample is kept
|
|
306
|
+
to reduce autocorrelation in the chain.
|
|
307
|
+
n_walkers: Number of walkers. Defaults to ``max(32, 8 * p)``.
|
|
308
|
+
init_scale: Initial scatter scale for walker initialization.
|
|
309
|
+
seed: Random seed for walker initialization.
|
|
310
|
+
prior_terms: Optional prior term specifications used to build a prior via
|
|
311
|
+
:func:`derivkit.forecasting.priors.core.build_prior`. Mutually exclusive with ``logprior``.
|
|
312
|
+
Can be combined with ``prior_bounds`` to truncate prior support.
|
|
313
|
+
prior_bounds: Optional per-parameter bounds passed to
|
|
314
|
+
:func:`derivkit.forecasting.priors.core.build_prior` to truncate the prior support.
|
|
315
|
+
If provided with no ``prior_terms``, this corresponds to a bounded-uniform (top-hat) prior.
|
|
316
|
+
A bounded-uniform prior is proper (normalizable), unlike an unbounded flat prior which is improper.
|
|
317
|
+
Mutually exclusive with ``logprior``.
|
|
318
|
+
logprior: Optional custom log-prior ``logprior(theta)``. Mutually exclusive with
|
|
319
|
+
``prior_terms``/``prior_bounds``. If none of these are provided, a flat (typically improper)
|
|
320
|
+
prior is used.
|
|
321
|
+
sampler_bounds: Optional per-parameter sampling-domain bounds used for early rejection
|
|
322
|
+
and walker initialization.
|
|
323
|
+
label: Label attached to the returned samples output (e.g., used by GetDist in plot legends/titles).
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
:class:`getdist.MCSamples` containing MCMC chains.
|
|
327
|
+
:attr:`getdist.MCSamples.loglikes` stores ``-log(posterior) (likelihoods x prior)``
|
|
328
|
+
up to an additive constant, consistent with GetDist's convention.
|
|
329
|
+
|
|
330
|
+
Raises:
|
|
331
|
+
ValueError: If shapes are inconsistent, mutually exclusive options are provided,
|
|
332
|
+
or the effective support bounds are invalid (e.g., an empty intersection).
|
|
333
|
+
RuntimeError: If walker initialization fails (no valid starting points).
|
|
334
|
+
"""
|
|
335
|
+
fiducial = np.asarray(theta0, dtype=float).reshape(-1)
|
|
336
|
+
|
|
337
|
+
if not isinstance(dali, dict):
|
|
338
|
+
raise TypeError(
|
|
339
|
+
"dali must be the dict form returned by build_dali.")
|
|
340
|
+
|
|
341
|
+
validate_dali_shape(fiducial, dali)
|
|
342
|
+
|
|
343
|
+
if 1 not in dali.keys():
|
|
344
|
+
raise ValueError("dali must contain key 1 with dali[1] == (F,).")
|
|
345
|
+
|
|
346
|
+
fisher_matrix = np.asarray(dali[1][0], dtype=float)
|
|
347
|
+
|
|
348
|
+
n_params = int(fiducial.size)
|
|
349
|
+
n_names = len(names)
|
|
350
|
+
n_labels = len(labels)
|
|
351
|
+
if n_names != n_params:
|
|
352
|
+
raise ValueError(f"names must have length p={n_params}; got len(names)={n_names}.")
|
|
353
|
+
if n_labels != n_params:
|
|
354
|
+
raise ValueError(f"labels must have length p={n_params}; got len(labels)={n_labels}.")
|
|
355
|
+
|
|
356
|
+
if n_walkers is None:
|
|
357
|
+
n_walkers = max(32, 8 * n_params)
|
|
358
|
+
|
|
359
|
+
if logprior is not None and (prior_terms is not None or prior_bounds is not None):
|
|
360
|
+
raise ValueError(
|
|
361
|
+
"Ambiguous prior specification: pass either `logprior` or (`prior_terms` and/or `prior_bounds`), not both."
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
support_bounds = _resolve_support_bounds(
|
|
365
|
+
n_params=n_params,
|
|
366
|
+
prior_bounds=prior_bounds,
|
|
367
|
+
sampler_bounds=sampler_bounds,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
# Compute prior once, as this prevents rebuilding inside logposterior_dali.
|
|
371
|
+
logprior_fn = logprior
|
|
372
|
+
if logprior_fn is None and (
|
|
373
|
+
prior_terms is not None or prior_bounds is not None):
|
|
374
|
+
logprior_fn = build_prior(terms=prior_terms, bounds=prior_bounds)
|
|
375
|
+
|
|
376
|
+
walker_init = init_walkers_from_fisher(
|
|
377
|
+
fiducial,
|
|
378
|
+
fisher_matrix,
|
|
379
|
+
n_walkers=int(n_walkers),
|
|
380
|
+
init_scale=float(init_scale),
|
|
381
|
+
seed=seed,
|
|
382
|
+
sampler_bounds=support_bounds,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
walker_init = np.asarray(walker_init, dtype=float)
|
|
386
|
+
if walker_init.ndim != 2 or walker_init.shape[0] == 0 or walker_init.shape[1] != n_params:
|
|
387
|
+
raise RuntimeError(
|
|
388
|
+
"Walker initialization failed: no valid starting points. "
|
|
389
|
+
"The provided bounds/prior support likely exclude the Fisher kernel around theta0. "
|
|
390
|
+
"Try loosening bounds, increasing init_scale, or checking theta0 is inside support."
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
_base_log_prob = partial(
|
|
394
|
+
build_logposterior_dali,
|
|
395
|
+
theta0=fiducial,
|
|
396
|
+
dali=dali,
|
|
397
|
+
forecast_order=forecast_order,
|
|
398
|
+
logprior=logprior_fn,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
def log_prob(theta: NDArray[np.floating]) -> float:
|
|
402
|
+
"""Target log-posterior with hard support bounds (returns -inf outside)."""
|
|
403
|
+
if support_bounds is not None:
|
|
404
|
+
th = np.asarray(theta, dtype=float).reshape(1, -1)
|
|
405
|
+
if apply_parameter_bounds(th, support_bounds).shape[0] == 0:
|
|
406
|
+
return -np.inf
|
|
407
|
+
return float(_base_log_prob(theta))
|
|
408
|
+
|
|
409
|
+
sampler = emcee.EnsembleSampler(int(n_walkers), n_params, log_prob)
|
|
410
|
+
sampler.run_mcmc(walker_init, int(n_steps), progress=True)
|
|
411
|
+
|
|
412
|
+
chains = sampler.get_chain(discard=int(burn), thin=int(thin))
|
|
413
|
+
log_posteriors = sampler.get_log_prob(discard=int(burn), thin=int(thin))
|
|
414
|
+
|
|
415
|
+
chain_list = [chains[:, walker_idx, :] for walker_idx in range(chains.shape[1])]
|
|
416
|
+
loglikes_list = [-log_posteriors[:, walker_idx] for walker_idx in range(log_posteriors.shape[1])]
|
|
417
|
+
|
|
418
|
+
ranges = None
|
|
419
|
+
if support_bounds is not None:
|
|
420
|
+
ranges = {n: [lo, hi] for n, (lo, hi) in zip(names, support_bounds, strict=True)}
|
|
421
|
+
|
|
422
|
+
return MCSamples(
|
|
423
|
+
samples=chain_list,
|
|
424
|
+
loglikes=loglikes_list,
|
|
425
|
+
names=list(names),
|
|
426
|
+
labels=list(labels),
|
|
427
|
+
ranges=ranges,
|
|
428
|
+
label=label,
|
|
429
|
+
)
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Provides conversion helpers for Fisher–Gaussian forecasts and GetDist objects.
|
|
2
|
+
|
|
3
|
+
This module converts Fisher-matrix Gaussian approximations into
|
|
4
|
+
GetDist-compatible representations for plotting and analysis.
|
|
5
|
+
|
|
6
|
+
Two outputs are supported:
|
|
7
|
+
|
|
8
|
+
- An analytic Gaussian approximation via :class:`getdist.gaussian_mixtures.GaussianND`
|
|
9
|
+
with mean ``theta0`` and covariance from the (pseudo-)inverse Fisher matrix.
|
|
10
|
+
- Monte Carlo samples drawn from the Fisher Gaussian as :class:`getdist.MCSamples`, with
|
|
11
|
+
optional prior support hard bounds, and :attr:`getdist.MCSamples.loglikes`.
|
|
12
|
+
|
|
13
|
+
These helpers are intended for quick visualization (e.g. triangle plots) and
|
|
14
|
+
simple prior truncation without running an MCMC sampler.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Any, Callable, Sequence
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
from getdist import MCSamples
|
|
23
|
+
from getdist.gaussian_mixtures import GaussianND
|
|
24
|
+
from numpy.typing import NDArray
|
|
25
|
+
|
|
26
|
+
from derivkit.forecasting.priors_core import build_prior
|
|
27
|
+
from derivkit.forecasting.sampling_utils import (
|
|
28
|
+
apply_parameter_bounds,
|
|
29
|
+
fisher_to_cov,
|
|
30
|
+
kernel_samples_from_fisher,
|
|
31
|
+
)
|
|
32
|
+
from derivkit.utils.validate import validate_fisher_shape
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"fisher_to_getdist_gaussiannd",
|
|
36
|
+
"fisher_to_getdist_samples",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def fisher_to_getdist_gaussiannd(
|
|
41
|
+
theta0: NDArray[np.floating],
|
|
42
|
+
fisher: NDArray[np.floating],
|
|
43
|
+
*,
|
|
44
|
+
names: Sequence[str] | None = None,
|
|
45
|
+
labels: Sequence[str] | None = None,
|
|
46
|
+
label: str = "Fisher (Gaussian)",
|
|
47
|
+
rcond: float | None = None,
|
|
48
|
+
) -> GaussianND:
|
|
49
|
+
"""Returns :class:`getdist.gaussian_mixtures.GaussianND` for the Fisher Gaussian.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
theta0: Fiducial parameter vector with shape ``(p,)`` with ``p`` parameters.
|
|
53
|
+
fisher: Fisher matrix with shape ``(p, p)``.
|
|
54
|
+
names: Optional parameter names (length ``p``).
|
|
55
|
+
Defaults to ``["p" + str(x) for x in range(len(theta0))]``.
|
|
56
|
+
labels: Optional parameter labels (length ``p``).
|
|
57
|
+
Defaults to ``["p" + str(x) for x in range(len(theta0))]``.
|
|
58
|
+
label: Label attached to the returned object.
|
|
59
|
+
rcond: Cutoff passed to the Fisher (pseudo-)inverse when forming the covariance.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
A :class:`getdist.gaussian_mixtures.GaussianND` with mean ``theta0`` and covariance from ``fisher``.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
ValueError: If ``names``/``labels`` lengths do not match ``p``.
|
|
66
|
+
"""
|
|
67
|
+
theta0 = np.asarray(theta0, dtype=float)
|
|
68
|
+
fisher = np.asarray(fisher, dtype=float)
|
|
69
|
+
validate_fisher_shape(theta0, fisher)
|
|
70
|
+
|
|
71
|
+
n_params = int(theta0.size)
|
|
72
|
+
|
|
73
|
+
default_names = [f"p{i}" for i in range(n_params)]
|
|
74
|
+
default_labels = [rf"p_{{{i}}}" for i in range(n_params)]
|
|
75
|
+
|
|
76
|
+
param_names = list(default_names if names is None else names)
|
|
77
|
+
param_labels = list(default_labels if labels is None else labels)
|
|
78
|
+
|
|
79
|
+
if len(param_names) != n_params:
|
|
80
|
+
raise ValueError(
|
|
81
|
+
f"`names` must have length {n_params} (number of parameters), got {len(param_names)}."
|
|
82
|
+
)
|
|
83
|
+
if len(param_labels) != n_params:
|
|
84
|
+
raise ValueError(
|
|
85
|
+
f"`labels` must have length {n_params} (number of parameters), got {len(param_labels)}."
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
covariance = fisher_to_cov(fisher, rcond=rcond)
|
|
89
|
+
|
|
90
|
+
return GaussianND(
|
|
91
|
+
mean=theta0,
|
|
92
|
+
cov=covariance,
|
|
93
|
+
names=param_names,
|
|
94
|
+
labels=param_labels,
|
|
95
|
+
label=label,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def fisher_to_getdist_samples(
|
|
100
|
+
theta0: NDArray[np.floating],
|
|
101
|
+
fisher: NDArray[np.floating],
|
|
102
|
+
*,
|
|
103
|
+
names: Sequence[str],
|
|
104
|
+
labels: Sequence[str],
|
|
105
|
+
n_samples: int = 30_000,
|
|
106
|
+
seed: int | None = None,
|
|
107
|
+
kernel_scale: float = 1.0,
|
|
108
|
+
prior_terms: Sequence[tuple[str, dict[str, Any]] | dict[str, Any]] | None = None,
|
|
109
|
+
prior_bounds: Sequence[tuple[float | None, float | None]] | None = None,
|
|
110
|
+
logprior: Callable[[NDArray[np.floating]], float] | None = None,
|
|
111
|
+
hard_bounds: Sequence[tuple[float | None, float | None]] | None = None,
|
|
112
|
+
store_loglikes: bool = True,
|
|
113
|
+
label: str = "Fisher (samples)",
|
|
114
|
+
) -> MCSamples:
|
|
115
|
+
"""Draws samples from the Fisher Gaussian as :class:`getdist.MCSamples`.
|
|
116
|
+
|
|
117
|
+
Samples are drawn from a multivariate Gaussian with mean ``theta0`` and
|
|
118
|
+
covariance ``kernel_scale**2 * pinv(fisher)``. Optionally, samples are
|
|
119
|
+
truncated by hard bounds and/or by a prior (via ``logprior`` or
|
|
120
|
+
``prior_terms``/``prior_bounds``).
|
|
121
|
+
|
|
122
|
+
GetDist stores :attr:`getdist.MCSamples.loglikes` as ``-log(posterior)`` up to an additive
|
|
123
|
+
constant. When ``store_loglikes=True``, this function stores::
|
|
124
|
+
|
|
125
|
+
-log p(theta|d) = 0.5 * (theta-theta0)^T F (theta-theta0) - logprior(theta) + C
|
|
126
|
+
|
|
127
|
+
where ``C`` is an arbitrary additive constant and ``F`` defines the Fisher-Gaussian
|
|
128
|
+
approximation to ``-log(likelihoods)`` around ``theta0``.
|
|
129
|
+
In this implementation, ``C`` is effectively zero since no additional shifting is applied.
|
|
130
|
+
If no prior is provided, the prior term is omitted.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
theta0: Fiducial parameter vector with shape ``(p,)`` with ``p`` parameters.
|
|
134
|
+
fisher: Fisher matrix with shape ``(p, p)``.
|
|
135
|
+
names: Parameter names (length ``p``).
|
|
136
|
+
labels: Parameter labels (length ``p``).
|
|
137
|
+
n_samples: Number of samples to draw.
|
|
138
|
+
seed: Random seed.
|
|
139
|
+
kernel_scale: Multiplicative scale applied to the Gaussian covariance.
|
|
140
|
+
prior_terms: Optional prior term specifications used to build a prior.
|
|
141
|
+
Can be provided with or without ``prior_bounds``.
|
|
142
|
+
prior_bounds: Optional bounds used to truncate prior support.
|
|
143
|
+
Can be provided with or without ``prior_terms``.
|
|
144
|
+
logprior: Custom log-prior callable. Mutually exclusive with
|
|
145
|
+
``prior_terms``/``prior_bounds``.
|
|
146
|
+
hard_bounds: Hard bounds applied by rejection (samples outside are dropped).
|
|
147
|
+
Mutually exclusive with encoding support via ``prior_terms``/``prior_bounds``.
|
|
148
|
+
store_loglikes: If ``True``, compute and store :attr:`getdist.MCSamples.loglikes`.
|
|
149
|
+
label: Label for the returned :class:`getdist.MCSamples`.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
:class:`getdist.MCSamples` containing the retained samples and
|
|
153
|
+
optional :attr:`getdist.MCSamples.loglikes`.
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
ValueError: If ``n_samples`` is non-positive, if ``names`` or ``labels`` have
|
|
157
|
+
incorrect length, or if mutually exclusive options are provided.
|
|
158
|
+
RuntimeError: If all samples are rejected by bounds or prior support.
|
|
159
|
+
"""
|
|
160
|
+
theta0 = np.asarray(theta0, dtype=float)
|
|
161
|
+
fisher = np.asarray(fisher, dtype=float)
|
|
162
|
+
validate_fisher_shape(theta0, fisher)
|
|
163
|
+
|
|
164
|
+
n_params = int(theta0.size)
|
|
165
|
+
|
|
166
|
+
if len(names) != n_params:
|
|
167
|
+
raise ValueError(
|
|
168
|
+
f"`names` must have length {n_params} (number of parameters), got {len(names)}."
|
|
169
|
+
)
|
|
170
|
+
if len(labels) != n_params:
|
|
171
|
+
raise ValueError(
|
|
172
|
+
f"`labels` must have length {n_params} (number of parameters), got {len(labels)}."
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
n_samples = int(n_samples)
|
|
176
|
+
if n_samples <= 0:
|
|
177
|
+
raise ValueError("n_samples must be a positive integer")
|
|
178
|
+
|
|
179
|
+
if logprior is not None and (prior_terms is not None or prior_bounds is not None):
|
|
180
|
+
raise ValueError(
|
|
181
|
+
"Ambiguous prior specification: pass either `logprior` or `prior_terms` and `prior_bounds`, not both. "
|
|
182
|
+
"`prior_terms` and `prior_bounds` can be provided independently."
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if hard_bounds is not None and (prior_terms is not None or prior_bounds is not None):
|
|
186
|
+
raise ValueError(
|
|
187
|
+
"Ambiguous support: choose either `hard_bounds` or prior-based support "
|
|
188
|
+
"via (`prior_terms`/`prior_bounds`)."
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
samples = kernel_samples_from_fisher(
|
|
192
|
+
theta0,
|
|
193
|
+
fisher,
|
|
194
|
+
n_samples=n_samples,
|
|
195
|
+
kernel_scale=float(kernel_scale),
|
|
196
|
+
seed=seed,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
samples = apply_parameter_bounds(samples, hard_bounds)
|
|
200
|
+
if samples.shape[0] == 0:
|
|
201
|
+
raise RuntimeError("All samples rejected by hard bounds (no samples left).")
|
|
202
|
+
|
|
203
|
+
delta = samples - theta0[None, :]
|
|
204
|
+
theta_quad = np.einsum("ni,ij,nj->n", delta, fisher, delta).astype(float, copy=False)
|
|
205
|
+
|
|
206
|
+
loglikes: NDArray[np.floating] | None = None
|
|
207
|
+
if store_loglikes:
|
|
208
|
+
logprior_fn: Callable[[NDArray[np.floating]], float] | None = None
|
|
209
|
+
if logprior is not None:
|
|
210
|
+
logprior_fn = logprior
|
|
211
|
+
elif prior_terms is not None or prior_bounds is not None:
|
|
212
|
+
logprior_fn = build_prior(terms=prior_terms, bounds=prior_bounds)
|
|
213
|
+
|
|
214
|
+
log_prior_vals: NDArray[np.floating] | None = None
|
|
215
|
+
if logprior_fn is not None:
|
|
216
|
+
log_prior_vals = np.array([float(logprior_fn(s)) for s in samples], dtype=float)
|
|
217
|
+
keep = np.isfinite(log_prior_vals)
|
|
218
|
+
samples = samples[keep]
|
|
219
|
+
theta_quad = theta_quad[keep]
|
|
220
|
+
log_prior_vals = log_prior_vals[keep]
|
|
221
|
+
if samples.shape[0] == 0:
|
|
222
|
+
raise RuntimeError(
|
|
223
|
+
"All samples were rejected by the prior: logprior evaluated to -inf for every sample, "
|
|
224
|
+
"indicating zero prior density outside the allowed prior support."
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
loglikes = 0.5 * theta_quad if log_prior_vals is None else (0.5 * theta_quad - log_prior_vals)
|
|
228
|
+
|
|
229
|
+
return MCSamples(
|
|
230
|
+
samples=samples,
|
|
231
|
+
loglikes=loglikes,
|
|
232
|
+
names=list(names),
|
|
233
|
+
labels=list(labels),
|
|
234
|
+
label=label,
|
|
235
|
+
)
|