binference 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. binference/__init__.py +85 -0
  2. binference/_kernels.py +252 -0
  3. binference/_quadrature.py +202 -0
  4. binference/_simplex.py +54 -0
  5. binference/_utils.py +139 -0
  6. binference/_version.py +1 -0
  7. binference/basis.py +314 -0
  8. binference/bins.py +645 -0
  9. binference/density.py +323 -0
  10. binference/diagnostics.py +400 -0
  11. binference/estimator.py +197 -0
  12. binference/inference.py +368 -0
  13. binference/io.py +309 -0
  14. binference/likelihoods.py +1684 -0
  15. binference/measure.py +246 -0
  16. binference/model.py +833 -0
  17. binference/plotting.py +538 -0
  18. binference/priors/__init__.py +47 -0
  19. binference/priors/base.py +248 -0
  20. binference/priors/dirichlet.py +640 -0
  21. binference/priors/hyper.py +240 -0
  22. binference/priors/logistic.py +676 -0
  23. binference/priors/rates.py +139 -0
  24. binference/response.py +539 -0
  25. binference/results.py +2271 -0
  26. binference/samplers/__init__.py +19 -0
  27. binference/samplers/_curvature.py +67 -0
  28. binference/samplers/_nuts_core.py +284 -0
  29. binference/samplers/base.py +118 -0
  30. binference/samplers/bootstrap.py +136 -0
  31. binference/samplers/gibbs.py +287 -0
  32. binference/samplers/laplace.py +200 -0
  33. binference/samplers/nuts.py +681 -0
  34. binference/samplers/optimize.py +488 -0
  35. binference/samplers/vb.py +118 -0
  36. binference/selection.py +621 -0
  37. binference/simulate.py +720 -0
  38. binference/validation.py +477 -0
  39. binference/warnings.py +48 -0
  40. binference-0.1.0.dist-info/METADATA +130 -0
  41. binference-0.1.0.dist-info/RECORD +43 -0
  42. binference-0.1.0.dist-info/WHEEL +4 -0
  43. binference-0.1.0.dist-info/licenses/LICENSE +21 -0
binference/measure.py ADDED
@@ -0,0 +1,246 @@
1
+ """Measures: the coordinate in which a population is flat within each bin.
2
+
3
+ A measure is a strictly increasing map :math:`u = T(z)` of a bin axis. Within a
4
+ bin the population is taken to be flat in :math:`u`, so its density in
5
+ :math:`z` is proportional to :math:`T'(z)`. Linear bins use :math:`u = z`;
6
+ logarithmic bins use :math:`u = \\log z`.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Callable
12
+ from typing import Any
13
+
14
+ import numpy as np
15
+ from scipy.integrate import cumulative_trapezoid
16
+
17
+ from ._utils import frozen
18
+
19
+ __all__ = ["Measure"]
20
+
21
+
22
+ class Measure:
23
+ """A strictly increasing map ``u = T(z)`` of one bin axis.
24
+
25
+ Build one with :meth:`linear`, :meth:`log`, :meth:`transform` (a function
26
+ and its inverse), :meth:`density` (a positive density ``g(z)``, whose
27
+ cumulative integral is the map) or :meth:`tabulated`. Strings ``"linear"``
28
+ and ``"log"`` are accepted wherever a measure is.
29
+ """
30
+
31
+ __slots__ = ("_forward", "_hash", "_inverse", "_key", "_table", "name")
32
+
33
+ def __init__(
34
+ self,
35
+ name: str,
36
+ forward: Callable[[np.ndarray], np.ndarray],
37
+ inverse: Callable[[np.ndarray], np.ndarray],
38
+ key: tuple,
39
+ table: tuple[np.ndarray, np.ndarray] | None = None,
40
+ ) -> None:
41
+ self.name = name
42
+ self._forward = forward
43
+ self._inverse = inverse
44
+ # identity: ("named", name), ("table", z bytes, u bytes) or ("fn", forward, inverse)
45
+ self._key = key
46
+ self._hash = hash(key)
47
+ self._table = table
48
+
49
+ # ------------------------------------------------------------ constructors
50
+ @classmethod
51
+ def linear(cls) -> Measure:
52
+ """``u = z``: flat within bins in the bin coordinate itself."""
53
+ return _NAMED["linear"]
54
+
55
+ @classmethod
56
+ def log(cls) -> Measure:
57
+ """``u = log z`` (``z > 0``): flat within bins in the logarithm."""
58
+ return _NAMED["log"]
59
+
60
+ @classmethod
61
+ def transform(
62
+ cls,
63
+ forward: Callable[[np.ndarray], np.ndarray],
64
+ inverse: Callable[[np.ndarray], np.ndarray],
65
+ name: str = "transform",
66
+ ) -> Measure:
67
+ """A user map ``u = forward(z)`` with its inverse; both take and return arrays.
68
+
69
+ Parameters
70
+ ----------
71
+ forward, inverse : callable
72
+ The map ``z -> u`` and its inverse, strictly increasing.
73
+ name : str, optional
74
+ Name shown for the measure.
75
+ """
76
+ if not (callable(forward) and callable(inverse)):
77
+ raise TypeError("forward and inverse must be callables.")
78
+ return cls(name, forward, inverse, ("fn", forward, inverse))
79
+
80
+ @classmethod
81
+ def density(
82
+ cls, g: Callable[[np.ndarray], np.ndarray], lo: float, hi: float, n: int = 4097
83
+ ) -> Measure:
84
+ """The map whose derivative is the positive density ``g`` on ``[lo, hi]``.
85
+
86
+ ``T(z) = integral of g from lo to z``, computed by the trapezoidal rule
87
+ on ``n`` points and interpolated linearly, as is its inverse.
88
+
89
+ Parameters
90
+ ----------
91
+ g : callable
92
+ Positive density, evaluated on an array of ``z``.
93
+ lo, hi : float
94
+ Range of ``z`` the map covers.
95
+ n : int, optional
96
+ Points of the trapezoidal rule that tabulates the map.
97
+ """
98
+ z = np.linspace(float(lo), float(hi), int(n))
99
+ dens = np.asarray(g(z), dtype=float)
100
+ if dens.shape != z.shape or not np.all(np.isfinite(dens)) or np.any(dens <= 0):
101
+ raise ValueError("g must return finite, strictly positive values on [lo, hi].")
102
+ return cls.tabulated(z, cumulative_trapezoid(dens, z, initial=0.0), name="density")
103
+
104
+ @classmethod
105
+ def tabulated(cls, z: Any, u: Any, name: str = "tabulated") -> Measure:
106
+ """A piecewise-linear map through the points ``(z, u)``, both strictly increasing.
107
+
108
+ Parameters
109
+ ----------
110
+ z, u : array_like
111
+ Points of the map, at least two.
112
+ name : str, optional
113
+ Name shown for the measure.
114
+ """
115
+ z = np.array(z, dtype=float)
116
+ u = np.array(u, dtype=float)
117
+ if z.ndim != 1 or z.shape != u.shape or z.size < 2:
118
+ raise ValueError("z and u must be 1-D arrays of the same length (at least 2).")
119
+ if not (np.all(np.diff(z) > 0) and np.all(np.diff(u) > 0)):
120
+ raise ValueError("z and u must both be strictly increasing.")
121
+ z, u = frozen(z), frozen(u)
122
+ return cls(
123
+ name,
124
+ lambda x: np.interp(x, z, u),
125
+ lambda y: np.interp(y, u, z),
126
+ ("table", z.tobytes(), u.tobytes()),
127
+ table=(z, u),
128
+ )
129
+
130
+ @classmethod
131
+ def coerce(cls, measure: Measure | str) -> Measure:
132
+ """A :class:`Measure` from a measure or one of the names ``"linear"`` and ``"log"``.
133
+
134
+ Parameters
135
+ ----------
136
+ measure : Measure or {"linear", "log"}
137
+ A measure or its name.
138
+ """
139
+ if isinstance(measure, Measure):
140
+ return measure
141
+ if isinstance(measure, str) and measure in _NAMED:
142
+ return _NAMED[measure]
143
+ raise ValueError(f"measure must be a Measure, 'linear' or 'log'; got {measure!r}.")
144
+
145
+ # ----------------------------------------------------------------- methods
146
+ @property
147
+ def is_named(self) -> bool:
148
+ """Whether this is one of the built-in measures ``"linear"`` and ``"log"``."""
149
+ return self._key[0] == "named"
150
+
151
+ def forward(self, z: Any) -> np.ndarray:
152
+ """``u = T(z)``.
153
+
154
+ Parameters
155
+ ----------
156
+ z : array_like
157
+ Values of the bin coordinate.
158
+ """
159
+ return np.asarray(self._forward(np.asarray(z, dtype=float)), dtype=float)
160
+
161
+ def inverse(self, u: Any) -> np.ndarray:
162
+ """``z = T^{-1}(u)``.
163
+
164
+ Parameters
165
+ ----------
166
+ u : array_like
167
+ Values of the measure coordinate.
168
+ """
169
+ return np.asarray(self._inverse(np.asarray(u, dtype=float)), dtype=float)
170
+
171
+ def log_jacobian(self, z: Any) -> np.ndarray:
172
+ """``log dT/dz``: the log of the within-bin population density in ``z``, up to a constant.
173
+
174
+ Exact for linear, log and tabulated measures; a central difference for
175
+ :meth:`transform`.
176
+
177
+ Parameters
178
+ ----------
179
+ z : array_like
180
+ Values of the bin coordinate.
181
+ """
182
+ z = np.asarray(z, dtype=float)
183
+ if self is _NAMED["linear"]:
184
+ return np.zeros_like(z)
185
+ if self is _NAMED["log"]:
186
+ return -np.log(z)
187
+ if self._table is not None:
188
+ zt, ut = self._table
189
+ j = np.clip(np.searchsorted(zt, z, side="right") - 1, 0, zt.size - 2)
190
+ return np.log((ut[j + 1] - ut[j]) / (zt[j + 1] - zt[j]))
191
+ h = 1e-6 * np.where(z != 0, np.abs(z), 1.0) # relative, so small z stay in the domain
192
+ return np.log((self.forward(z + h) - self.forward(z - h)) / (2 * h))
193
+
194
+ def _covers(self, lo: float, hi: float) -> bool:
195
+ # whether [lo, hi] lies inside the measure's domain
196
+ if self is _NAMED["log"]:
197
+ return lo > 0
198
+ if self._table is not None:
199
+ return self._table[0][0] <= lo and hi <= self._table[0][-1]
200
+ return True
201
+
202
+ def _tabulate(self, edges: np.ndarray, n: int = 4097) -> tuple[np.ndarray, np.ndarray]:
203
+ # points (z, u) that reproduce the map on [edges[0], edges[-1]]
204
+ if self._table is not None:
205
+ return self._table
206
+ z = np.unique(np.concatenate([np.linspace(edges[0], edges[-1], n), edges]))
207
+ return z, self.forward(z)
208
+
209
+ def _check(self, edges: np.ndarray, axis: int) -> np.ndarray:
210
+ # the edges mapped to u; raises unless they stay finite and increasing
211
+ if not self._covers(edges[0], edges[-1]):
212
+ raise ValueError(f"Edges along axis {axis} lie outside the domain of {self!r}.")
213
+ u = self.forward(edges)
214
+ if not (np.all(np.isfinite(u)) and np.all(np.diff(u) > 0)):
215
+ raise ValueError(f"The measure must map the edges of axis {axis} to increasing u.")
216
+ return u
217
+
218
+ def __eq__(self, other: object) -> bool:
219
+ if not isinstance(other, Measure):
220
+ return NotImplemented
221
+ return self._key == other._key
222
+
223
+ def __hash__(self) -> int:
224
+ return self._hash
225
+
226
+ def __reduce__(self):
227
+ kind = self._key[0]
228
+ if kind == "named":
229
+ return (Measure.coerce, (self.name,))
230
+ if kind == "table":
231
+ z, u = (np.array(t) for t in self._table)
232
+ return (Measure.tabulated, (z, u, self.name))
233
+ return (Measure.transform, (self._forward, self._inverse, self.name))
234
+
235
+ def __repr__(self) -> str:
236
+ return f"Measure.{self.name}()" if self.is_named else f"Measure({self.name!r})"
237
+
238
+
239
+ def _identity(x: np.ndarray) -> np.ndarray:
240
+ return x
241
+
242
+
243
+ _NAMED = {
244
+ "linear": Measure("linear", _identity, _identity, ("named", "linear")),
245
+ "log": Measure("log", np.log, np.exp, ("named", "log")),
246
+ }