derivkit 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. derivkit/__init__.py +22 -0
  2. derivkit/calculus/__init__.py +17 -0
  3. derivkit/calculus/calculus_core.py +152 -0
  4. derivkit/calculus/gradient.py +97 -0
  5. derivkit/calculus/hessian.py +528 -0
  6. derivkit/calculus/hyper_hessian.py +296 -0
  7. derivkit/calculus/jacobian.py +156 -0
  8. derivkit/calculus_kit.py +128 -0
  9. derivkit/derivative_kit.py +315 -0
  10. derivkit/derivatives/__init__.py +6 -0
  11. derivkit/derivatives/adaptive/__init__.py +5 -0
  12. derivkit/derivatives/adaptive/adaptive_fit.py +238 -0
  13. derivkit/derivatives/adaptive/batch_eval.py +179 -0
  14. derivkit/derivatives/adaptive/diagnostics.py +325 -0
  15. derivkit/derivatives/adaptive/grid.py +333 -0
  16. derivkit/derivatives/adaptive/polyfit_utils.py +513 -0
  17. derivkit/derivatives/adaptive/spacing.py +66 -0
  18. derivkit/derivatives/adaptive/transforms.py +245 -0
  19. derivkit/derivatives/autodiff/__init__.py +1 -0
  20. derivkit/derivatives/autodiff/jax_autodiff.py +95 -0
  21. derivkit/derivatives/autodiff/jax_core.py +217 -0
  22. derivkit/derivatives/autodiff/jax_utils.py +146 -0
  23. derivkit/derivatives/finite/__init__.py +5 -0
  24. derivkit/derivatives/finite/batch_eval.py +91 -0
  25. derivkit/derivatives/finite/core.py +84 -0
  26. derivkit/derivatives/finite/extrapolators.py +511 -0
  27. derivkit/derivatives/finite/finite_difference.py +247 -0
  28. derivkit/derivatives/finite/stencil.py +206 -0
  29. derivkit/derivatives/fornberg.py +245 -0
  30. derivkit/derivatives/local_polynomial_derivative/__init__.py +1 -0
  31. derivkit/derivatives/local_polynomial_derivative/diagnostics.py +90 -0
  32. derivkit/derivatives/local_polynomial_derivative/fit.py +199 -0
  33. derivkit/derivatives/local_polynomial_derivative/local_poly_config.py +95 -0
  34. derivkit/derivatives/local_polynomial_derivative/local_polynomial_derivative.py +205 -0
  35. derivkit/derivatives/local_polynomial_derivative/sampling.py +72 -0
  36. derivkit/derivatives/tabulated_model/__init__.py +1 -0
  37. derivkit/derivatives/tabulated_model/one_d.py +247 -0
  38. derivkit/forecast_kit.py +783 -0
  39. derivkit/forecasting/__init__.py +1 -0
  40. derivkit/forecasting/dali.py +78 -0
  41. derivkit/forecasting/expansions.py +486 -0
  42. derivkit/forecasting/fisher.py +298 -0
  43. derivkit/forecasting/fisher_gaussian.py +171 -0
  44. derivkit/forecasting/fisher_xy.py +357 -0
  45. derivkit/forecasting/forecast_core.py +313 -0
  46. derivkit/forecasting/getdist_dali_samples.py +429 -0
  47. derivkit/forecasting/getdist_fisher_samples.py +235 -0
  48. derivkit/forecasting/laplace.py +259 -0
  49. derivkit/forecasting/priors_core.py +860 -0
  50. derivkit/forecasting/sampling_utils.py +388 -0
  51. derivkit/likelihood_kit.py +114 -0
  52. derivkit/likelihoods/__init__.py +1 -0
  53. derivkit/likelihoods/gaussian.py +136 -0
  54. derivkit/likelihoods/poisson.py +176 -0
  55. derivkit/utils/__init__.py +13 -0
  56. derivkit/utils/concurrency.py +213 -0
  57. derivkit/utils/extrapolation.py +254 -0
  58. derivkit/utils/linalg.py +513 -0
  59. derivkit/utils/logger.py +26 -0
  60. derivkit/utils/numerics.py +262 -0
  61. derivkit/utils/sandbox.py +74 -0
  62. derivkit/utils/types.py +15 -0
  63. derivkit/utils/validate.py +811 -0
  64. derivkit-1.0.0.dist-info/METADATA +50 -0
  65. derivkit-1.0.0.dist-info/RECORD +68 -0
  66. derivkit-1.0.0.dist-info/WHEEL +5 -0
  67. derivkit-1.0.0.dist-info/licenses/LICENSE +21 -0
  68. derivkit-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,333 @@
1
+ """Utility functions for building grids of points."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+
7
+ from .spacing import resolve_spacing
8
+ from .transforms import (
9
+ signed_log_forward,
10
+ signed_log_to_physical,
11
+ sqrt_domain_forward,
12
+ sqrt_to_physical,
13
+ )
14
+
15
+ __all__ = ["make_offsets",
16
+ "make_grid",
17
+ "chebyshev_offsets",
18
+ "make_domain_aware_chebyshev_grid",
19
+ "ensure_min_samples_and_maybe_rebuild"
20
+ ]
21
+
22
+
23
+ def make_offsets(n_points: int, base: float, direction: str) -> np.ndarray:
24
+ """Construct a grid of offsets around zero, never including 0.
25
+
26
+ Args:
27
+ n_points: number of points to generate (>=1)
28
+ base: spacing between points (>0)
29
+ direction: 'both', 'pos', or 'neg'. 'both' gives a symmetric grid around 0,
30
+ 'pos' gives points > 0, 'neg' gives points < 0.
31
+
32
+ Returns:
33
+ Array of offsets (length n_points), never including 0.
34
+ """
35
+ if not np.isfinite(base) or base <= 0:
36
+ raise ValueError("Resolved spacing is not a positive finite number.")
37
+ if n_points < 1:
38
+ raise ValueError("n_points must be >= 1.")
39
+ if direction not in {"both", "pos", "neg"}:
40
+ raise ValueError("direction must be 'both', 'pos', or 'neg'.")
41
+
42
+ h = float(base)
43
+
44
+ if direction == "both":
45
+ left = n_points // 2
46
+ right = n_points - left
47
+ k = np.concatenate(
48
+ (
49
+ -np.arange(left, 0, -1, dtype=float),
50
+ np.arange(1, right + 1, dtype=float),
51
+ )
52
+ )
53
+ return h * k
54
+
55
+ if direction == "pos":
56
+ return h * np.arange(1, n_points + 1, dtype=float)
57
+
58
+ # direction == "neg"
59
+ return -h * np.arange(1, n_points + 1, dtype=float)
60
+
61
+
62
+ def make_grid(
63
+ x0: float,
64
+ *,
65
+ n_points: int,
66
+ spacing: str | float | np.ndarray,
67
+ base_abs: float | None,
68
+ need_min: int,
69
+ use_physical_grid: bool,
70
+ ) -> tuple[np.ndarray, np.ndarray, int, float, str]:
71
+ """Unified grid builder.
72
+
73
+ Args:
74
+ x0: expansion point
75
+ n_points: number of points to generate (if not use_physical_grid)
76
+ spacing: 'auto', '<pct>%', numeric > 0, or array of physical
77
+ sample points (if use_physical_grid)
78
+ base_abs: absolute fallback (also used by 'auto'); if None, uses 1
79
+ need_min: minimum number of points required (for validation)
80
+ use_physical_grid: if True, spacing is an array of physical sample points
81
+
82
+ Returns:
83
+ x: array of physical sample points
84
+ t: offsets (x - x0)
85
+ n_pts: number of samples
86
+ spacing_resolved: numeric spacing used (np.nan if physical grid given)
87
+ direction_used: 'custom' if physical grid, else the input direction
88
+ """
89
+ x0 = float(x0)
90
+
91
+ if use_physical_grid:
92
+ x = np.asarray(spacing, dtype=float)
93
+ if x.ndim != 1:
94
+ raise ValueError("When use_physical_grid=True, spacing must be a 1D array of x-samples.")
95
+ if not np.all(np.isfinite(x)):
96
+ raise ValueError("Physical grid contains non-finite values.")
97
+ if x.size < need_min:
98
+ raise ValueError(f"Physical grid must have at least {need_min} points for requested order.")
99
+ t = x - x0
100
+ return x, t, x.size, float("nan"), "physical"
101
+
102
+ # Chebyshev mode
103
+ if n_points < need_min:
104
+ raise ValueError(f"n_points must be >= {need_min} for requested order.")
105
+
106
+ halfwidth = resolve_spacing(spacing, x0, base_abs)
107
+ t = chebyshev_offsets(halfwidth, n_points, include_center=True)
108
+ x = x0 + t
109
+ return x, t, x.size, float(halfwidth), "chebyshev"
110
+
111
+
112
+ def chebyshev_offsets(halfwidth: float, n_points: int, include_center: bool = True) -> np.ndarray:
113
+ """Generate Chebyshev-distributed offsets within [-halfwidth, halfwidth].
114
+
115
+ This function generates ``n_points`` offsets based on the Chebyshev nodes,
116
+ which are distributed to minimize interpolation error. The offsets lie
117
+ within the interval `[-halfwidth, halfwidth]`. Optionally, the center point
118
+ `0.0` can be included in the offsets if it is not already present.
119
+
120
+ Args:
121
+ halfwidth: Half the width of the interval (>0).
122
+ n_points: Number of points to generate (>=1).
123
+ include_center: If True, include 0.0 in the offsets if not already present.
124
+ Default is True.
125
+
126
+ Returns:
127
+ Array of offsets sorted in ascending order.
128
+ """
129
+ k = np.arange(1, n_points + 1)
130
+ u = np.cos((2*k - 1) * np.pi / (2 * n_points)) # (-1,1]
131
+ t = halfwidth * u
132
+ if include_center and 0.0 not in t:
133
+ t = np.append(t, 0.0)
134
+ t = np.sort(t)
135
+ return t
136
+
137
+
138
+ def make_domain_aware_chebyshev_grid(
139
+ x0: float,
140
+ *,
141
+ n_points: int,
142
+ spacing: str | float,
143
+ base_abs: float | None,
144
+ domain: tuple[float | None, float | None] | None,
145
+ max_cheby_points: int = 30,
146
+ ) -> tuple[str, np.ndarray, np.ndarray, float, float | None]:
147
+ """Build a Chebyshev grid around ``x0`` with optional domain-aware transforms.
148
+
149
+ This constructs Chebyshev-distributed sample points around ``x0``. If a
150
+ one-sided domain is supplied, it switches to a transform that respects the
151
+ domain. When the domain is strictly non-negative or non-positive and
152
+ ``x0 == 0``, a square-root coordinate centered at the boundary is used.
153
+ When the domain is single-signed and a symmetric grid around ``x0`` would
154
+ violate it, a signed-log coordinate is used.
155
+
156
+ Args:
157
+ x0: Expansion point about which to place the grid.
158
+ n_points: Number of Chebyshev nodes to generate (center included).
159
+ spacing: Sampling half-width control. Accepts ``"auto"``, a percentage
160
+ string such as ``"2%"``, or a positive float half-width.
161
+ base_abs: Absolute fallback scale used by ``"auto"`` (and transforms)
162
+ near zero.
163
+ domain: Optional bounds ``(lo, hi)``. Use ``None`` for an open end. If the
164
+ domain is strictly non-negative or non-positive, a domain-aware transform
165
+ may be applied as described above.
166
+ max_cheby_points: Safety cap for the default Chebyshev node count.
167
+
168
+ Returns:
169
+ Tuple[str, np.ndarray, np.ndarray, float, float | None]: A 5-tuple
170
+ ``(mode, x, t, spacing_resolved, sign_used)``. ``mode`` is one of
171
+ ``"x"``, ``"signed_log"``, or ``"sqrt"``. ``x`` are physical samples.
172
+ ``t`` are internal offsets in the fit coordinate. ``spacing_resolved``
173
+ is the numeric half-width actually used. ``sign_used`` is ``+1`` or
174
+ ``-1`` for ``"sqrt"`` mode and ``None`` otherwise.
175
+
176
+ Raises:
177
+ ValueError: If ``n_points`` exceeds ``max_cheby_points`` or if ``spacing``
178
+ cannot be resolved to a positive finite half-width.
179
+
180
+ Notes:
181
+ The returned ``x`` always respects a one-sided domain. Chebyshev nodes
182
+ include the center if not already present.
183
+ """
184
+ mode = "x"
185
+ sign_used = None
186
+
187
+ if n_points > max_cheby_points:
188
+ raise ValueError(
189
+ f"Too many points for default Chebyshev grid (n_points={n_points}, max={max_cheby_points}). "
190
+ "Increase `spacing` (wider half-width) or pass an explicit grid."
191
+ )
192
+
193
+ halfwidth = resolve_spacing(spacing, float(x0), base_abs)
194
+ t = chebyshev_offsets(halfwidth, n_points, include_center=True)
195
+ x = x0 + t
196
+ spacing_resolved = float(halfwidth)
197
+
198
+ if domain is None:
199
+ return "x", x, t, spacing_resolved, None
200
+
201
+ lo, hi = domain
202
+ pos_only = (lo is not None and lo >= 0.0) and (hi is None or hi >= 0.0)
203
+ neg_only = (hi is not None and hi <= 0.0) and (lo is None or lo <= 0.0)
204
+ single_sign = pos_only or neg_only
205
+
206
+ if single_sign and x0 == 0.0:
207
+ # sqrt-domain centered at 0 boundary, symmetric in u
208
+ _u0, sgn = sqrt_domain_forward(x0)
209
+ half_u = resolve_spacing(spacing, 0.0, base_abs=1e-3)
210
+ if n_points > max_cheby_points:
211
+ raise ValueError(
212
+ f"Too many points for sqrt-mode Chebyshev (n_points={n_points}, max={max_cheby_points})."
213
+ )
214
+ tu = chebyshev_offsets(half_u, n_points, include_center=True)
215
+ t = tu
216
+ x = sqrt_to_physical(t, sgn)
217
+ spacing_resolved = float(half_u)
218
+ mode = "sqrt"
219
+ sign_used = sgn
220
+
221
+ elif single_sign:
222
+ violates = ((lo is not None) and np.any(x < lo)) or ((hi is not None) and np.any(x > hi))
223
+ if violates and x0 != 0.0:
224
+ q0, sgn = signed_log_forward(x0)
225
+ half_q = resolve_spacing(spacing, q0, base_abs=1e-3)
226
+ if n_points > max_cheby_points:
227
+ raise ValueError(
228
+ f"Too many points for signed-log Chebyshev (n_points={n_points}, max={max_cheby_points})."
229
+ )
230
+ tq = chebyshev_offsets(half_q, n_points, include_center=True)
231
+ t = tq
232
+ x = signed_log_to_physical(q0 + tq, sgn)
233
+ spacing_resolved = float(half_q)
234
+ mode = "signed_log"
235
+
236
+ return mode, x, t, spacing_resolved, sign_used
237
+
238
+
239
+ def ensure_min_samples_and_maybe_rebuild(
240
+ *,
241
+ mode: str,
242
+ x: np.ndarray,
243
+ t: np.ndarray,
244
+ spacing_resolved: float,
245
+ sign_used: float | None,
246
+ x0: float,
247
+ order: int,
248
+ n_points: int,
249
+ spacing: str | float,
250
+ base_abs: float = 1e-3,
251
+ max_cheby_points: int = 30,
252
+ ) -> tuple[str, np.ndarray, np.ndarray, float, float | None]:
253
+ """Guarantee sufficient samples for the requested derivative; rebuild if needed.
254
+
255
+ Computes the minimum number of samples required for a stable polynomial-fit
256
+ derivative estimate and, when the current grid is a default Chebyshev grid
257
+ (indicated by a finite ``spacing_resolved``), rebuilds it with more nodes
258
+ if necessary. For explicit/physical grids (``spacing_resolved`` is not finite),
259
+ a deficiency is treated as an error.
260
+
261
+ The required sample count is:
262
+ - ``min_pts = 2 * deg_req + 1``, where
263
+ - ``deg_req = 2 * order`` for ``mode == "sqrt"`` (due to pullback), else ``deg_req = order``.
264
+
265
+ Args:
266
+ mode: Sampling mode, one of ``"x"``, ``"signed_log"``, or ``"sqrt"``.
267
+ x: Physical sample locations, shape ``(n,)``.
268
+ t: Internal offsets used for fitting, shape ``(n,)``.
269
+ spacing_resolved: Numeric half-width used to generate the Chebyshev grid.
270
+ Finite implies a default/rebuildable grid; non-finite implies an explicit grid.
271
+ sign_used: For ``"sqrt"`` mode, the branch sign ``(+1 or -1)``; otherwise ``None``.
272
+ x0: Expansion point about which derivatives are computed.
273
+ order: Derivative order (``>= 1``).
274
+ n_points: Target number of Chebyshev nodes if a rebuild is performed.
275
+ spacing: Half-width control used when rebuilding (``"auto"``, percentage string, or positive float).
276
+ base_abs: Absolute fallback scale for resolving ``spacing`` near zero. Defaults to ``1e-3``.
277
+ max_cheby_points: Safety cap on Chebyshev node count when rebuilding.
278
+
279
+ Returns:
280
+ Tuple[str, np.ndarray, np.ndarray, float, float | None]:
281
+ A 5-tuple ``(mode, x_out, t_out, spacing_out, sign_out)`` with the (possibly)
282
+ rebuilt grid and associated metadata. The return types mirror the inputs.
283
+
284
+ Raises:
285
+ ValueError: If the grid is explicit (non-finite ``spacing_resolved``) and
286
+ has fewer than the required samples.
287
+ ValueError: If a rebuild would exceed ``max_cheby_points``.
288
+ ValueError: If ``order < 1``.
289
+
290
+ Notes:
291
+ - When rebuilding in ``"signed_log"`` or ``"sqrt"`` mode, ``spacing`` is
292
+ resolved in the transform coordinate (log/sqrt) space and mapped back
293
+ to physical ``x``.
294
+ - The center point is included for Chebyshev grids if not already present.
295
+ """
296
+ n_eff = len(t)
297
+ deg_req = (2 * order) if (mode == "sqrt") else order
298
+ min_pts = 2 * deg_req + 1
299
+ if n_eff >= min_pts:
300
+ return mode, x, t, spacing_resolved, sign_used
301
+
302
+ # explicit grid → spacing_resolved will be NaN (we don't rebuild those)
303
+ if not np.isfinite(spacing_resolved):
304
+ raise ValueError(
305
+ f"Not enough samples for order={order} (mode={mode}). Need ≥{min_pts}, got {n_eff}."
306
+ )
307
+
308
+ target = max(min_pts, n_points)
309
+ if target > max_cheby_points:
310
+ raise ValueError(
311
+ f"Order={order} needs ≥{min_pts} points but cap is {max_cheby_points}. "
312
+ "Increase `spacing`, reduce `order`, or provide an explicit grid."
313
+ )
314
+
315
+ if mode == "x":
316
+ t = chebyshev_offsets(spacing_resolved, target, include_center=True)
317
+ x = x0 + t
318
+ return mode, x, t, spacing_resolved, sign_used
319
+
320
+ if mode == "signed_log":
321
+ q0, sgn = signed_log_forward(x0)
322
+ half_q = resolve_spacing(spacing, q0, base_abs=base_abs)
323
+ tq = chebyshev_offsets(half_q, target, include_center=True)
324
+ x = signed_log_to_physical(q0 + tq, sgn)
325
+ return mode, x, tq, float(half_q), sgn
326
+
327
+ if mode == "sqrt":
328
+ half_u = resolve_spacing(spacing, 0.0, base_abs=base_abs)
329
+ tu = chebyshev_offsets(half_u, target, include_center=True)
330
+ x = sqrt_to_physical(tu, sign_used)
331
+ return mode, x, tu, float(half_u), sign_used
332
+
333
+ return mode, x, t, spacing_resolved, sign_used