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.
- chebax-0.2.0/PKG-INFO +106 -0
- chebax-0.2.0/README.md +82 -0
- chebax-0.2.0/pyproject.toml +34 -0
- chebax-0.2.0/src/chebax/__init__.py +38 -0
- chebax-0.2.0/src/chebax/_src/generate.py +177 -0
- chebax-0.2.0/src/chebax/_src/pytree.py +59 -0
- chebax-0.2.0/src/chebax/_src/recipes/_common.py +95 -0
- chebax-0.2.0/src/chebax/_src/recipes/_gen_common.py +121 -0
- chebax-0.2.0/src/chebax/_src/recipes/besseli.py +209 -0
- chebax-0.2.0/src/chebax/_src/recipes/besseli_gen.py +84 -0
- chebax-0.2.0/src/chebax/_src/recipes/besseli_table.py +138 -0
- chebax-0.2.0/src/chebax/_src/recipes/besselj.py +176 -0
- chebax-0.2.0/src/chebax/_src/recipes/besselj_gen.py +146 -0
- chebax-0.2.0/src/chebax/_src/recipes/besselj_table.py +39 -0
- chebax-0.2.0/src/chebax/_src/recipes/besselj_table_ext.py +87 -0
- chebax-0.2.0/src/chebax/_src/recipes/besselk.py +184 -0
- chebax-0.2.0/src/chebax/_src/recipes/besselk_gen.py +107 -0
- chebax-0.2.0/src/chebax/_src/recipes/besselk_table.py +208 -0
- chebax-0.2.0/src/chebax/_src/recipes/bessely.py +185 -0
- chebax-0.2.0/src/chebax/_src/recipes/bessely_gen.py +103 -0
- chebax-0.2.0/src/chebax/_src/recipes/bessely_table.py +284 -0
- chebax-0.2.0/src/chebax/_src/recipes/betainc.py +128 -0
- chebax-0.2.0/src/chebax/_src/recipes/betainc_gen.py +78 -0
- chebax-0.2.0/src/chebax/_src/recipes/betainc_table.py +1791 -0
- chebax-0.2.0/src/chebax/_src/recipes/erf_family.py +63 -0
- chebax-0.2.0/src/chebax/_src/recipes/erf_gen.py +69 -0
- chebax-0.2.0/src/chebax/_src/recipes/erf_table.py +51 -0
- chebax-0.2.0/src/chebax/_src/recipes/lambertw.py +106 -0
- chebax-0.2.0/src/chebax/_src/recipes/quantiles.py +339 -0
- chebax-0.2.0/src/chebax/_src/recipes/spherical.py +96 -0
- chebax-0.2.0/src/chebax/_src/recipes/vonmises.py +113 -0
- chebax-0.2.0/src/chebax/_src/recipes/vonmises_gen.py +68 -0
- chebax-0.2.0/src/chebax/_src/recipes/vonmises_table.py +127 -0
- {chebax-0.1.0.dev0 → chebax-0.2.0}/src/chebax/_src/series.py +37 -9
- chebax-0.2.0/src/chebax/bake/__init__.py +178 -0
- chebax-0.2.0/src/chebax/bake/_jaxpr_emit.py +264 -0
- chebax-0.2.0/src/chebax/pytensor.py +195 -0
- chebax-0.2.0/src/chebax.egg-info/PKG-INFO +106 -0
- chebax-0.2.0/src/chebax.egg-info/SOURCES.txt +59 -0
- chebax-0.2.0/src/chebax.egg-info/requires.txt +16 -0
- chebax-0.2.0/tests/test_bake.py +171 -0
- chebax-0.2.0/tests/test_besseli.py +130 -0
- {chebax-0.1.0.dev0 → chebax-0.2.0}/tests/test_besselj.py +38 -7
- chebax-0.2.0/tests/test_besselj_m3.py +127 -0
- chebax-0.2.0/tests/test_besselk.py +237 -0
- chebax-0.2.0/tests/test_bessely.py +87 -0
- chebax-0.2.0/tests/test_betainc.py +103 -0
- chebax-0.2.0/tests/test_erf_family.py +80 -0
- chebax-0.2.0/tests/test_lambertw.py +80 -0
- {chebax-0.1.0.dev0 → chebax-0.2.0}/tests/test_m1.py +61 -2
- chebax-0.2.0/tests/test_pytensor_plugin.py +142 -0
- chebax-0.2.0/tests/test_quantiles.py +274 -0
- chebax-0.2.0/tests/test_spherical.py +59 -0
- chebax-0.2.0/tests/test_vonmises.py +115 -0
- chebax-0.1.0.dev0/PKG-INFO +0 -35
- chebax-0.1.0.dev0/README.md +0 -17
- chebax-0.1.0.dev0/pyproject.toml +0 -26
- chebax-0.1.0.dev0/src/chebax/__init__.py +0 -11
- chebax-0.1.0.dev0/src/chebax/_src/generate.py +0 -106
- chebax-0.1.0.dev0/src/chebax/_src/recipes/besselj.py +0 -138
- chebax-0.1.0.dev0/src/chebax/_src/recipes/besselj_gen.py +0 -76
- chebax-0.1.0.dev0/src/chebax/_src/recipes/besselj_table.py +0 -37
- chebax-0.1.0.dev0/src/chebax.egg-info/PKG-INFO +0 -35
- chebax-0.1.0.dev0/src/chebax.egg-info/SOURCES.txt +0 -19
- chebax-0.1.0.dev0/src/chebax.egg-info/requires.txt +0 -8
- {chebax-0.1.0.dev0 → chebax-0.2.0}/LICENSE +0 -0
- {chebax-0.1.0.dev0 → chebax-0.2.0}/setup.cfg +0 -0
- {chebax-0.1.0.dev0 → chebax-0.2.0}/src/chebax/_src/__init__.py +0 -0
- {chebax-0.1.0.dev0 → chebax-0.2.0}/src/chebax/_src/algorithms.py +0 -0
- {chebax-0.1.0.dev0 → chebax-0.2.0}/src/chebax/_src/recipes/__init__.py +0 -0
- {chebax-0.1.0.dev0 → chebax-0.2.0}/src/chebax.egg-info/dependency_links.txt +0 -0
- {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
|