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,860 @@
|
|
|
1
|
+
"""Prior utilities (core priors + unified builder).
|
|
2
|
+
|
|
3
|
+
Priors are represented as functions that evaluate how compatible a parameter vector is
|
|
4
|
+
with the prior assumptions. They return a single log-prior value, with negative infinity
|
|
5
|
+
used to exclude invalid parameter values.
|
|
6
|
+
|
|
7
|
+
The return value is interpreted as a log-density defined up to an additive
|
|
8
|
+
constant. Returning ``-np.inf`` denotes zero probability (hard exclusion).
|
|
9
|
+
|
|
10
|
+
These priors are intended for use when constructing log-posteriors for sampling
|
|
11
|
+
or when evaluating posterior surfaces. Plotting tools such as GetDist only
|
|
12
|
+
visualize the distributions they are given and do not apply priors implicitly.
|
|
13
|
+
As a result, priors must be included explicitly: either by adding them when
|
|
14
|
+
generating samples, or, in the case of Gaussian approximations, by incorporating
|
|
15
|
+
them directly into the Fisher matrix.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from functools import partial
|
|
21
|
+
from typing import Any, Callable, Sequence
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
from numpy.typing import NDArray
|
|
25
|
+
|
|
26
|
+
from derivkit.utils.linalg import invert_covariance, normalize_covariance
|
|
27
|
+
from derivkit.utils.numerics import (
|
|
28
|
+
apply_hard_bounds,
|
|
29
|
+
get_index_value,
|
|
30
|
+
logsumexp_1d,
|
|
31
|
+
sum_terms,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"prior_none",
|
|
36
|
+
"prior_uniform",
|
|
37
|
+
"prior_gaussian",
|
|
38
|
+
"prior_gaussian_diag",
|
|
39
|
+
"prior_log_uniform",
|
|
40
|
+
"prior_jeffreys",
|
|
41
|
+
"prior_half_normal",
|
|
42
|
+
"prior_half_cauchy",
|
|
43
|
+
"prior_log_normal",
|
|
44
|
+
"prior_beta",
|
|
45
|
+
"prior_gaussian_mixture",
|
|
46
|
+
"build_prior",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _prior_none_impl(
|
|
51
|
+
_theta: NDArray[np.floating]
|
|
52
|
+
) -> float:
|
|
53
|
+
"""Returns a constant log-prior (improper flat prior).
|
|
54
|
+
|
|
55
|
+
This prior is constant in parameter space (up to an additive constant), so it
|
|
56
|
+
contributes zero to the log-posterior at every point.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
_theta: Parameter vector. This argument is unused
|
|
60
|
+
and present only for signature compatibility.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Log-prior value (always ``0.0``).
|
|
64
|
+
"""
|
|
65
|
+
return 0.0
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _prior_1d_impl(
|
|
69
|
+
theta: NDArray[np.floating],
|
|
70
|
+
*,
|
|
71
|
+
index: int,
|
|
72
|
+
domain: str,
|
|
73
|
+
kind: str,
|
|
74
|
+
a: float | np.floating = 0.0,
|
|
75
|
+
b: float | np.floating = 1.0,
|
|
76
|
+
) -> float:
|
|
77
|
+
"""A generic 1D prior implementation.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
theta: Parameter vector.
|
|
81
|
+
index: Index of the parameter to which the prior applies.
|
|
82
|
+
domain: Domain restriction (``"positive"``, ``"nonnegative"``, ``"unit_open"``).
|
|
83
|
+
kind: Prior kind (``"log_uniform"``, ``"half_normal"``, ``"half_cauchy"``, ``"log_normal"``, ``"beta"``).
|
|
84
|
+
a: First prior parameter (meaning depends on ``kind``).
|
|
85
|
+
b: Second prior parameter (meaning depends on ``kind``).
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Log-prior value for the specified parameter.
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
ValueError: If ``domain`` is unknown, if ``kind`` is unknown, or if the
|
|
92
|
+
distribution parameters are invalid (e.g., non-positive scale/sigma or
|
|
93
|
+
non-positive ``a/b``).
|
|
94
|
+
"""
|
|
95
|
+
x = get_index_value(theta, index, name="theta")
|
|
96
|
+
|
|
97
|
+
match domain:
|
|
98
|
+
case "positive":
|
|
99
|
+
if x <= 0.0:
|
|
100
|
+
return -np.inf
|
|
101
|
+
case "nonnegative":
|
|
102
|
+
if x < 0.0:
|
|
103
|
+
return -np.inf
|
|
104
|
+
case "unit_open":
|
|
105
|
+
if x <= 0.0 or x >= 1.0:
|
|
106
|
+
return -np.inf
|
|
107
|
+
case _:
|
|
108
|
+
raise ValueError(f"unknown domain '{domain}'")
|
|
109
|
+
|
|
110
|
+
logp: float
|
|
111
|
+
match kind:
|
|
112
|
+
case "log_uniform":
|
|
113
|
+
logp = float(-np.log(x))
|
|
114
|
+
|
|
115
|
+
case "half_normal":
|
|
116
|
+
sigma = float(a)
|
|
117
|
+
if sigma <= 0.0:
|
|
118
|
+
raise ValueError("sigma must be > 0")
|
|
119
|
+
logp = float(-0.5 * (x / sigma) ** 2)
|
|
120
|
+
|
|
121
|
+
case "half_cauchy":
|
|
122
|
+
scale = float(a)
|
|
123
|
+
if scale <= 0.0:
|
|
124
|
+
raise ValueError("scale must be > 0")
|
|
125
|
+
t = x / scale
|
|
126
|
+
logp = float(-np.log1p(t * t))
|
|
127
|
+
|
|
128
|
+
case "log_normal":
|
|
129
|
+
mean = float(a)
|
|
130
|
+
sigma = float(b)
|
|
131
|
+
if sigma <= 0.0:
|
|
132
|
+
raise ValueError("sigma must be > 0")
|
|
133
|
+
lx = np.log(x)
|
|
134
|
+
z = (lx - mean) / sigma
|
|
135
|
+
logp = float(-0.5 * z * z - lx)
|
|
136
|
+
|
|
137
|
+
case "beta":
|
|
138
|
+
alpha = float(a)
|
|
139
|
+
beta = float(b)
|
|
140
|
+
if alpha <= 0.0 or beta <= 0.0:
|
|
141
|
+
raise ValueError("alpha and beta must be > 0")
|
|
142
|
+
logp = float((alpha - 1.0) * np.log(x) + (beta - 1.0) * np.log1p(-x))
|
|
143
|
+
|
|
144
|
+
case _:
|
|
145
|
+
raise ValueError(f"unknown prior kind '{kind}'")
|
|
146
|
+
|
|
147
|
+
return logp
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _prior_gaussian_impl(
|
|
151
|
+
theta: NDArray[np.floating],
|
|
152
|
+
*,
|
|
153
|
+
mean: NDArray[np.floating],
|
|
154
|
+
inv_cov: NDArray[np.floating],
|
|
155
|
+
) -> float:
|
|
156
|
+
"""Evaluates a multivariate Gaussian log-prior (up to an additive constant).
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
theta: Parameter vector.
|
|
160
|
+
mean: Mean vector.
|
|
161
|
+
inv_cov: Inverse covariance matrix.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Log-prior value.
|
|
165
|
+
|
|
166
|
+
Raises:
|
|
167
|
+
ValueError: If ``theta``/``mean`` are not 1D or if ``inv_cov`` does not have
|
|
168
|
+
shape ``(p, p)`` consistent with ``mean``.
|
|
169
|
+
"""
|
|
170
|
+
thetas = np.asarray(theta, dtype=np.float64)
|
|
171
|
+
means = np.asarray(mean, dtype=np.float64)
|
|
172
|
+
inv_cov = np.asarray(inv_cov, dtype=np.float64)
|
|
173
|
+
|
|
174
|
+
if means.ndim != 1:
|
|
175
|
+
raise ValueError(f"mean must be 1D, got shape {means.shape}")
|
|
176
|
+
if inv_cov.ndim != 2 or inv_cov.shape[0] != inv_cov.shape[1] or inv_cov.shape[0] != means.size:
|
|
177
|
+
raise ValueError(f"inv_cov must have shape (p, p) with p={means.size}, got {inv_cov.shape}")
|
|
178
|
+
if thetas.ndim != 1 or thetas.size != means.size:
|
|
179
|
+
raise ValueError(f"theta must have shape ({means.size},), got {thetas.shape}")
|
|
180
|
+
|
|
181
|
+
diff = thetas - means
|
|
182
|
+
return float(-0.5 * (diff @ inv_cov @ diff))
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _prior_gaussian_diag_impl(
|
|
186
|
+
theta: NDArray[np.floating],
|
|
187
|
+
*,
|
|
188
|
+
mean: NDArray[np.floating],
|
|
189
|
+
inv_cov: NDArray[np.floating],
|
|
190
|
+
) -> float:
|
|
191
|
+
"""Evaluates a diagonal multivariate Gaussian log-prior (up to an additive constant).
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
theta: Parameter vector.
|
|
195
|
+
mean: Mean vector.
|
|
196
|
+
inv_cov: Inverse covariance matrix (must be diagonal).
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Log-prior value.
|
|
200
|
+
|
|
201
|
+
Raises:
|
|
202
|
+
ValueError: If shapes are inconsistent, if ``inv_cov`` is not diagonal, or if
|
|
203
|
+
any diagonal entry of ``inv_cov`` is non-positive.
|
|
204
|
+
"""
|
|
205
|
+
thetas = np.asarray(theta, dtype=np.float64)
|
|
206
|
+
means = np.asarray(mean, dtype=np.float64)
|
|
207
|
+
inv_cov = np.asarray(inv_cov, dtype=np.float64)
|
|
208
|
+
|
|
209
|
+
if means.ndim != 1:
|
|
210
|
+
raise ValueError(f"mean must be 1D, got shape {means.shape}")
|
|
211
|
+
p = means.size
|
|
212
|
+
if inv_cov.ndim != 2 or inv_cov.shape != (p, p):
|
|
213
|
+
raise ValueError(f"inv_cov must have shape ({p},{p}), got {inv_cov.shape}")
|
|
214
|
+
if thetas.ndim != 1 or thetas.size != p:
|
|
215
|
+
raise ValueError(f"theta must have shape ({p},), got {thetas.shape}")
|
|
216
|
+
|
|
217
|
+
inv_var = np.diag(inv_cov)
|
|
218
|
+
off_diag = inv_cov.copy()
|
|
219
|
+
np.fill_diagonal(off_diag, 0.0)
|
|
220
|
+
if not np.allclose(off_diag, 0.0, rtol=1e-12, atol=1e-12):
|
|
221
|
+
raise ValueError("inv_cov must be diagonal for prior_gaussian_diag")
|
|
222
|
+
if np.any(inv_var <= 0.0):
|
|
223
|
+
raise ValueError("diagonal inv_cov entries must be > 0")
|
|
224
|
+
|
|
225
|
+
diff = thetas - means
|
|
226
|
+
return float(-0.5 * np.sum(diff * diff * inv_var))
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _prior_gaussian_mixture_impl(
|
|
230
|
+
theta: NDArray[np.floating],
|
|
231
|
+
*,
|
|
232
|
+
means: NDArray[np.floating],
|
|
233
|
+
inv_covs: NDArray[np.floating],
|
|
234
|
+
log_weights: NDArray[np.floating],
|
|
235
|
+
log_component_norm: NDArray[np.floating],
|
|
236
|
+
) -> float:
|
|
237
|
+
"""Evaluates a Gaussian mixture log-prior at ``theta``.
|
|
238
|
+
|
|
239
|
+
This function computes the log of a weighted sum of Gaussian components::
|
|
240
|
+
|
|
241
|
+
p(theta) = sum_n w_n * N(theta | means_n, cov_n)
|
|
242
|
+
|
|
243
|
+
where ``N(theta | mean, cov)`` is the multivariate Gaussian density with the
|
|
244
|
+
specified mean and covariance; ``w_n` are the mixture weights (in log-space); and
|
|
245
|
+
the sum runs over the ``n=1..N`` components.
|
|
246
|
+
|
|
247
|
+
The result is a log-density defined up to an additive constant.
|
|
248
|
+
|
|
249
|
+
Notes:
|
|
250
|
+
- ``inv_covs`` are the per-component inverse covariances (precision matrices).
|
|
251
|
+
- ``log_weights`` are the mixture weights in log-space; they are typically
|
|
252
|
+
normalized so that ``logsumexp_1d(log_weights) = 0``.
|
|
253
|
+
- ``log_component_norm`` controls whether per-component normalization is
|
|
254
|
+
included. If it contains ``-0.5 * log(|C_n|)`` (or the equivalent computed
|
|
255
|
+
from ``C_n^{-1}``), then components with different covariances get the
|
|
256
|
+
correct relative normalization. If it is all zeros, the mixture keeps
|
|
257
|
+
only the quadratic terms, which can be useful for shape-only priors.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
theta: Parameter vector ``theta`` with shape ``(p,)``.
|
|
261
|
+
means: Component means with shape ``(n, p)``.
|
|
262
|
+
inv_covs: Component inverse covariances with shape ``(n, p, p)``.
|
|
263
|
+
log_weights: Log-weights for the ``n`` components with shape ``(n,)``.
|
|
264
|
+
log_component_norm: Per-component log-normalization terms with shape
|
|
265
|
+
``(n,)`` (often ``-0.5 * log(|C_n|)``). Use zeros to omit this factor.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
The mixture log-prior value at ``theta`` (a finite float or ``-np.inf`` if
|
|
269
|
+
the caller enforces hard bounds elsewhere).
|
|
270
|
+
|
|
271
|
+
Raises:
|
|
272
|
+
ValueError: If input arrays have incompatible shapes or dimensions.
|
|
273
|
+
"""
|
|
274
|
+
thetas = np.asarray(theta, dtype=np.float64)
|
|
275
|
+
means = np.asarray(means, dtype=np.float64)
|
|
276
|
+
inv_covs = np.asarray(inv_covs, dtype=np.float64)
|
|
277
|
+
log_weights = np.asarray(log_weights, dtype=np.float64)
|
|
278
|
+
log_comp_norm = np.asarray(log_component_norm, dtype=np.float64)
|
|
279
|
+
|
|
280
|
+
if thetas.ndim != 1:
|
|
281
|
+
raise ValueError(f"theta must be 1D, got shape {thetas.shape}")
|
|
282
|
+
|
|
283
|
+
if means.ndim != 2:
|
|
284
|
+
raise ValueError(f"means must be (n, p), got shape {means.shape}")
|
|
285
|
+
|
|
286
|
+
n, p = means.shape
|
|
287
|
+
if thetas.size != p:
|
|
288
|
+
raise ValueError(f"theta length {thetas.size} != p {p}")
|
|
289
|
+
|
|
290
|
+
if inv_covs.ndim != 3 or inv_covs.shape != (n, p, p):
|
|
291
|
+
raise ValueError(f"inv_covs must be (n, p, p) with n={n}, p={p}, got shape {inv_covs.shape}")
|
|
292
|
+
|
|
293
|
+
if log_weights.ndim != 1 or log_weights.size != n:
|
|
294
|
+
raise ValueError(f"log_weights must be (n,), got shape {log_weights.shape}")
|
|
295
|
+
|
|
296
|
+
if log_comp_norm.ndim != 1 or log_comp_norm.size != n:
|
|
297
|
+
raise ValueError(f"log_component_norm must be (n,), got shape {log_comp_norm.shape}")
|
|
298
|
+
|
|
299
|
+
vals = np.empty(n, dtype=np.float64)
|
|
300
|
+
for i in range(n):
|
|
301
|
+
diff = thetas - means[i]
|
|
302
|
+
vals[i] = log_weights[i] + log_comp_norm[i] - 0.5 * float(diff @ inv_covs[i] @ diff)
|
|
303
|
+
|
|
304
|
+
return float(logsumexp_1d(vals))
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def prior_none() -> Callable[[NDArray[np.floating]], float]:
|
|
308
|
+
"""Constructs an improper flat prior (constant log-density).
|
|
309
|
+
|
|
310
|
+
This prior has a density proportional to 1 everywhere.
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
Log-prior value (always ``0.0``).
|
|
314
|
+
"""
|
|
315
|
+
return _prior_none_impl
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def prior_uniform(
|
|
319
|
+
*,
|
|
320
|
+
bounds: Sequence[tuple[float | np.floating | None, float | np.floating | None]],
|
|
321
|
+
) -> Callable[[NDArray[np.floating]], float]:
|
|
322
|
+
"""Constructs a uniform prior with hard bounds.
|
|
323
|
+
|
|
324
|
+
This prior has a density proportional to 1 within the specified bounds
|
|
325
|
+
and zero outside. The log-density is constant (up to an additive constant)
|
|
326
|
+
within the bounds and ``-np.inf`` outside.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
bounds: Sequence of (min, max) pairs for each parameter.
|
|
330
|
+
Use None for unbounded sides.
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
A callable that evaluates the log-prior at a given parameter vector.
|
|
334
|
+
"""
|
|
335
|
+
return apply_hard_bounds(_prior_none_impl, bounds=bounds)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def prior_gaussian(
|
|
339
|
+
*,
|
|
340
|
+
mean: NDArray[np.floating],
|
|
341
|
+
cov: NDArray[np.floating] | None = None,
|
|
342
|
+
inv_cov: NDArray[np.floating] | None = None,
|
|
343
|
+
) -> Callable[[NDArray[np.floating]], float]:
|
|
344
|
+
"""Constructs a multivariate Gaussian prior (up to an additive constant).
|
|
345
|
+
|
|
346
|
+
This prior has a density proportional to
|
|
347
|
+
``exp(-0.5 * (theta - mean)^T @ inv_cov @ (theta - mean))`` with ``inv_cov`` being the
|
|
348
|
+
inverse of the provided covariance matrix and ``theta`` being the parameter vector.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
mean: Mean vector.
|
|
352
|
+
cov: Covariance matrix (provide exactly one of ``cov`` or ``inv_cov``).
|
|
353
|
+
inv_cov: Inverse covariance matrix (provide exactly one of ``cov`` or ``inv_cov``).
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
A callable that evaluates the log-prior at a given parameter vector.
|
|
357
|
+
|
|
358
|
+
Raises:
|
|
359
|
+
ValueError: If neither or both of ``cov`` and ``inv_cov`` are provided,
|
|
360
|
+
or if the provided covariance/inverse covariance cannot be
|
|
361
|
+
normalized/validated.
|
|
362
|
+
"""
|
|
363
|
+
if (cov is None) == (inv_cov is None):
|
|
364
|
+
raise ValueError("Provide exactly one of `cov` or `inv_cov`.")
|
|
365
|
+
|
|
366
|
+
means = np.asarray(mean, dtype=np.float64)
|
|
367
|
+
if means.ndim != 1:
|
|
368
|
+
raise ValueError(f"mean must be 1D, got shape {means.shape}")
|
|
369
|
+
|
|
370
|
+
if inv_cov is None:
|
|
371
|
+
cov = normalize_covariance(cov, n_parameters=means.size)
|
|
372
|
+
inv_cov = invert_covariance(cov, warn_prefix="prior_gaussian")
|
|
373
|
+
else:
|
|
374
|
+
inv_cov = np.asarray(inv_cov, dtype=np.float64)
|
|
375
|
+
if inv_cov.ndim != 2 or inv_cov.shape != (means.size, means.size):
|
|
376
|
+
raise ValueError(f"inv_cov must have shape (p,p) with p={means.size}, got {inv_cov.shape}")
|
|
377
|
+
if not np.all(np.isfinite(inv_cov)):
|
|
378
|
+
raise ValueError("inv_cov contains non-finite values.")
|
|
379
|
+
|
|
380
|
+
return partial(_prior_gaussian_impl, mean=means, inv_cov=inv_cov)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def prior_gaussian_diag(
|
|
384
|
+
*,
|
|
385
|
+
mean: NDArray[np.floating],
|
|
386
|
+
sigma: NDArray[np.floating],
|
|
387
|
+
) -> Callable[[NDArray[np.floating]], float]:
|
|
388
|
+
"""Constructs a diagonal Gaussian prior (up to an additive constant).
|
|
389
|
+
|
|
390
|
+
This prior has a density proportional to
|
|
391
|
+
``exp(-0.5 * sum_i ((x_i - mean_i) / sigma_i)^2)``, with independent components.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
mean: Mean vector.
|
|
395
|
+
sigma: Standard deviation vector (must be positive).
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
A callable that evaluates the log-prior at a given parameter vector.
|
|
399
|
+
|
|
400
|
+
Raises:
|
|
401
|
+
ValueError: If `mean` and `sigma` have incompatible shapes,
|
|
402
|
+
or if any `sigma` entries are non-positive.
|
|
403
|
+
"""
|
|
404
|
+
means = np.asarray(mean, dtype=np.float64)
|
|
405
|
+
sigmas = np.asarray(sigma, dtype=np.float64)
|
|
406
|
+
|
|
407
|
+
if means.ndim != 1 or sigmas.ndim != 1 or means.shape != sigmas.shape:
|
|
408
|
+
raise ValueError("mean and sigma must be 1D arrays with the same shape")
|
|
409
|
+
if np.any(sigmas <= 0.0):
|
|
410
|
+
raise ValueError("all sigma entries must be > 0")
|
|
411
|
+
|
|
412
|
+
inv_cov = np.diag(1.0 / (sigmas ** 2))
|
|
413
|
+
return partial(_prior_gaussian_diag_impl, mean=means, inv_cov=inv_cov)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def prior_log_uniform(
|
|
417
|
+
*,
|
|
418
|
+
index: int,
|
|
419
|
+
) -> Callable[[NDArray[np.floating]], float]:
|
|
420
|
+
"""Constructs a log-uniform prior for a single positive parameter.
|
|
421
|
+
|
|
422
|
+
This prior assigns equal weight to multiplicative (relative) changes in the
|
|
423
|
+
parameter rather than additive changes. It is commonly used for positive,
|
|
424
|
+
scale-like parameters when no preferred scale is known.
|
|
425
|
+
|
|
426
|
+
For a positive parameter :math:`x`, the (improper) prior density is
|
|
427
|
+
proportional to :math:`1/x` on :math:`(0, infinity)`. This has the same
|
|
428
|
+
functional form as the Jeffreys prior for a positive scale parameter; see
|
|
429
|
+
``prior_jeffreys`` for that interpretation.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
index: Index of the parameter to which the prior applies.
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
A callable that evaluates the log-prior at a given parameter vector.
|
|
436
|
+
"""
|
|
437
|
+
return partial(_prior_1d_impl, index=int(index), domain="positive", kind="log_uniform")
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def prior_jeffreys(
|
|
441
|
+
*,
|
|
442
|
+
index: int,
|
|
443
|
+
) -> Callable[[NDArray[np.floating]], float]:
|
|
444
|
+
"""Constructs a Jeffreys prior for a single positive scale parameter.
|
|
445
|
+
|
|
446
|
+
This prior encodes reparameterization invariance for a positive scale
|
|
447
|
+
parameter: inference does not depend on the choice of units used to describe
|
|
448
|
+
the parameter. It is commonly used to express ignorance about the absolute
|
|
449
|
+
scale of a quantity.
|
|
450
|
+
|
|
451
|
+
For a positive scale parameter :math:`x`, the (improper) prior density is
|
|
452
|
+
proportional to :math:`1/x` on :math:`(0, infinity)`. In practice, this has
|
|
453
|
+
the same functional form as the log-uniform prior; the separate name exists
|
|
454
|
+
to emphasize the statistical motivation.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
index: Index of the parameter to which the prior applies.
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
A callable that evaluates the log-prior at a given parameter vector.
|
|
461
|
+
"""
|
|
462
|
+
return partial(_prior_1d_impl, index=int(index), domain="positive", kind="log_uniform")
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def prior_half_normal(
|
|
466
|
+
*,
|
|
467
|
+
index: int,
|
|
468
|
+
sigma: float | np.floating,
|
|
469
|
+
) -> Callable[[NDArray[np.floating]], float]:
|
|
470
|
+
"""Constructs a half-normal prior for a single non-negative parameter.
|
|
471
|
+
|
|
472
|
+
This prior is a commonly used weakly informative choice for non-negative
|
|
473
|
+
amplitude or scale parameters. It concentrates probability near zero while
|
|
474
|
+
allowing larger values with Gaussian tails, making it suitable when the
|
|
475
|
+
parameter is expected to be small but not exactly zero.
|
|
476
|
+
|
|
477
|
+
The half-normal distribution is obtained by taking the absolute value of a
|
|
478
|
+
zero-mean normal distribution, ``|N(0, sigma)|`` with N being a normal random
|
|
479
|
+
variable and sigma the standard deviation.
|
|
480
|
+
|
|
481
|
+
The (unnormalized) density is proportional to
|
|
482
|
+
``exp(-0.5 * (x / sigma)**2)`` for ``x >= 0``.
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
index: Index of the parameter to which the prior applies.
|
|
486
|
+
sigma: Standard deviation of the underlying normal distribution.
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
A callable that evaluates the log-prior at a given parameter vector.
|
|
490
|
+
|
|
491
|
+
Raises:
|
|
492
|
+
ValueError: If `sigma` is not positive.
|
|
493
|
+
"""
|
|
494
|
+
sigma = float(sigma)
|
|
495
|
+
if sigma <= 0.0:
|
|
496
|
+
raise ValueError("sigma must be > 0")
|
|
497
|
+
return partial(_prior_1d_impl, index=int(index), domain="nonnegative", kind="half_normal", a=sigma)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def prior_half_cauchy(
|
|
501
|
+
*,
|
|
502
|
+
index: int,
|
|
503
|
+
scale: float | np.floating
|
|
504
|
+
) -> Callable[[NDArray[np.floating]], float]:
|
|
505
|
+
"""Constructs a half-Cauchy prior for a single non-negative parameter.
|
|
506
|
+
|
|
507
|
+
This prior has a density proportional to ``1 / (1 + (x/scale)^2)`` for ``x >= 0``,
|
|
508
|
+
with x being the parameter.
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
index: Index of the parameter to which the prior applies.
|
|
512
|
+
scale: Scale parameter of the half-Cauchy distribution.
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
A callable that evaluates the log-prior at a given parameter vector.
|
|
516
|
+
|
|
517
|
+
Raises:
|
|
518
|
+
ValueError: If `scale` is not positive.
|
|
519
|
+
"""
|
|
520
|
+
scale = float(scale)
|
|
521
|
+
if scale <= 0.0:
|
|
522
|
+
raise ValueError("scale must be > 0")
|
|
523
|
+
return partial(_prior_1d_impl, index=int(index), domain="nonnegative", kind="half_cauchy", a=scale)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def prior_log_normal(
|
|
527
|
+
*,
|
|
528
|
+
index: int,
|
|
529
|
+
mean_log: float | np.floating,
|
|
530
|
+
sigma_log: float | np.floating
|
|
531
|
+
) -> Callable[[NDArray[np.floating]], float]:
|
|
532
|
+
"""Constructs a log-normal prior for a single positive parameter.
|
|
533
|
+
|
|
534
|
+
This prior has a density proportional to
|
|
535
|
+
``exp(-0.5 * ((log(x) - mean_log) / sigma_log) ** 2) / x`` for ``x > 0``.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
index: Index of the parameter to which the prior applies.
|
|
539
|
+
mean_log: Mean of the underlying normal distribution in log-space.
|
|
540
|
+
sigma_log: Standard deviation of the underlying normal distribution in log-space.
|
|
541
|
+
|
|
542
|
+
Returns:
|
|
543
|
+
A callable that evaluates the log-prior at a given parameter vector.
|
|
544
|
+
|
|
545
|
+
Raises:
|
|
546
|
+
ValueError: If `sigma_log` is not positive.
|
|
547
|
+
"""
|
|
548
|
+
sig_log = float(sigma_log)
|
|
549
|
+
if sig_log <= 0.0:
|
|
550
|
+
raise ValueError("sigma_log must be > 0")
|
|
551
|
+
return partial(
|
|
552
|
+
_prior_1d_impl,
|
|
553
|
+
index=int(index),
|
|
554
|
+
domain="positive",
|
|
555
|
+
kind="log_normal",
|
|
556
|
+
a=float(mean_log),
|
|
557
|
+
b=sig_log)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def prior_beta(
|
|
561
|
+
*,
|
|
562
|
+
index: int,
|
|
563
|
+
alpha: float | np.floating,
|
|
564
|
+
beta: float | np.floating,
|
|
565
|
+
) -> Callable[[NDArray[np.floating]], float]:
|
|
566
|
+
"""Constructs a Beta distribution prior for a single parameter in ``(0, 1)``.
|
|
567
|
+
|
|
568
|
+
This prior uses the Beta density on ``x in (0, 1)``, with shape parameters
|
|
569
|
+
``alpha > 0`` and ``beta > 0``. The returned callable evaluates the
|
|
570
|
+
corresponding log-density up to an additive constant (the normalization
|
|
571
|
+
constant does not depend on ``x`` and is therefore omitted).
|
|
572
|
+
|
|
573
|
+
The (unnormalized) density is proportional to::
|
|
574
|
+
|
|
575
|
+
x**(alpha - 1) * (1 - x)**(beta - 1)
|
|
576
|
+
|
|
577
|
+
Args:
|
|
578
|
+
index: Index of the parameter to which the prior applies.
|
|
579
|
+
alpha: Alpha shape parameter (must be greater than 0).
|
|
580
|
+
beta: Beta shape parameter (must be greater than 0).
|
|
581
|
+
|
|
582
|
+
Returns:
|
|
583
|
+
A callable that evaluates the log-prior at a given parameter vector.
|
|
584
|
+
|
|
585
|
+
Raises:
|
|
586
|
+
ValueError: If ``alpha`` or ``beta`` are not positive.
|
|
587
|
+
"""
|
|
588
|
+
if alpha <= 0.0 or beta <= 0.0:
|
|
589
|
+
raise ValueError("alpha and beta must be > 0")
|
|
590
|
+
return partial(
|
|
591
|
+
_prior_1d_impl,
|
|
592
|
+
index=int(index),
|
|
593
|
+
domain="unit_open",
|
|
594
|
+
kind="beta",
|
|
595
|
+
a=alpha,
|
|
596
|
+
b=beta)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def prior_gaussian_mixture(
|
|
600
|
+
*,
|
|
601
|
+
means: NDArray[np.floating],
|
|
602
|
+
covs: NDArray[np.floating] | None = None,
|
|
603
|
+
inv_covs: NDArray[np.floating] | None = None,
|
|
604
|
+
weights: NDArray[np.floating] | None = None,
|
|
605
|
+
log_weights: NDArray[np.floating] | None = None,
|
|
606
|
+
include_component_norm: bool = True,
|
|
607
|
+
) -> Callable[[NDArray[np.floating]], float]:
|
|
608
|
+
"""Constructs a Gaussian mixture prior (up to an additive constant).
|
|
609
|
+
|
|
610
|
+
This prior has a density proportional to a weighted sum of Gaussian components.
|
|
611
|
+
|
|
612
|
+
The mixture is::
|
|
613
|
+
|
|
614
|
+
p(theta) = sum_n w_n * N(theta | mean_n, cov_n)
|
|
615
|
+
|
|
616
|
+
where ``N(theta | mean, cov)`` is the multivariate Gaussian density with the
|
|
617
|
+
specified mean and covariance; ``w_n`` are the mixture weights (non-negative,
|
|
618
|
+
summing to 1); and the sum runs over the n=1..N components.
|
|
619
|
+
|
|
620
|
+
Provide exactly one of:
|
|
621
|
+
|
|
622
|
+
- covs: ``(n, p, p)``
|
|
623
|
+
- inv_covs: ``(n, p, p)``
|
|
624
|
+
|
|
625
|
+
Here, ``n`` is the number of components and ``p`` is the parameter dimension.
|
|
626
|
+
|
|
627
|
+
Provide exactly one of:
|
|
628
|
+
|
|
629
|
+
- weights: (n,) non-negative (normalized internally)
|
|
630
|
+
- log_weights: (n,) (normalized internally in log-space)
|
|
631
|
+
|
|
632
|
+
Args:
|
|
633
|
+
means: Component means with shape ``(n, p)``.
|
|
634
|
+
covs: Component covariances with shape ``(n, p, p)``.
|
|
635
|
+
inv_covs: Component inverse covariances with shape ``(n, p, p)``.
|
|
636
|
+
weights: Mixture weights with shape ``(n,)``. Can include zeros.
|
|
637
|
+
log_weights: Log-weights with shape ``(n,)``. Can include -inf entries.
|
|
638
|
+
include_component_norm: If ``True`` (default), include the per-component
|
|
639
|
+
Gaussian normalization factor proportional to :math:`|C_n|^{-1/2}`.
|
|
640
|
+
This is important for *mixtures* when covariances differ.
|
|
641
|
+
|
|
642
|
+
Returns:
|
|
643
|
+
A callable that evaluates the log-prior at a given parameter vector.
|
|
644
|
+
|
|
645
|
+
Raises:
|
|
646
|
+
ValueError: If inputs have incompatible shapes, if both/neither of
|
|
647
|
+
``covs``/``inv_covs`` are provided, if both/neither of ``weights``/``log_weights``
|
|
648
|
+
are provided, if weights are invalid, or if covariance inputs are not
|
|
649
|
+
compatible with ``include_component_norm=True``.
|
|
650
|
+
"""
|
|
651
|
+
means = np.asarray(means, dtype=np.float64)
|
|
652
|
+
if means.ndim != 2:
|
|
653
|
+
raise ValueError(f"means must be (n, p), got shape {means.shape}")
|
|
654
|
+
n, p = means.shape
|
|
655
|
+
|
|
656
|
+
include = bool(include_component_norm)
|
|
657
|
+
|
|
658
|
+
if (covs is None) == (inv_covs is None):
|
|
659
|
+
raise ValueError("Provide exactly one of `covs` or `inv_covs`.")
|
|
660
|
+
|
|
661
|
+
if inv_covs is None:
|
|
662
|
+
cov = np.asarray(covs, dtype=np.float64)
|
|
663
|
+
if cov.ndim != 3 or cov.shape != (n, p, p):
|
|
664
|
+
raise ValueError(f"covs must be (n, p, p) with n={n}, p={p}, got shape {cov.shape}")
|
|
665
|
+
|
|
666
|
+
if include:
|
|
667
|
+
log_component_norm = np.empty(n, dtype=np.float64)
|
|
668
|
+
for i in range(n):
|
|
669
|
+
sign, logdet = np.linalg.slogdet(cov[i])
|
|
670
|
+
if sign <= 0 or not np.isfinite(logdet):
|
|
671
|
+
raise ValueError(
|
|
672
|
+
"include_component_norm=True requires each cov_n to be positive-definite "
|
|
673
|
+
"(slogdet sign>0 and finite)."
|
|
674
|
+
)
|
|
675
|
+
log_component_norm[i] = -0.5 * logdet
|
|
676
|
+
else:
|
|
677
|
+
log_component_norm = np.zeros(n, dtype=np.float64)
|
|
678
|
+
|
|
679
|
+
inv_cov_n = np.empty_like(cov)
|
|
680
|
+
for i in range(n):
|
|
681
|
+
inv_cov_n[i] = invert_covariance(cov[i], warn_prefix="prior_gaussian_mixture")
|
|
682
|
+
|
|
683
|
+
else:
|
|
684
|
+
inv_cov_n = np.asarray(inv_covs, dtype=np.float64)
|
|
685
|
+
if inv_cov_n.ndim != 3 or inv_cov_n.shape != (n, p, p):
|
|
686
|
+
raise ValueError(f"inv_covs must be (n, p, p) with n={n}, p={p}, got shape {inv_cov_n.shape}")
|
|
687
|
+
|
|
688
|
+
if include:
|
|
689
|
+
log_component_norm = np.empty(n, dtype=np.float64)
|
|
690
|
+
for i in range(n):
|
|
691
|
+
sign, logdet = np.linalg.slogdet(inv_cov_n[i])
|
|
692
|
+
if sign <= 0 or not np.isfinite(logdet):
|
|
693
|
+
raise ValueError(
|
|
694
|
+
"include_component_norm=True requires each inv_cov_n to have positive determinant "
|
|
695
|
+
"(slogdet sign>0 and finite)."
|
|
696
|
+
)
|
|
697
|
+
# -0.5 log|C| = +0.5 log|C^{-1}|
|
|
698
|
+
log_component_norm[i] = 0.5 * logdet
|
|
699
|
+
else:
|
|
700
|
+
log_component_norm = np.zeros(n, dtype=np.float64)
|
|
701
|
+
|
|
702
|
+
if (weights is None) == (log_weights is None):
|
|
703
|
+
raise ValueError("Provide exactly one of `weights` or `log_weights`.")
|
|
704
|
+
|
|
705
|
+
if weights is not None:
|
|
706
|
+
w = np.asarray(weights, dtype=np.float64)
|
|
707
|
+
if w.ndim != 1 or w.size != n:
|
|
708
|
+
raise ValueError(f"weights must be (n,) with n={n}, got shape {w.shape}")
|
|
709
|
+
if np.any(w < 0.0):
|
|
710
|
+
raise ValueError("weights must be non-negative")
|
|
711
|
+
s = float(np.sum(w))
|
|
712
|
+
if s <= 0.0:
|
|
713
|
+
raise ValueError("weights must sum to a positive value")
|
|
714
|
+
w = w / s
|
|
715
|
+
lw = np.full_like(w, -np.inf, dtype=np.float64)
|
|
716
|
+
np.log(w, out=lw, where=(w > 0))
|
|
717
|
+
else:
|
|
718
|
+
lw_in = np.asarray(log_weights, dtype=np.float64)
|
|
719
|
+
if lw_in.ndim != 1 or lw_in.size != n:
|
|
720
|
+
raise ValueError(f"log_weights must be (n,) with n={n}, got shape {lw_in.shape}")
|
|
721
|
+
lw = lw_in - logsumexp_1d(lw_in)
|
|
722
|
+
|
|
723
|
+
if not (np.all(np.isfinite(means)) and np.all(np.isfinite(inv_cov_n))):
|
|
724
|
+
raise ValueError("mixture prior inputs must be finite")
|
|
725
|
+
|
|
726
|
+
if np.any(np.isnan(lw)) or np.any(lw == np.inf):
|
|
727
|
+
raise ValueError("log_weights must not contain nan or +inf")
|
|
728
|
+
|
|
729
|
+
return partial(
|
|
730
|
+
_prior_gaussian_mixture_impl,
|
|
731
|
+
means=means,
|
|
732
|
+
inv_covs=inv_cov_n,
|
|
733
|
+
log_weights=lw,
|
|
734
|
+
log_component_norm=log_component_norm,
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
_PRIOR_REGISTRY: dict[str, Callable[..., Callable[[NDArray[np.floating]], float]]] = {
|
|
738
|
+
"none": prior_none,
|
|
739
|
+
"uniform": prior_uniform,
|
|
740
|
+
"gaussian": prior_gaussian,
|
|
741
|
+
"gaussian_diag": prior_gaussian_diag,
|
|
742
|
+
"log_uniform": prior_log_uniform,
|
|
743
|
+
"jeffreys": prior_jeffreys,
|
|
744
|
+
"half_normal": prior_half_normal,
|
|
745
|
+
"half_cauchy": prior_half_cauchy,
|
|
746
|
+
"log_normal": prior_log_normal,
|
|
747
|
+
"beta": prior_beta,
|
|
748
|
+
"gaussian_mixture": prior_gaussian_mixture,
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def _make_prior_term(spec: dict[str, Any]) -> Callable[[NDArray[np.floating]], float]:
|
|
753
|
+
"""Builds one log-prior contribution from a configuration dictionary.
|
|
754
|
+
|
|
755
|
+
Args:
|
|
756
|
+
spec: Prior specification dictionary with the form
|
|
757
|
+
``{"name": "<prior_name>", "params": {...}, "bounds": optional_bounds}``.
|
|
758
|
+
The ``"name"`` selects a registered prior constructor and ``"params"``
|
|
759
|
+
are forwarded to that constructor. The optional top-level ``"bounds"``
|
|
760
|
+
apply additional hard bounds to the resulting callable. For
|
|
761
|
+
``name="uniform"``, bounds must be provided via either
|
|
762
|
+
``params={"bounds": ...}`` or the top-level ``"bounds"`` key, but not
|
|
763
|
+
both.
|
|
764
|
+
|
|
765
|
+
Returns:
|
|
766
|
+
A callable that evaluates the specified log-prior term.
|
|
767
|
+
|
|
768
|
+
Raises:
|
|
769
|
+
ValueError: If ``spec`` is missing a valid prior name, if the name is not
|
|
770
|
+
registered, or if a uniform prior does not include bounds (or includes
|
|
771
|
+
bounds twice).
|
|
772
|
+
"""
|
|
773
|
+
name = str(spec.get("name", "")).strip().lower()
|
|
774
|
+
if not name:
|
|
775
|
+
raise ValueError("prior spec must include non-empty 'name'")
|
|
776
|
+
if name not in _PRIOR_REGISTRY:
|
|
777
|
+
raise ValueError(f"Unknown prior name '{name}'")
|
|
778
|
+
|
|
779
|
+
params = dict(spec.get("params", {}))
|
|
780
|
+
term_bounds = spec.get("bounds", None)
|
|
781
|
+
|
|
782
|
+
if name == "uniform":
|
|
783
|
+
# allow either params["bounds"] or spec["bounds"] (but not both)
|
|
784
|
+
pb = params.get("bounds", None)
|
|
785
|
+
if pb is not None and term_bounds is not None:
|
|
786
|
+
raise ValueError(
|
|
787
|
+
"uniform prior: provide bounds via either params['bounds'] or top-level 'bounds', not both")
|
|
788
|
+
if pb is None and term_bounds is None:
|
|
789
|
+
raise ValueError("uniform prior requires bounds")
|
|
790
|
+
b = pb if pb is not None else term_bounds
|
|
791
|
+
return prior_uniform(bounds=b)
|
|
792
|
+
|
|
793
|
+
term = _PRIOR_REGISTRY[name](**params)
|
|
794
|
+
return apply_hard_bounds(term, bounds=term_bounds)
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
def build_prior(
|
|
798
|
+
*,
|
|
799
|
+
terms: Sequence[tuple[str, dict[str, Any]] | dict[str, Any]] | None = None,
|
|
800
|
+
bounds: Sequence[tuple[float | np.floating | None, float | np.floating | None]] | None = None,
|
|
801
|
+
) -> Callable[[NDArray[np.floating]], float]:
|
|
802
|
+
"""Build and return a single log-prior callable from a simple specification.
|
|
803
|
+
|
|
804
|
+
This function combines one or more prior components into a single
|
|
805
|
+
``logprior(theta) -> float`` callable by summing their log-densities and
|
|
806
|
+
optionally applying global hard bounds.
|
|
807
|
+
|
|
808
|
+
Each prior component (a “term”) specifies one distribution, such as a
|
|
809
|
+
Gaussian or log-uniform prior, and may also include its own hard bounds.
|
|
810
|
+
|
|
811
|
+
Args:
|
|
812
|
+
terms: Optional list of prior terms. Each term is specified either as
|
|
813
|
+
``("prior_name", params)`` or as a dictionary with keys ``"name"``,
|
|
814
|
+
``"params"``, and optional per-term ``"bounds"``.
|
|
815
|
+
bounds: Optional global hard bounds applied to the combined prior.
|
|
816
|
+
|
|
817
|
+
Returns:
|
|
818
|
+
A callable that evaluates the combined log-prior at a given parameter
|
|
819
|
+
vector.
|
|
820
|
+
|
|
821
|
+
Behavior:
|
|
822
|
+
- If ``terms`` is ``None`` or empty:
|
|
823
|
+
* If ``bounds`` is ``None``, returns an improper flat prior.
|
|
824
|
+
* If ``bounds`` is provided, returns a uniform prior over ``bounds``.
|
|
825
|
+
- Prior terms are summed in log-space.
|
|
826
|
+
- Global ``bounds`` are applied after all terms are combined.
|
|
827
|
+
|
|
828
|
+
Examples:
|
|
829
|
+
```
|
|
830
|
+
build_prior()
|
|
831
|
+
build_prior(bounds=[(0.0, None), (None, None)])
|
|
832
|
+
build_prior(terms=[("gaussian_diag", {"mean": mu, "sigma": sig})])
|
|
833
|
+
```
|
|
834
|
+
"""
|
|
835
|
+
term_list = [] if terms is None else list(terms)
|
|
836
|
+
|
|
837
|
+
# If empty then use either flat or bounded-uniform
|
|
838
|
+
if len(term_list) == 0:
|
|
839
|
+
base = prior_none() if bounds is None else prior_uniform(bounds=bounds)
|
|
840
|
+
return base
|
|
841
|
+
|
|
842
|
+
specs: list[dict[str, Any]] = []
|
|
843
|
+
for t in term_list:
|
|
844
|
+
if isinstance(t, dict):
|
|
845
|
+
specs.append(t)
|
|
846
|
+
continue
|
|
847
|
+
|
|
848
|
+
if not isinstance(t, (tuple, list)) or len(t) != 2:
|
|
849
|
+
raise TypeError(
|
|
850
|
+
"Each term must be either a dict spec or a (name, params) tuple/list of length 2."
|
|
851
|
+
)
|
|
852
|
+
name, params = t
|
|
853
|
+
if not isinstance(params, dict):
|
|
854
|
+
raise TypeError("Term params must be a dict.")
|
|
855
|
+
specs.append({"name": str(name), "params": dict(params)})
|
|
856
|
+
|
|
857
|
+
built_terms = [_make_prior_term(s) for s in specs]
|
|
858
|
+
combined = sum_terms(*built_terms)
|
|
859
|
+
|
|
860
|
+
return apply_hard_bounds(combined, bounds=bounds)
|