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.
- chebax-0.1.0/LICENSE +28 -0
- chebax-0.1.0/PKG-INFO +94 -0
- chebax-0.1.0/README.md +72 -0
- chebax-0.1.0/pyproject.toml +33 -0
- chebax-0.1.0/setup.cfg +4 -0
- chebax-0.1.0/src/chebax/__init__.py +38 -0
- chebax-0.1.0/src/chebax/_src/__init__.py +0 -0
- chebax-0.1.0/src/chebax/_src/algorithms.py +74 -0
- chebax-0.1.0/src/chebax/_src/generate.py +177 -0
- chebax-0.1.0/src/chebax/_src/pytree.py +59 -0
- chebax-0.1.0/src/chebax/_src/recipes/__init__.py +0 -0
- chebax-0.1.0/src/chebax/_src/recipes/_common.py +95 -0
- chebax-0.1.0/src/chebax/_src/recipes/_gen_common.py +121 -0
- chebax-0.1.0/src/chebax/_src/recipes/besseli.py +209 -0
- chebax-0.1.0/src/chebax/_src/recipes/besseli_gen.py +84 -0
- chebax-0.1.0/src/chebax/_src/recipes/besseli_table.py +138 -0
- chebax-0.1.0/src/chebax/_src/recipes/besselj.py +176 -0
- chebax-0.1.0/src/chebax/_src/recipes/besselj_gen.py +146 -0
- chebax-0.1.0/src/chebax/_src/recipes/besselj_table.py +39 -0
- chebax-0.1.0/src/chebax/_src/recipes/besselj_table_ext.py +87 -0
- chebax-0.1.0/src/chebax/_src/recipes/besselk.py +184 -0
- chebax-0.1.0/src/chebax/_src/recipes/besselk_gen.py +107 -0
- chebax-0.1.0/src/chebax/_src/recipes/besselk_table.py +208 -0
- chebax-0.1.0/src/chebax/_src/recipes/bessely.py +185 -0
- chebax-0.1.0/src/chebax/_src/recipes/bessely_gen.py +103 -0
- chebax-0.1.0/src/chebax/_src/recipes/bessely_table.py +284 -0
- chebax-0.1.0/src/chebax/_src/recipes/betainc.py +128 -0
- chebax-0.1.0/src/chebax/_src/recipes/betainc_gen.py +78 -0
- chebax-0.1.0/src/chebax/_src/recipes/betainc_table.py +1791 -0
- chebax-0.1.0/src/chebax/_src/recipes/erf_family.py +63 -0
- chebax-0.1.0/src/chebax/_src/recipes/erf_gen.py +69 -0
- chebax-0.1.0/src/chebax/_src/recipes/erf_table.py +51 -0
- chebax-0.1.0/src/chebax/_src/recipes/lambertw.py +106 -0
- chebax-0.1.0/src/chebax/_src/recipes/quantiles.py +339 -0
- chebax-0.1.0/src/chebax/_src/recipes/spherical.py +96 -0
- chebax-0.1.0/src/chebax/_src/recipes/vonmises.py +113 -0
- chebax-0.1.0/src/chebax/_src/recipes/vonmises_gen.py +68 -0
- chebax-0.1.0/src/chebax/_src/recipes/vonmises_table.py +127 -0
- chebax-0.1.0/src/chebax/_src/series.py +223 -0
- chebax-0.1.0/src/chebax/bake/__init__.py +178 -0
- chebax-0.1.0/src/chebax/bake/_jaxpr_emit.py +264 -0
- chebax-0.1.0/src/chebax.egg-info/PKG-INFO +94 -0
- chebax-0.1.0/src/chebax.egg-info/SOURCES.txt +57 -0
- chebax-0.1.0/src/chebax.egg-info/dependency_links.txt +1 -0
- chebax-0.1.0/src/chebax.egg-info/requires.txt +13 -0
- chebax-0.1.0/src/chebax.egg-info/top_level.txt +1 -0
- chebax-0.1.0/tests/test_bake.py +171 -0
- chebax-0.1.0/tests/test_besseli.py +130 -0
- chebax-0.1.0/tests/test_besselj.py +152 -0
- chebax-0.1.0/tests/test_besselj_m3.py +127 -0
- chebax-0.1.0/tests/test_besselk.py +237 -0
- chebax-0.1.0/tests/test_bessely.py +87 -0
- chebax-0.1.0/tests/test_betainc.py +103 -0
- chebax-0.1.0/tests/test_erf_family.py +80 -0
- chebax-0.1.0/tests/test_lambertw.py +80 -0
- chebax-0.1.0/tests/test_m1.py +271 -0
- chebax-0.1.0/tests/test_quantiles.py +274 -0
- chebax-0.1.0/tests/test_spherical.py +59 -0
- 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,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
|