chebax 0.1.0.dev0__tar.gz → 0.2.0__tar.gz

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 (72) hide show
  1. chebax-0.2.0/PKG-INFO +106 -0
  2. chebax-0.2.0/README.md +82 -0
  3. chebax-0.2.0/pyproject.toml +34 -0
  4. chebax-0.2.0/src/chebax/__init__.py +38 -0
  5. chebax-0.2.0/src/chebax/_src/generate.py +177 -0
  6. chebax-0.2.0/src/chebax/_src/pytree.py +59 -0
  7. chebax-0.2.0/src/chebax/_src/recipes/_common.py +95 -0
  8. chebax-0.2.0/src/chebax/_src/recipes/_gen_common.py +121 -0
  9. chebax-0.2.0/src/chebax/_src/recipes/besseli.py +209 -0
  10. chebax-0.2.0/src/chebax/_src/recipes/besseli_gen.py +84 -0
  11. chebax-0.2.0/src/chebax/_src/recipes/besseli_table.py +138 -0
  12. chebax-0.2.0/src/chebax/_src/recipes/besselj.py +176 -0
  13. chebax-0.2.0/src/chebax/_src/recipes/besselj_gen.py +146 -0
  14. chebax-0.2.0/src/chebax/_src/recipes/besselj_table.py +39 -0
  15. chebax-0.2.0/src/chebax/_src/recipes/besselj_table_ext.py +87 -0
  16. chebax-0.2.0/src/chebax/_src/recipes/besselk.py +184 -0
  17. chebax-0.2.0/src/chebax/_src/recipes/besselk_gen.py +107 -0
  18. chebax-0.2.0/src/chebax/_src/recipes/besselk_table.py +208 -0
  19. chebax-0.2.0/src/chebax/_src/recipes/bessely.py +185 -0
  20. chebax-0.2.0/src/chebax/_src/recipes/bessely_gen.py +103 -0
  21. chebax-0.2.0/src/chebax/_src/recipes/bessely_table.py +284 -0
  22. chebax-0.2.0/src/chebax/_src/recipes/betainc.py +128 -0
  23. chebax-0.2.0/src/chebax/_src/recipes/betainc_gen.py +78 -0
  24. chebax-0.2.0/src/chebax/_src/recipes/betainc_table.py +1791 -0
  25. chebax-0.2.0/src/chebax/_src/recipes/erf_family.py +63 -0
  26. chebax-0.2.0/src/chebax/_src/recipes/erf_gen.py +69 -0
  27. chebax-0.2.0/src/chebax/_src/recipes/erf_table.py +51 -0
  28. chebax-0.2.0/src/chebax/_src/recipes/lambertw.py +106 -0
  29. chebax-0.2.0/src/chebax/_src/recipes/quantiles.py +339 -0
  30. chebax-0.2.0/src/chebax/_src/recipes/spherical.py +96 -0
  31. chebax-0.2.0/src/chebax/_src/recipes/vonmises.py +113 -0
  32. chebax-0.2.0/src/chebax/_src/recipes/vonmises_gen.py +68 -0
  33. chebax-0.2.0/src/chebax/_src/recipes/vonmises_table.py +127 -0
  34. {chebax-0.1.0.dev0 → chebax-0.2.0}/src/chebax/_src/series.py +37 -9
  35. chebax-0.2.0/src/chebax/bake/__init__.py +178 -0
  36. chebax-0.2.0/src/chebax/bake/_jaxpr_emit.py +264 -0
  37. chebax-0.2.0/src/chebax/pytensor.py +195 -0
  38. chebax-0.2.0/src/chebax.egg-info/PKG-INFO +106 -0
  39. chebax-0.2.0/src/chebax.egg-info/SOURCES.txt +59 -0
  40. chebax-0.2.0/src/chebax.egg-info/requires.txt +16 -0
  41. chebax-0.2.0/tests/test_bake.py +171 -0
  42. chebax-0.2.0/tests/test_besseli.py +130 -0
  43. {chebax-0.1.0.dev0 → chebax-0.2.0}/tests/test_besselj.py +38 -7
  44. chebax-0.2.0/tests/test_besselj_m3.py +127 -0
  45. chebax-0.2.0/tests/test_besselk.py +237 -0
  46. chebax-0.2.0/tests/test_bessely.py +87 -0
  47. chebax-0.2.0/tests/test_betainc.py +103 -0
  48. chebax-0.2.0/tests/test_erf_family.py +80 -0
  49. chebax-0.2.0/tests/test_lambertw.py +80 -0
  50. {chebax-0.1.0.dev0 → chebax-0.2.0}/tests/test_m1.py +61 -2
  51. chebax-0.2.0/tests/test_pytensor_plugin.py +142 -0
  52. chebax-0.2.0/tests/test_quantiles.py +274 -0
  53. chebax-0.2.0/tests/test_spherical.py +59 -0
  54. chebax-0.2.0/tests/test_vonmises.py +115 -0
  55. chebax-0.1.0.dev0/PKG-INFO +0 -35
  56. chebax-0.1.0.dev0/README.md +0 -17
  57. chebax-0.1.0.dev0/pyproject.toml +0 -26
  58. chebax-0.1.0.dev0/src/chebax/__init__.py +0 -11
  59. chebax-0.1.0.dev0/src/chebax/_src/generate.py +0 -106
  60. chebax-0.1.0.dev0/src/chebax/_src/recipes/besselj.py +0 -138
  61. chebax-0.1.0.dev0/src/chebax/_src/recipes/besselj_gen.py +0 -76
  62. chebax-0.1.0.dev0/src/chebax/_src/recipes/besselj_table.py +0 -37
  63. chebax-0.1.0.dev0/src/chebax.egg-info/PKG-INFO +0 -35
  64. chebax-0.1.0.dev0/src/chebax.egg-info/SOURCES.txt +0 -19
  65. chebax-0.1.0.dev0/src/chebax.egg-info/requires.txt +0 -8
  66. {chebax-0.1.0.dev0 → chebax-0.2.0}/LICENSE +0 -0
  67. {chebax-0.1.0.dev0 → chebax-0.2.0}/setup.cfg +0 -0
  68. {chebax-0.1.0.dev0 → chebax-0.2.0}/src/chebax/_src/__init__.py +0 -0
  69. {chebax-0.1.0.dev0 → chebax-0.2.0}/src/chebax/_src/algorithms.py +0 -0
  70. {chebax-0.1.0.dev0 → chebax-0.2.0}/src/chebax/_src/recipes/__init__.py +0 -0
  71. {chebax-0.1.0.dev0 → chebax-0.2.0}/src/chebax.egg-info/dependency_links.txt +0 -0
  72. {chebax-0.1.0.dev0 → chebax-0.2.0}/src/chebax.egg-info/top_level.txt +0 -0
chebax-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: chebax
3
+ Version: 0.2.0
4
+ Summary: Differentiable Chebyshev approximants for special functions, aimed at JAX and GPUs
5
+ Author-email: Andres Hernandez <andres.hernandez.deml@gmail.com>
6
+ License-Expression: BSD-3-Clause
7
+ Project-URL: Repository, https://github.com/andreshernandez-spec/chebax
8
+ Requires-Python: >=3.11
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: jax>=0.5.3
12
+ Requires-Dist: numpy>=1.26
13
+ Provides-Extra: gen
14
+ Requires-Dist: mpmath<1.5,>=1.4.1; extra == "gen"
15
+ Provides-Extra: test
16
+ Requires-Dist: pytest; extra == "test"
17
+ Requires-Dist: scipy; extra == "test"
18
+ Provides-Extra: examples
19
+ Requires-Dist: matplotlib; extra == "examples"
20
+ Requires-Dist: numpyro; extra == "examples"
21
+ Provides-Extra: pytensor
22
+ Requires-Dist: pytensor>=3.2; extra == "pytensor"
23
+ Dynamic: license-file
24
+
25
+ # chebax
26
+
27
+ Differentiable special functions for JAX and GPUs, built from Chebyshev
28
+ approximants. A build-time generator (mpmath precision) produces fixed-degree
29
+ branchless polynomial kernels with exact derivative series — including gradients
30
+ with respect to function *parameters* (Bessel order, Beta shape, von Mises
31
+ concentration), which mainstream ML libraries lack. No mpmath at use time; the
32
+ runtime imports only jax and numpy.
33
+
34
+ ```sh
35
+ pip install chebax
36
+ ```
37
+
38
+ ```python
39
+ import jax
40
+ jax.config.update("jax_enable_x64", True) # before building instances: tables
41
+ # materialize in the active precision
42
+ import jax.numpy as jnp
43
+ import chebax
44
+
45
+ jv = chebax.besselj(2.5) # any real order in [0, 10]; cached
46
+ x = jnp.linspace(0.1, 30.0, 200)
47
+ jv(x), jax.vmap(jax.grad(jv))(x) # values and dJ/dx, jit/vmap-safe
48
+
49
+ chebax.besselk_fn(1.7, x) # order as a traced scalar:
50
+ jax.grad(chebax.besselk_fn)(1.7, 5.0) # jax.grad w.r.t. the ORDER
51
+ chebax.betainc_fn(2.0, 3.0, 0.4) # dI/da, dI/db via jax.grad (jax#38610)
52
+ chebax.betaincinv(2.0, 3.0, 0.05) # differentiable Beta quantile (jax#2399)
53
+ ```
54
+
55
+ ## What's in
56
+
57
+ | family | factory | traced params | param derivative | notes |
58
+ |---|---|---|---|---|
59
+ | `besselj` | `besselj(v)` | — | `besselj_dnu(v)` | x ≥ 0, validated to 1e4 |
60
+ | `bessely` | `bessely(v)` | — (structural) | `bessely_dnu(v)` | x ≥ 1e-6 |
61
+ | `besseli` | `besseli(v, scaled=)` | `besseli_fn` | `besseli_dnu(v, scaled=)` | `scaled` = scipy's ive |
62
+ | `besselk` | `besselk(v)` | `besselk_fn`, `log_besselk_fn` | `besselk_dnu(v)` | x ≥ 1e-6; Matérn demo; the log form has no underflow ceiling |
63
+ | `betainc` | `betainc(a, b)` | `betainc_fn` | via `grad` of `_fn` | (a, b) ∈ [0.1, 10]² |
64
+ | spherical | `spherical_jn/yn(n)` | — | — | n ∈ [0, 9], via half-integer tables |
65
+ | quantiles | — | `betaincinv`, `gammaincinv`, `stdtr`, `stdtrit` | via `grad` (IFT) | jax#2399/#5350/#20358 |
66
+ | von Mises | — | `vonmises_cdf/icdf` | via `grad` | κ ∈ [0, 50] |
67
+ | erf family | — | `dawsn`, `erfcx` | — (no params) | recent jax ships both; kept for the C++ bake path |
68
+ | Lambert W | — | `lambertw(x, k)` | — | both real branches, jax#13680 |
69
+
70
+ Plus the generic core (`fit`, `ChebSeries`, `PiecewiseCheb`) and bake emitters
71
+ (`chebax.bake.jax_module`, `chebax.bake.xsf_header`: self-contained pure-jax
72
+ modules and C++17 headers from an instance).
73
+
74
+ For pymc users: `import chebax.pytensor` (installs with
75
+ `pip install chebax[pytensor]`) registers JAX-backend lowerings for the
76
+ pytensor ops that otherwise need tfp-nightly under
77
+ `pm.sample(nuts_sampler="numpyro"|"blackjax")` — betaincinv, gammaincinv,
78
+ gammainccinv, erfcx, erfcinv, ive, kve — and adds betainc gradients in its
79
+ shape parameters, so censored or truncated StudentT/Beta likelihoods with a
80
+ latent scalar shape sample instead of raising. Shape parameters must be
81
+ scalar (batched ones fall back to tfp or fail loudly); out-of-domain values
82
+ return nan, never silently wrong numbers.
83
+
84
+ `notebooks/` holds themed, executed walkthroughs: why Chebyshev nodes work,
85
+ the Bessel family with a learnable Matérn kernel, differentiable quantiles,
86
+ truncated and circular distributions in numpyro, Gaussian tails / Lambert W /
87
+ binomial reliability, copulas, and baking. The numpyro and plotting
88
+ dependencies install with `pip install chebax[examples]`. `examples/` holds
89
+ plain scripts: a few of the notebook workflows in copy-paste form, plus
90
+ integration examples with no notebook counterpart (a GIG distribution for
91
+ efax, learned Matérn smoothness, truncated sampling).
92
+
93
+ Parameters come first, evaluation point last (opposite of scipy). Orders/shapes
94
+ are uniform per call; per-element parameter arrays are out of scope by design.
95
+ Accuracy is validated against mpmath at 40 dps — measured worst cases and the
96
+ per-family error metrics are in each test file's docstring; tables regenerate
97
+ bit-for-bit from checked-in generators.
98
+
99
+ ## Docs
100
+
101
+ `docs/adding-a-recipe.md` is the workflow for new functions;
102
+ `docs/increments.md` the design log of every recipe (what was measured, which
103
+ traps were found); `PROJECT.md` the plan and evidence; `CLAUDE.md` how to work
104
+ in the repo. Grown out of a private research project on GPU Bessel functions;
105
+ its two load-bearing measurements are reproduced here in `experiments/`.
106
+ BSD-3-Clause.
chebax-0.2.0/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # chebax
2
+
3
+ Differentiable special functions for JAX and GPUs, built from Chebyshev
4
+ approximants. A build-time generator (mpmath precision) produces fixed-degree
5
+ branchless polynomial kernels with exact derivative series — including gradients
6
+ with respect to function *parameters* (Bessel order, Beta shape, von Mises
7
+ concentration), which mainstream ML libraries lack. No mpmath at use time; the
8
+ runtime imports only jax and numpy.
9
+
10
+ ```sh
11
+ pip install chebax
12
+ ```
13
+
14
+ ```python
15
+ import jax
16
+ jax.config.update("jax_enable_x64", True) # before building instances: tables
17
+ # materialize in the active precision
18
+ import jax.numpy as jnp
19
+ import chebax
20
+
21
+ jv = chebax.besselj(2.5) # any real order in [0, 10]; cached
22
+ x = jnp.linspace(0.1, 30.0, 200)
23
+ jv(x), jax.vmap(jax.grad(jv))(x) # values and dJ/dx, jit/vmap-safe
24
+
25
+ chebax.besselk_fn(1.7, x) # order as a traced scalar:
26
+ jax.grad(chebax.besselk_fn)(1.7, 5.0) # jax.grad w.r.t. the ORDER
27
+ chebax.betainc_fn(2.0, 3.0, 0.4) # dI/da, dI/db via jax.grad (jax#38610)
28
+ chebax.betaincinv(2.0, 3.0, 0.05) # differentiable Beta quantile (jax#2399)
29
+ ```
30
+
31
+ ## What's in
32
+
33
+ | family | factory | traced params | param derivative | notes |
34
+ |---|---|---|---|---|
35
+ | `besselj` | `besselj(v)` | — | `besselj_dnu(v)` | x ≥ 0, validated to 1e4 |
36
+ | `bessely` | `bessely(v)` | — (structural) | `bessely_dnu(v)` | x ≥ 1e-6 |
37
+ | `besseli` | `besseli(v, scaled=)` | `besseli_fn` | `besseli_dnu(v, scaled=)` | `scaled` = scipy's ive |
38
+ | `besselk` | `besselk(v)` | `besselk_fn`, `log_besselk_fn` | `besselk_dnu(v)` | x ≥ 1e-6; Matérn demo; the log form has no underflow ceiling |
39
+ | `betainc` | `betainc(a, b)` | `betainc_fn` | via `grad` of `_fn` | (a, b) ∈ [0.1, 10]² |
40
+ | spherical | `spherical_jn/yn(n)` | — | — | n ∈ [0, 9], via half-integer tables |
41
+ | quantiles | — | `betaincinv`, `gammaincinv`, `stdtr`, `stdtrit` | via `grad` (IFT) | jax#2399/#5350/#20358 |
42
+ | von Mises | — | `vonmises_cdf/icdf` | via `grad` | κ ∈ [0, 50] |
43
+ | erf family | — | `dawsn`, `erfcx` | — (no params) | recent jax ships both; kept for the C++ bake path |
44
+ | Lambert W | — | `lambertw(x, k)` | — | both real branches, jax#13680 |
45
+
46
+ Plus the generic core (`fit`, `ChebSeries`, `PiecewiseCheb`) and bake emitters
47
+ (`chebax.bake.jax_module`, `chebax.bake.xsf_header`: self-contained pure-jax
48
+ modules and C++17 headers from an instance).
49
+
50
+ For pymc users: `import chebax.pytensor` (installs with
51
+ `pip install chebax[pytensor]`) registers JAX-backend lowerings for the
52
+ pytensor ops that otherwise need tfp-nightly under
53
+ `pm.sample(nuts_sampler="numpyro"|"blackjax")` — betaincinv, gammaincinv,
54
+ gammainccinv, erfcx, erfcinv, ive, kve — and adds betainc gradients in its
55
+ shape parameters, so censored or truncated StudentT/Beta likelihoods with a
56
+ latent scalar shape sample instead of raising. Shape parameters must be
57
+ scalar (batched ones fall back to tfp or fail loudly); out-of-domain values
58
+ return nan, never silently wrong numbers.
59
+
60
+ `notebooks/` holds themed, executed walkthroughs: why Chebyshev nodes work,
61
+ the Bessel family with a learnable Matérn kernel, differentiable quantiles,
62
+ truncated and circular distributions in numpyro, Gaussian tails / Lambert W /
63
+ binomial reliability, copulas, and baking. The numpyro and plotting
64
+ dependencies install with `pip install chebax[examples]`. `examples/` holds
65
+ plain scripts: a few of the notebook workflows in copy-paste form, plus
66
+ integration examples with no notebook counterpart (a GIG distribution for
67
+ efax, learned Matérn smoothness, truncated sampling).
68
+
69
+ Parameters come first, evaluation point last (opposite of scipy). Orders/shapes
70
+ are uniform per call; per-element parameter arrays are out of scope by design.
71
+ Accuracy is validated against mpmath at 40 dps — measured worst cases and the
72
+ per-family error metrics are in each test file's docstring; tables regenerate
73
+ bit-for-bit from checked-in generators.
74
+
75
+ ## Docs
76
+
77
+ `docs/adding-a-recipe.md` is the workflow for new functions;
78
+ `docs/increments.md` the design log of every recipe (what was measured, which
79
+ traps were found); `PROJECT.md` the plan and evidence; `CLAUDE.md` how to work
80
+ in the repo. Grown out of a private research project on GPU Bessel functions;
81
+ its two load-bearing measurements are reproduced here in `experiments/`.
82
+ BSD-3-Clause.
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "chebax"
7
+ version = "0.2.0"
8
+ description = "Differentiable Chebyshev approximants for special functions, aimed at JAX and GPUs"
9
+ readme = "README.md"
10
+ license = "BSD-3-Clause"
11
+ authors = [{ name = "Andres Hernandez", email = "andres.hernandez.deml@gmail.com" }]
12
+ requires-python = ">=3.11"
13
+ # jax floor = the oldest version the full suite passes on (CI pins a job to it)
14
+ dependencies = ["jax>=0.5.3", "numpy>=1.26"]
15
+
16
+ [project.urls]
17
+ Repository = "https://github.com/andreshernandez-spec/chebax"
18
+
19
+ [project.optional-dependencies]
20
+ # tables record the mpmath version that built them; regeneration is verified
21
+ # byte for byte, so the build dependency is pinned to the recorded minor
22
+ gen = ["mpmath>=1.4.1,<1.5"]
23
+ test = ["pytest", "scipy"]
24
+ examples = ["matplotlib", "numpyro"]
25
+ pytensor = ["pytensor>=3.2"]
26
+
27
+ [tool.setuptools.packages.find]
28
+ where = ["src"]
29
+
30
+ [tool.pytest.ini_options]
31
+ testpaths = ["tests"]
32
+ markers = [
33
+ "slow: table regeneration and other multi-minute tests",
34
+ ]
@@ -0,0 +1,38 @@
1
+ """chebax: differentiable Chebyshev approximants for special functions.
2
+
3
+ Full float64 accuracy needs jax's x64 mode (jax.config.update("jax_enable_x64", True)).
4
+ """
5
+
6
+ from chebax._src.generate import fit
7
+ from chebax._src.recipes.besseli import besseli, besseli_dnu, besseli_fn
8
+ from chebax._src.recipes.erf_family import dawsn, erfcx
9
+ from chebax._src.recipes.lambertw import lambertw
10
+ from chebax._src.recipes.spherical import spherical_jn, spherical_yn
11
+ from chebax._src.recipes.besselj import besselj, besselj_dnu
12
+ from chebax._src.recipes.besselk import (besselk, besselk_dnu, besselk_fn,
13
+ log_besselk_fn)
14
+ from chebax._src.recipes.bessely import bessely, bessely_dnu
15
+ from chebax._src.recipes.betainc import betainc, betainc_fn
16
+ from chebax._src.recipes.quantiles import betaincinv, gammaincinv, stdtr, stdtrit
17
+ from chebax._src.recipes.vonmises import vonmises_cdf, vonmises_icdf
18
+ from chebax._src.series import ChebSeries, PiecewiseCheb
19
+
20
+ # single-sourced from pyproject.toml via package metadata
21
+ try:
22
+ from importlib.metadata import version as _pkg_version
23
+ __version__ = _pkg_version("chebax")
24
+ except Exception: # not installed (e.g. running from a source checkout)
25
+ __version__ = "0+unknown"
26
+ __all__ = ["ChebSeries", "PiecewiseCheb", "besseli", "besseli_dnu", "besseli_fn",
27
+ "besselj", "besselj_dnu", "besselk", "besselk_dnu", "besselk_fn",
28
+ "log_besselk_fn",
29
+ "bessely", "bessely_dnu", "betainc", "betainc_fn", "betaincinv",
30
+ "dawsn", "erfcx", "fit", "gammaincinv", "lambertw", "spherical_jn",
31
+ "spherical_yn", "stdtr", "stdtrit", "vonmises_cdf", "vonmises_icdf",
32
+ "__version__"]
33
+
34
+ # submodule re-export so `import chebax; chebax.bake` works as documented
35
+ # (bake reads __version__, so this import must stay below its definition)
36
+ from chebax import bake # noqa: E402,F401
37
+
38
+ __all__.append("bake")
@@ -0,0 +1,177 @@
1
+ """Build-time fitting: sample at Chebyshev points, DCT to coefficients.
2
+
3
+ The float64 path uses numpy. Passing dps switches sampling and the DCT to
4
+ mpmath at that precision; mpmath is imported inside that branch only (the
5
+ runtime-imports rule in CLAUDE.md).
6
+
7
+ Adaptive fitting doubles the node count until the coefficient tail sits at
8
+ the fit's own noise floor, then truncates there. A converged-looking tail
9
+ on one grid is never trusted by itself: on n first-kind points T_{2n-k}
10
+ samples exactly as -T_k, so an aliased fit can look converged (T_34 on 17
11
+ nodes reproduces -T_0 to the last bit). Every candidate is therefore
12
+ validated against fresh samples at off-grid points before it is returned.
13
+
14
+ The noise floor is estimated from the trailing quarter of the candidate's
15
+ own coefficients rather than assumed: float64 sampling noise scales with
16
+ the function's derivative (measured: ~1e-16 relative for exp, ~2.5e-15 for
17
+ sin(50x)), so a fixed tolerance either fails to converge on oscillatory
18
+ functions or keeps hundreds of noise coefficients.
19
+ """
20
+
21
+ import numpy as np
22
+
23
+ from chebax._src import algorithms
24
+ from chebax._src.series import ChebSeries, PiecewiseCheb
25
+
26
+ # ~2*eps: the practical floor of the float64 build path. Use dps= to go below.
27
+ _DEFAULT_TOL = 5e-16
28
+
29
+
30
+ def fit(f, domain=(-1.0, 1.0), *, deg=None, tol=None, breaks=None, dps=None, max_deg=256):
31
+ """Chebyshev interpolant of f, as a ChebSeries (or PiecewiseCheb if breaks given).
32
+
33
+ f: callable on the domain. Vectorized over numpy arrays if possible;
34
+ with dps set it must accept mpmath floats instead.
35
+ domain: (a, b). Ignored when breaks is given.
36
+ deg: fixed degree; otherwise the degree adapts until tol is met.
37
+ tol: target for the relative coefficient tail (default 5e-16). The
38
+ effective chop level is max(tol, the fit's measured noise floor);
39
+ float64 sampling noise can sit above tol for functions with
40
+ large derivatives. Build with dps to control the floor.
41
+ breaks: strictly increasing knots, endpoints included; one series per
42
+ segment, each fit independently, padded to a common degree.
43
+ dps: mpmath working precision for sampling and the DCT. Derivatives of
44
+ a float64-built fit sit at a ~deg^2 * eps floor (differentiation
45
+ amplifies sample noise); build with dps when that matters.
46
+ max_deg: hard cap on the returned degree. Reaching it without a validated
47
+ converged fit raises: f is not smooth enough on the interval
48
+ (split the domain) or genuinely needs a higher degree.
49
+ """
50
+ if tol is None:
51
+ tol = _DEFAULT_TOL
52
+ if breaks is not None:
53
+ br = [float(t) for t in breaks]
54
+ cs = [_fit_interval(f, br[i], br[i + 1], deg, tol, max_deg, dps)
55
+ for i in range(len(br) - 1)]
56
+ n = max(c.size for c in cs)
57
+ coef = np.zeros((len(cs), n))
58
+ for i, c in enumerate(cs):
59
+ coef[i, : c.size] = c
60
+ return PiecewiseCheb(coef, br)
61
+ a, b = float(domain[0]), float(domain[1])
62
+ return ChebSeries(_fit_interval(f, a, b, deg, tol, max_deg, dps), (a, b))
63
+
64
+
65
+ def _fit_interval(f, a, b, deg, tol, max_deg, dps):
66
+ dct, sample_unit = (_dct_mp(f, a, b, dps) if dps is not None
67
+ else _dct_np(f, a, b))
68
+ if deg is not None:
69
+ return dct(deg + 1)
70
+ return _adaptive(dct, sample_unit, tol, max_deg, (a, b))
71
+
72
+
73
+ def _chop(c, tol):
74
+ """Truncate at the noise level; returns (candidate, absolute level).
75
+
76
+ The level is max(tol, noise floor), where the noise floor is read off
77
+ the last eighth of the coefficients: for a still-decaying tail those
78
+ sit below tol and the requested tol governs (measured: exp's last
79
+ coefficients decay through 1e-16); for a converged fit they are the
80
+ sampling-noise plateau (sin(50x): ~5e-16 relative at n=129, edge-
81
+ singular sampling can push higher) and doubling the largest of them
82
+ clears the plateau's own scatter."""
83
+ scale = np.max(np.abs(c))
84
+ noise = 2.0 * float(np.max(np.abs(c[-max(2, c.size // 16):])))
85
+ level = max(tol * scale, noise)
86
+ keep = np.nonzero(np.abs(c) > level)[0]
87
+ return (c[: keep[-1] + 1] if keep.size else c[:1]), level
88
+
89
+
90
+ def _validated(cand, level, sample_unit):
91
+ """Check the candidate against fresh samples at off-grid points.
92
+
93
+ chebpts(m) for m != n shares no points with the fitting grid, so an
94
+ aliased fit cannot pass: its off-grid error is O(scale), ~1e15x the
95
+ chop level, while an honest fit's is ~1x. The 1000x margin makes this
96
+ a gross-error detector, not the accuracy contract.
97
+ """
98
+ m = cand.size + 11
99
+ t = algorithms.chebpts(m)
100
+ y = sample_unit(t)
101
+ err = np.max(np.abs(algorithms.chebval(t, cand) - y))
102
+ allow = max(1000.0 * level,
103
+ 100.0 * np.finfo(float).eps * float(np.sum(np.abs(cand))))
104
+ return err <= allow
105
+
106
+
107
+ def _adaptive(dct, sample_unit, tol, max_deg, interval):
108
+ n = min(17, max_deg + 1)
109
+ prev_rel = np.inf
110
+ while True:
111
+ c = dct(n)
112
+ scale = float(np.max(np.abs(c)))
113
+ if scale == 0.0:
114
+ return np.zeros(1)
115
+ cand, level = _chop(c, tol)
116
+ rel = level / scale
117
+ # The tail has settled if the chop level reached the requested tol,
118
+ # or if it stopped improving under node doubling while being far
119
+ # below the function scale: that is the sampling-noise plateau, and
120
+ # more nodes cannot beat it. A small-but-still-shrinking tail (a
121
+ # segment one doubling short of converged) must escalate, not chop.
122
+ settled = level <= 2.0 * tol * scale or (rel <= 1e-10 and rel >= prev_rel / 4.0)
123
+ if settled and cand.size <= c.size - max(2, c.size // 8):
124
+ if _validated(cand, level, sample_unit):
125
+ return cand
126
+ prev_rel = rel
127
+ if n >= max_deg + 1:
128
+ raise ValueError(
129
+ f"no validated convergence to tol={tol:g} by degree {max_deg} on "
130
+ f"{interval}; f may not be smooth there (split the domain), may "
131
+ "need a higher max_deg, or float64 sampling noise may sit above "
132
+ "tol (build with dps)")
133
+ n = min(2 * n - 1, max_deg + 1)
134
+
135
+
136
+ def _dct_np(f, a, b):
137
+ def sample_unit(t):
138
+ xk = a + (b - a) * (t + 1) / 2
139
+ return _sample(f, xk)
140
+
141
+ def dct(n):
142
+ return algorithms.chebfit_dct(sample_unit(algorithms.chebpts(n)))
143
+ return dct, sample_unit
144
+
145
+
146
+ def _sample(f, xk):
147
+ # scalar-only callables (wrapped special functions) are common; fall back.
148
+ try:
149
+ y = np.asarray(f(xk), dtype=float)
150
+ if y.shape == xk.shape:
151
+ return y
152
+ except (TypeError, ValueError):
153
+ pass
154
+ return np.array([float(f(x)) for x in xk])
155
+
156
+
157
+ def _dct_mp(f, a, b, dps):
158
+ import mpmath as mp
159
+
160
+ def sample_unit(t):
161
+ with mp.workdps(dps):
162
+ mid = (mp.mpf(b) + mp.mpf(a)) / 2
163
+ half = (mp.mpf(b) - mp.mpf(a)) / 2
164
+ return np.array([float(f(mid + half * mp.mpf(float(tk)))) for tk in t])
165
+
166
+ def dct(n):
167
+ with mp.workdps(dps):
168
+ mid = (mp.mpf(b) + mp.mpf(a)) / 2
169
+ half = (mp.mpf(b) - mp.mpf(a)) / 2
170
+ fk = [f(mid + half * mp.cos(mp.pi * (2 * i + 1) / (2 * n))) for i in range(n)]
171
+ c = []
172
+ for j in range(n):
173
+ s = mp.fsum(fk[i] * mp.cos(mp.pi * j * (2 * i + 1) / (2 * n)) for i in range(n))
174
+ c.append(2 * s / n)
175
+ c[0] /= 2
176
+ return np.array([float(x) for x in c])
177
+ return dct, sample_unit
@@ -0,0 +1,59 @@
1
+ """Base class for recipe callables, retiring per-class pytree boilerplate.
2
+
3
+ A recipe instance is a pytree whose children are ChebSeries (listed in
4
+ _series_fields) and whose auxiliary data are static scalars (listed in
5
+ _static_fields). Constructors take statics first, series after, in field
6
+ order; _post_init derives cached constants (called by both __init__ and
7
+ tree_unflatten). astype casts every series; __repr__ shows the statics and
8
+ the coefficient dtype.
9
+
10
+ Subclasses register themselves with the standard
11
+ @jax.tree_util.register_pytree_node_class decorator at the class site.
12
+ """
13
+
14
+ class Recipe:
15
+ _static_fields = ()
16
+ _series_fields = ()
17
+
18
+ def __init__(self, *args):
19
+ names = self._static_fields + self._series_fields
20
+ if len(args) != len(names):
21
+ raise TypeError(f"{type(self).__name__} expects {names}, got {len(args)} args")
22
+ for name, val in zip(names, args):
23
+ setattr(self, name, val)
24
+ self._post_init()
25
+
26
+ def _post_init(self):
27
+ pass
28
+
29
+ def astype(self, dtype):
30
+ return type(self)(*[getattr(self, f) for f in self._static_fields],
31
+ *[getattr(self, f).astype(dtype) for f in self._series_fields])
32
+
33
+ def __repr__(self):
34
+ statics = ", ".join(f"{f}={getattr(self, f)!r}" for f in self._static_fields)
35
+ dtype = "-"
36
+ # first series child with a coefficient array; children may be
37
+ # nested recipes (spherical wraps a cylindrical instance)
38
+ node = self
39
+ while getattr(node, "_series_fields", ()):
40
+ node = getattr(node, node._series_fields[0])
41
+ coef = getattr(node, "coef", None)
42
+ if coef is not None:
43
+ dtype = coef.dtype
44
+ break
45
+ return f"{type(self).__name__}({statics}, dtype={dtype})"
46
+
47
+ def tree_flatten(self):
48
+ return (tuple(getattr(self, f) for f in self._series_fields),
49
+ tuple(getattr(self, f) for f in self._static_fields))
50
+
51
+ @classmethod
52
+ def tree_unflatten(cls, aux, children):
53
+ obj = object.__new__(cls)
54
+ for name, val in zip(cls._static_fields, aux):
55
+ setattr(obj, name, val)
56
+ for name, val in zip(cls._series_fields, children):
57
+ setattr(obj, name, val)
58
+ obj._post_init()
59
+ return obj
@@ -0,0 +1,95 @@
1
+ """Shared runtime machinery for recipes: parameter-table reconstruction,
2
+ domain checks, the safeguarded root solver, and a float64 digamma.
3
+
4
+ Reconstruction convention (matches _gen_common's emitted axis note): for a
5
+ 2-D table, TABLE[k, j] is the j-th parameter-direction coefficient of the
6
+ k-th argument-direction coefficient, the parameter mapped affinely from
7
+ [lo, hi] onto [-1, 1]. `param_coefs` is the eager numpy path used by the
8
+ cached factories; `traced_coefs` is the jax path used by the *_fn traced
9
+ variants (differentiable in the parameter through the series custom_jvp).
10
+ """
11
+
12
+ import math
13
+
14
+ import jax
15
+ import jax.numpy as jnp
16
+ import numpy as np
17
+
18
+ from chebax._src import algorithms
19
+ from chebax._src.series import _chebval
20
+
21
+
22
+ def canon_tag():
23
+ """Cache-key tag for the canonical float dtype: factory instances built
24
+ under different x64 settings must not share a cache slot."""
25
+ return str(jnp.empty(()).dtype)
26
+
27
+
28
+ def canon_float(x):
29
+ """Promote to the canonical float dtype (float64 under x64).
30
+
31
+ Solvers mix inputs with reconstructed float64 tables inside a fori_loop
32
+ carry; an explicit float32 input under x64 raised a carry dtype
33
+ mismatch instead of computing. One deliberate dtype for everything."""
34
+ return jnp.asarray(x, dtype=jnp.empty(()).dtype)
35
+
36
+
37
+ def param_coefs(table, lo, hi, p):
38
+ """Coefficient vector at parameter p: Clenshaw across table rows. numpy."""
39
+ t = 2.0 * (p - lo) / (hi - lo) - 1.0
40
+ return np.array([algorithms.chebval(t, row) for row in table])
41
+
42
+
43
+ def param_coefs_der(table, lo, hi, p):
44
+ """d/dp of the coefficient vector, via chebder along the parameter axis."""
45
+ t = 2.0 * (p - lo) / (hi - lo) - 1.0
46
+ d = np.array([algorithms.chebval(t, algorithms.chebder(row)) for row in table])
47
+ return d * (2.0 / (hi - lo))
48
+
49
+
50
+ def traced_coefs(table, lo, hi, p):
51
+ """jax version of param_coefs: p may be a tracer (uniform per call)."""
52
+ t = 2.0 * (p - lo) / (hi - lo) - 1.0
53
+ return jax.vmap(lambda row: _chebval(t, row, (-1.0, 1.0)))(jnp.asarray(table))
54
+
55
+
56
+ def check_range(owner, name, v, lo, hi):
57
+ v = float(v)
58
+ if not lo <= v <= hi:
59
+ raise ValueError(f"{owner} table covers {name} in [{lo}, {hi}], got {v}")
60
+ return v
61
+
62
+
63
+ def newton_bisect(f_and_df, x0, lo0, hi0, iters):
64
+ """Fixed-count safeguarded Newton for increasing f: keep [lo, hi]
65
+ bracketing the root, fall back to the midpoint (or doubling while hi is
66
+ inf) when the Newton step leaves the bracket. The bounds test is
67
+ INCLUSIVE: a Newton step that stalls exactly at the current iterate
68
+ (converged) must be accepted, or a one-sided approach whose far bound
69
+ never tightened catapults a converged iterate to the midpoint of the
70
+ half-open bracket (measured, the quantile-toolkit bug)."""
71
+
72
+ def step(_, carry):
73
+ x, lo, hi = carry
74
+ f, df = f_and_df(x)
75
+ lo = jnp.where(f < 0, x, lo)
76
+ hi = jnp.where(f < 0, hi, x)
77
+ xn = x - f / df
78
+ mid = jnp.where(jnp.isfinite(hi), 0.5 * (lo + hi), 2.0 * x + 1.0)
79
+ ok = (xn >= lo) & (xn <= hi) & jnp.isfinite(xn)
80
+ return jnp.where(ok, xn, mid), lo, hi
81
+
82
+ x, _, _ = jax.lax.fori_loop(0, iters, step, (x0, lo0, hi0))
83
+ return x
84
+
85
+
86
+ def digamma64(z):
87
+ """float64 psi(z) for z >= 1: recurrence up to z >= 15, then the
88
+ Bernoulli series through z^-12 (A&S 6.3.5 / 6.3.18). Truncation < 1e-16."""
89
+ acc = 0.0
90
+ while z < 15.0:
91
+ acc -= 1.0 / z
92
+ z += 1.0
93
+ zi = 1.0 / (z * z)
94
+ tail = 1 / 12 - zi * (1 / 120 - zi * (1 / 252 - zi * (1 / 240 - zi * (1 / 132 - zi * (691 / 32760)))))
95
+ return acc + math.log(z) - 0.5 / z - zi * tail