chebax 0.1.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 (59) hide show
  1. chebax-0.1.0/LICENSE +28 -0
  2. chebax-0.1.0/PKG-INFO +94 -0
  3. chebax-0.1.0/README.md +72 -0
  4. chebax-0.1.0/pyproject.toml +33 -0
  5. chebax-0.1.0/setup.cfg +4 -0
  6. chebax-0.1.0/src/chebax/__init__.py +38 -0
  7. chebax-0.1.0/src/chebax/_src/__init__.py +0 -0
  8. chebax-0.1.0/src/chebax/_src/algorithms.py +74 -0
  9. chebax-0.1.0/src/chebax/_src/generate.py +177 -0
  10. chebax-0.1.0/src/chebax/_src/pytree.py +59 -0
  11. chebax-0.1.0/src/chebax/_src/recipes/__init__.py +0 -0
  12. chebax-0.1.0/src/chebax/_src/recipes/_common.py +95 -0
  13. chebax-0.1.0/src/chebax/_src/recipes/_gen_common.py +121 -0
  14. chebax-0.1.0/src/chebax/_src/recipes/besseli.py +209 -0
  15. chebax-0.1.0/src/chebax/_src/recipes/besseli_gen.py +84 -0
  16. chebax-0.1.0/src/chebax/_src/recipes/besseli_table.py +138 -0
  17. chebax-0.1.0/src/chebax/_src/recipes/besselj.py +176 -0
  18. chebax-0.1.0/src/chebax/_src/recipes/besselj_gen.py +146 -0
  19. chebax-0.1.0/src/chebax/_src/recipes/besselj_table.py +39 -0
  20. chebax-0.1.0/src/chebax/_src/recipes/besselj_table_ext.py +87 -0
  21. chebax-0.1.0/src/chebax/_src/recipes/besselk.py +184 -0
  22. chebax-0.1.0/src/chebax/_src/recipes/besselk_gen.py +107 -0
  23. chebax-0.1.0/src/chebax/_src/recipes/besselk_table.py +208 -0
  24. chebax-0.1.0/src/chebax/_src/recipes/bessely.py +185 -0
  25. chebax-0.1.0/src/chebax/_src/recipes/bessely_gen.py +103 -0
  26. chebax-0.1.0/src/chebax/_src/recipes/bessely_table.py +284 -0
  27. chebax-0.1.0/src/chebax/_src/recipes/betainc.py +128 -0
  28. chebax-0.1.0/src/chebax/_src/recipes/betainc_gen.py +78 -0
  29. chebax-0.1.0/src/chebax/_src/recipes/betainc_table.py +1791 -0
  30. chebax-0.1.0/src/chebax/_src/recipes/erf_family.py +63 -0
  31. chebax-0.1.0/src/chebax/_src/recipes/erf_gen.py +69 -0
  32. chebax-0.1.0/src/chebax/_src/recipes/erf_table.py +51 -0
  33. chebax-0.1.0/src/chebax/_src/recipes/lambertw.py +106 -0
  34. chebax-0.1.0/src/chebax/_src/recipes/quantiles.py +339 -0
  35. chebax-0.1.0/src/chebax/_src/recipes/spherical.py +96 -0
  36. chebax-0.1.0/src/chebax/_src/recipes/vonmises.py +113 -0
  37. chebax-0.1.0/src/chebax/_src/recipes/vonmises_gen.py +68 -0
  38. chebax-0.1.0/src/chebax/_src/recipes/vonmises_table.py +127 -0
  39. chebax-0.1.0/src/chebax/_src/series.py +223 -0
  40. chebax-0.1.0/src/chebax/bake/__init__.py +178 -0
  41. chebax-0.1.0/src/chebax/bake/_jaxpr_emit.py +264 -0
  42. chebax-0.1.0/src/chebax.egg-info/PKG-INFO +94 -0
  43. chebax-0.1.0/src/chebax.egg-info/SOURCES.txt +57 -0
  44. chebax-0.1.0/src/chebax.egg-info/dependency_links.txt +1 -0
  45. chebax-0.1.0/src/chebax.egg-info/requires.txt +13 -0
  46. chebax-0.1.0/src/chebax.egg-info/top_level.txt +1 -0
  47. chebax-0.1.0/tests/test_bake.py +171 -0
  48. chebax-0.1.0/tests/test_besseli.py +130 -0
  49. chebax-0.1.0/tests/test_besselj.py +152 -0
  50. chebax-0.1.0/tests/test_besselj_m3.py +127 -0
  51. chebax-0.1.0/tests/test_besselk.py +237 -0
  52. chebax-0.1.0/tests/test_bessely.py +87 -0
  53. chebax-0.1.0/tests/test_betainc.py +103 -0
  54. chebax-0.1.0/tests/test_erf_family.py +80 -0
  55. chebax-0.1.0/tests/test_lambertw.py +80 -0
  56. chebax-0.1.0/tests/test_m1.py +271 -0
  57. chebax-0.1.0/tests/test_quantiles.py +274 -0
  58. chebax-0.1.0/tests/test_spherical.py +59 -0
  59. chebax-0.1.0/tests/test_vonmises.py +115 -0
chebax-0.1.0/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Andres Hernandez
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
chebax-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.4
2
+ Name: chebax
3
+ Version: 0.1.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
+ Dynamic: license-file
22
+
23
+ # chebax
24
+
25
+ Differentiable special functions for JAX and GPUs, built from Chebyshev
26
+ approximants. A build-time generator (mpmath precision) produces fixed-degree
27
+ branchless polynomial kernels with exact derivative series — including gradients
28
+ with respect to function *parameters* (Bessel order, Beta shape, von Mises
29
+ concentration), which mainstream ML libraries lack. No mpmath at use time; the
30
+ runtime imports only jax and numpy.
31
+
32
+ ```sh
33
+ pip install chebax
34
+ ```
35
+
36
+ ```python
37
+ import jax
38
+ jax.config.update("jax_enable_x64", True) # before building instances: tables
39
+ # materialize in the active precision
40
+ import jax.numpy as jnp
41
+ import chebax
42
+
43
+ jv = chebax.besselj(2.5) # any real order in [0, 10]; cached
44
+ x = jnp.linspace(0.1, 30.0, 200)
45
+ jv(x), jax.vmap(jax.grad(jv))(x) # values and dJ/dx, jit/vmap-safe
46
+
47
+ chebax.besselk_fn(1.7, x) # order as a traced scalar:
48
+ jax.grad(chebax.besselk_fn)(1.7, 5.0) # jax.grad w.r.t. the ORDER
49
+ chebax.betainc_fn(2.0, 3.0, 0.4) # dI/da, dI/db via jax.grad (jax#38610)
50
+ chebax.betaincinv(2.0, 3.0, 0.05) # differentiable Beta quantile (jax#2399)
51
+ ```
52
+
53
+ ## What's in
54
+
55
+ | family | factory | traced params | param derivative | notes |
56
+ |---|---|---|---|---|
57
+ | `besselj` | `besselj(v)` | — | `besselj_dnu(v)` | x ≥ 0, validated to 1e4 |
58
+ | `bessely` | `bessely(v)` | — (structural) | `bessely_dnu(v)` | x ≥ 1e-6 |
59
+ | `besseli` | `besseli(v, scaled=)` | `besseli_fn` | `besseli_dnu(v, scaled=)` | `scaled` = scipy's ive |
60
+ | `besselk` | `besselk(v)` | `besselk_fn`, `log_besselk_fn` | `besselk_dnu(v)` | x ≥ 1e-6; Matérn demo; the log form has no underflow ceiling |
61
+ | `betainc` | `betainc(a, b)` | `betainc_fn` | via `grad` of `_fn` | (a, b) ∈ [0.1, 10]² |
62
+ | spherical | `spherical_jn/yn(n)` | — | — | n ∈ [0, 9], via half-integer tables |
63
+ | quantiles | — | `betaincinv`, `gammaincinv`, `stdtr`, `stdtrit` | via `grad` (IFT) | jax#2399/#5350/#20358 |
64
+ | von Mises | — | `vonmises_cdf/icdf` | via `grad` | κ ∈ [0, 50] |
65
+ | erf family | — | `dawsn`, `erfcx` | — (no params) | recent jax ships both; kept for the C++ bake path |
66
+ | Lambert W | — | `lambertw(x, k)` | — | both real branches, jax#13680 |
67
+
68
+ Plus the generic core (`fit`, `ChebSeries`, `PiecewiseCheb`) and bake emitters
69
+ (`chebax.bake.jax_module`, `chebax.bake.xsf_header`: self-contained pure-jax
70
+ modules and C++17 headers from an instance).
71
+
72
+ `notebooks/` holds themed, executed walkthroughs: why Chebyshev nodes work,
73
+ the Bessel family with a learnable Matérn kernel, differentiable quantiles,
74
+ truncated and circular distributions in numpyro, Gaussian tails / Lambert W /
75
+ binomial reliability, copulas, and baking. The numpyro and plotting
76
+ dependencies install with `pip install chebax[examples]`. `examples/` holds
77
+ plain scripts: a few of the notebook workflows in copy-paste form, plus
78
+ integration examples with no notebook counterpart (a GIG distribution for
79
+ efax, learned Matérn smoothness, truncated sampling).
80
+
81
+ Parameters come first, evaluation point last (opposite of scipy). Orders/shapes
82
+ are uniform per call; per-element parameter arrays are out of scope by design.
83
+ Accuracy is validated against mpmath at 40 dps — measured worst cases and the
84
+ per-family error metrics are in each test file's docstring; tables regenerate
85
+ bit-for-bit from checked-in generators.
86
+
87
+ ## Docs
88
+
89
+ `docs/adding-a-recipe.md` is the workflow for new functions;
90
+ `docs/increments.md` the design log of every recipe (what was measured, which
91
+ traps were found); `PROJECT.md` the plan and evidence; `CLAUDE.md` how to work
92
+ in the repo. Grown out of a private research project on GPU Bessel functions;
93
+ its two load-bearing measurements are reproduced here in `experiments/`.
94
+ BSD-3-Clause.
chebax-0.1.0/README.md ADDED
@@ -0,0 +1,72 @@
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
+ `notebooks/` holds themed, executed walkthroughs: why Chebyshev nodes work,
51
+ the Bessel family with a learnable Matérn kernel, differentiable quantiles,
52
+ truncated and circular distributions in numpyro, Gaussian tails / Lambert W /
53
+ binomial reliability, copulas, and baking. The numpyro and plotting
54
+ dependencies install with `pip install chebax[examples]`. `examples/` holds
55
+ plain scripts: a few of the notebook workflows in copy-paste form, plus
56
+ integration examples with no notebook counterpart (a GIG distribution for
57
+ efax, learned Matérn smoothness, truncated sampling).
58
+
59
+ Parameters come first, evaluation point last (opposite of scipy). Orders/shapes
60
+ are uniform per call; per-element parameter arrays are out of scope by design.
61
+ Accuracy is validated against mpmath at 40 dps — measured worst cases and the
62
+ per-family error metrics are in each test file's docstring; tables regenerate
63
+ bit-for-bit from checked-in generators.
64
+
65
+ ## Docs
66
+
67
+ `docs/adding-a-recipe.md` is the workflow for new functions;
68
+ `docs/increments.md` the design log of every recipe (what was measured, which
69
+ traps were found); `PROJECT.md` the plan and evidence; `CLAUDE.md` how to work
70
+ in the repo. Grown out of a private research project on GPU Bessel functions;
71
+ its two load-bearing measurements are reproduced here in `experiments/`.
72
+ BSD-3-Clause.
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "chebax"
7
+ version = "0.1.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
+
26
+ [tool.setuptools.packages.find]
27
+ where = ["src"]
28
+
29
+ [tool.pytest.ini_options]
30
+ testpaths = ["tests"]
31
+ markers = [
32
+ "slow: table regeneration and other multi-minute tests",
33
+ ]
chebax-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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")
File without changes
@@ -0,0 +1,74 @@
1
+ """The textbook algorithms, in plain numpy, on [-1, 1].
2
+
3
+ Coefficient convention is numpy.polynomial's: plain c[0], lowest order first,
4
+ p(t) = sum(c[k] * T_k(t)). These are the reference implementations; the jax
5
+ runtime in series.py must agree with them (tested against numpy.polynomial).
6
+ Domain handling lives in the series classes, not here.
7
+ """
8
+
9
+ import numpy as np
10
+
11
+
12
+ def chebpts(n):
13
+ """First-kind Chebyshev points cos(pi*(2i+1)/(2n)), decreasing from ~1 to ~-1."""
14
+ i = np.arange(n)
15
+ return np.cos(np.pi * (2 * i + 1) / (2 * n))
16
+
17
+
18
+ def chebfit_dct(samples):
19
+ """Interpolation coefficients from samples taken at chebpts(len(samples)).
20
+
21
+ Discrete orthogonality of cosines on the first-kind points; O(n^2), which
22
+ is fine at generator degrees.
23
+ """
24
+ n = len(samples)
25
+ i = np.arange(n)
26
+ j = np.arange(n)[:, None]
27
+ # reduce the angle with exact integer arithmetic; the raw product reaches
28
+ # hundreds of radians at large n and costs ~angle*eps of cosine accuracy
29
+ weights = np.cos(np.pi * ((j * (2 * i + 1)) % (4 * n)) / (2 * n))
30
+ c = (2.0 / n) * (weights @ np.asarray(samples, dtype=float))
31
+ c[0] /= 2
32
+ return c
33
+
34
+
35
+ def chebval(t, c):
36
+ """Clenshaw evaluation of a plain Chebyshev series."""
37
+ t = np.asarray(t, dtype=float)
38
+ b1 = np.zeros_like(t)
39
+ b2 = np.zeros_like(t)
40
+ for ck in np.asarray(c, dtype=float)[:0:-1]:
41
+ b1, b2 = 2 * t * b1 - b2 + ck, b1
42
+ return t * b1 - b2 + c[0]
43
+
44
+
45
+ def chebder(c):
46
+ """Coefficients of the derivative (numpy.polynomial's exact recurrence)."""
47
+ c = np.array(c, dtype=float)
48
+ n = c.size - 1
49
+ if n == 0:
50
+ return np.zeros(1)
51
+ der = np.zeros(n)
52
+ for j in range(n, 2, -1):
53
+ der[j - 1] = 2 * j * c[j]
54
+ c[j - 2] += j * c[j] / (j - 2)
55
+ if n > 1:
56
+ der[1] = 4 * c[2]
57
+ der[0] = c[1]
58
+ return der
59
+
60
+
61
+ def chebint(c):
62
+ """Coefficients of the antiderivative that vanishes at t=0
63
+ (numpy.polynomial's k=0, lbnd=0 convention)."""
64
+ c = np.asarray(c, dtype=float)
65
+ n = c.size
66
+ out = np.zeros(n + 1)
67
+ out[1] = c[0]
68
+ if n > 1:
69
+ out[2] = c[1] / 4
70
+ for j in range(2, n):
71
+ out[j + 1] = c[j] / (2 * (j + 1))
72
+ out[j - 1] -= c[j] / (2 * (j - 1))
73
+ out[0] -= chebval(0.0, out)
74
+ return out
@@ -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
File without changes