API reference¤
Everything below is exported from the top-level spexial namespace:
import spexial as sp
Names match scipy.special wherever a counterpart exists; see Conventions for the rules, and Accuracy and domains for the supported input range of each function.
spexial: scipy.special in JAX.
spexial implements special functions on top of JAX, so that they compose
with jax.jit, jax.vmap and jax.grad. Where a scipy.special counterpart
exists the name, call signature and return values follow it.
Two exports have no scipy.special counterpart:
polylog, the polylogarithm :math:\mathrm{Li}_n(z)(comparempmath.polylog).eval_gegenbauers, which returns the Gegenbauer polynomial of degreenand every lower degree, as a by-product of the recurrence.
The remaining exports do have one, but are not always drop-in replacements --
k0/k1/k2 are accurate to ~2e-7 rather than to machine precision, and
comb is the exact=False variant, so it returns a float that is only
close to the integer. Each docstring states its own domain and accuracy.
Examples:
>>> import spexial as sp
>>> round(float(sp.comb(5, 2)), 9)
10.0
comb is inexact, so the value is near 10 rather than exactly 10:
>>> float(sp.comb(5, 2)) == 10.0
False
__all__ = ['K0', 'K1', 'K2', 'K0e', 'K1e', 'K2e', 'Li', 'SphericalJnRecurrence', '__version__', 'comb', 'eval_gegenbauer', 'eval_gegenbauers', 'gamma', 'incomplete_beta', 'k0', 'k0e', 'k1', 'k1e', 'k2', 'k2e', 'polylog', 'spence', 'sph_harm_y', 'sph_harm_y_cart', 'sph_harm_y_cart_all', 'sph_harm_y_cart_all_terms', 'sph_legendre_p', 'spherical_jn', 'spherical_jn_all', 'zeta']
module-attribute
¤
Built-in mutable sequence.
If no argument is given, the constructor creates a new empty list. The argument must be an iterable if specified.
__cached__ = '/home/runner/work/spexial/spexial/src/spexial/__pycache__/__init__.cpython-314.pyc'
module-attribute
¤
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__doc__ = '``spexial``: `scipy.special` in JAX.\n\n``spexial`` implements special functions on top of JAX, so that they compose\nwith `jax.jit`, `jax.vmap` and `jax.grad`. Where a `scipy.special` counterpart\nexists the name, call signature and return values follow it.\n\nTwo exports have **no** `scipy.special` counterpart:\n\n- `polylog`, the polylogarithm :math:`\\mathrm{Li}_n(z)` (compare `mpmath.polylog`).\n- `eval_gegenbauers`, which returns the Gegenbauer polynomial of degree ``n``\n *and every lower degree*, as a by-product of the recurrence.\n\nThe remaining exports do have one, but are not always drop-in replacements --\n`k0`/`k1`/`k2` are accurate to ~2e-7 rather than to machine precision, and\n`comb` is the ``exact=False`` variant, so it returns a float that is only\nclose to the integer. Each docstring states its own domain and accuracy.\n\nExamples\n--------\n>>> import spexial as sp\n>>> round(float(sp.comb(5, 2)), 9)\n10.0\n\n`comb` is inexact, so the value is near 10 rather than exactly 10:\n\n>>> float(sp.comb(5, 2)) == 10.0\nFalse\n\n'
module-attribute
¤
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__file__ = '/home/runner/work/spexial/spexial/src/spexial/__init__.py'
module-attribute
¤
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__name__ = 'spexial'
module-attribute
¤
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__package__ = 'spexial'
module-attribute
¤
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__path__ = ['/home/runner/work/spexial/spexial/src/spexial']
module-attribute
¤
Built-in mutable sequence.
If no argument is given, the constructor creates a new empty list. The argument must be an iterable if specified.
__version__ = '0.2.1.dev16'
module-attribute
¤
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
SphericalJnRecurrence
¤
Which recurrence spherical_jn and spherical_jn_all evaluate.
Upward recurrence is stable above the turning point |z| ~ n and unstable
below it; Miller's downward recurrence is the reverse. BOTH runs each on
its own side, which is right everywhere and costs both.
BOTH = <SphericalJnRecurrence.BOTH: 'both'>
class-attribute
¤
Which recurrence spherical_jn and spherical_jn_all evaluate.
Upward recurrence is stable above the turning point |z| ~ n and unstable
below it; Miller's downward recurrence is the reverse. BOTH runs each on
its own side, which is right everywhere and costs both.
DOWN = <SphericalJnRecurrence.DOWN: 'down'>
class-attribute
¤
Which recurrence spherical_jn and spherical_jn_all evaluate.
Upward recurrence is stable above the turning point |z| ~ n and unstable
below it; Miller's downward recurrence is the reverse. BOTH runs each on
its own side, which is right everywhere and costs both.
UP = <SphericalJnRecurrence.UP: 'up'>
class-attribute
¤
Which recurrence spherical_jn and spherical_jn_all evaluate.
Upward recurrence is stable above the turning point |z| ~ n and unstable
below it; Miller's downward recurrence is the reverse. BOTH runs each on
its own side, which is right everywhere and costs both.
__doc__ = "Which recurrence `spherical_jn` and `spherical_jn_all` evaluate.\n\nUpward recurrence is stable above the turning point ``|z| ~ n`` and unstable\nbelow it; Miller's downward recurrence is the reverse. `BOTH` runs each on\nits own side, which is right everywhere and costs both.\n"
class-attribute
¤
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__module__ = 'spexial._src.spherical_bessel'
class-attribute
¤
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__format__(format_spec)
method descriptor
¤
Return a formatted version of the string as described by format_spec.
__str__()
method descriptor
¤
Return str(self).
K0(z: jaxtyping.Real[Array, ...] | jaxtyping.Real[ndarray, ...] | numpy.number | int | float) -> Shaped[Array, ...]
¤
Compute the modified Bessel function of the second kind of order 0.
Equivalent to scipy.special.kn(0, z). See Zhang and Jin,
SPECIAL_FUNCTIONS in FORTRAN77, for the algorithm: an ascending series
below z = 9 and an asymptotic expansion above it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
z
|
Real[Array, ...] | Real[ndarray, ...] | number | int | float
|
Real positive argument, of any shape. Evaluated elementwise. |
required |
Returns:
| Type | Description |
|---|---|
Array
|
Value(s) of :math: |
Examples:
>>> import jax.numpy as jnp
>>> import spexial as sp
>>> round(float(sp.k0(1.0)), 8)
0.42102444
Array input is evaluated elementwise, spanning both branches:
>>> [round(float(k), 8) for k in sp.k0(jnp.asarray([0.5, 5.0, 20.0]))]
[0.92441907, 0.0036911, 0.0]
K0e(z: jaxtyping.Real[Array, ...] | jaxtyping.Real[ndarray, ...] | numpy.number | int | float) -> Shaped[Array, ...]
¤
Compute the exponentially scaled :math:e^z K_0(z).
Equivalent to scipy.special.k0e(z), which has no JAX counterpart. This
is the form to reach for beyond z = 705, where :math:K_0(z) itself is
smaller than any normal double and unrepresentable; :math:e^z K_0(z)
decays only as :math:1/\sqrt{z} and stays accurate at any z.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
z
|
Real[Array, ...] | Real[ndarray, ...] | number | int | float
|
Real positive argument, of any shape. Evaluated elementwise. |
required |
Returns:
| Type | Description |
|---|---|
Array
|
Value(s) of :math: |
Examples:
>>> import spexial as sp
>>> round(float(sp.k0e(1.0)), 8)
1.14446308
Where k0 has underflowed to zero, the scaled form is still exact:
>>> float(sp.k0(800.0))
0.0
>>> round(float(sp.k0e(800.0)), 10)
0.0443044275
K1(z: jaxtyping.Real[Array, ...] | jaxtyping.Real[ndarray, ...] | numpy.number | int | float) -> Shaped[Array, ...]
¤
Compute the modified Bessel function of the second kind of order 1.
Obtained from k0 through the Wronskian
:math:I_0(z) K_1(z) + I_1(z) K_0(z) = 1/z.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
z
|
Real[Array, ...] | Real[ndarray, ...] | number | int | float
|
Real positive argument, of any shape. Evaluated elementwise. |
required |
Returns:
| Type | Description |
|---|---|
Array
|
Value(s) of :math: |
Examples:
>>> import jax.numpy as jnp
>>> import spexial as sp
>>> round(float(sp.k1(1.0)), 8)
0.60190723
>>> [round(float(k), 8) for k in sp.k1(jnp.asarray([0.5, 5.0, 20.0]))]
[1.65644112, 0.00404461, 0.0]
K1e(z: jaxtyping.Real[Array, ...] | jaxtyping.Real[ndarray, ...] | numpy.number | int | float) -> Shaped[Array, ...]
¤
Compute the exponentially scaled :math:e^z K_1(z).
Equivalent to scipy.special.k1e(z), which has no JAX counterpart.
Obtained from k0e through the Wronskian
:math:I_0(z) K_1(z) + I_1(z) K_0(z) = 1/z, in the scaled variables.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
z
|
Real[Array, ...] | Real[ndarray, ...] | number | int | float
|
Real positive argument, of any shape. Evaluated elementwise. |
required |
Returns:
| Type | Description |
|---|---|
Array
|
Value(s) of :math: |
Examples:
>>> import spexial as sp
>>> round(float(sp.k1e(1.0)), 8)
1.63615349
>>> round(float(sp.k1e(800.0)), 10)
0.0443321091
K2(z: jaxtyping.Real[Array, ...] | jaxtyping.Real[ndarray, ...] | numpy.number | int | float) -> Shaped[Array, ...]
¤
Compute the modified Bessel function of the second kind of order 2.
Obtained from the recurrence :math:K_2(z) = K_0(z) + (2/z) K_1(z).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
z
|
Real[Array, ...] | Real[ndarray, ...] | number | int | float
|
Real positive argument, of any shape. Evaluated elementwise. |
required |
Returns:
| Type | Description |
|---|---|
Array
|
Value(s) of :math: |
Examples:
>>> import jax.numpy as jnp
>>> import spexial as sp
>>> round(float(sp.k2(1.0)), 8)
1.6248389
>>> [round(float(k), 8) for k in sp.k2(jnp.asarray([0.5, 5.0, 20.0]))]
[7.55018355, 0.00530894, 0.0]
K2e(z: jaxtyping.Real[Array, ...] | jaxtyping.Real[ndarray, ...] | numpy.number | int | float) -> Shaped[Array, ...]
¤
Compute the exponentially scaled :math:e^z K_2(z).
Equivalent to scipy.special.kve(2, z), which has no JAX counterpart.
Obtained from the recurrence :math:K_2(z) = K_0(z) + (2/z) K_1(z), which
the scaling leaves unchanged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
z
|
Real[Array, ...] | Real[ndarray, ...] | number | int | float
|
Real positive argument, of any shape. Evaluated elementwise. |
required |
Returns:
| Type | Description |
|---|---|
Array
|
Value(s) of :math: |
Examples:
>>> import spexial as sp
>>> round(float(sp.k2e(1.0)), 8)
4.41677005
>>> round(float(sp.k2e(800.0)), 10)
0.0444152578
Li(n: int, z: jaxtyping.Shaped[Array, ...] | jaxtyping.Shaped[ndarray, ...] | numpy.bool | numpy.number | bool | int | float | complex) -> Shaped[Array, ...]
¤
Compute the polylogarithm :math:\mathrm{Li}_n(z).
There is no scipy.special counterpart; mpmath.polylog is the reference
used by the test suite.
Three series are stitched together: the defining sum for
:math:|z| \le 1/2, the Hurwitz-zeta expansion in :math:\log z for
:math:1/2 < |z| < 2, and the inversion formula for :math:|z| \ge 2.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Order of the polylogarithm. Must be a static Python |
required |
z
|
Shaped[Array, ...] | Shaped[ndarray, ...] | bool | number | bool | int | float | complex
|
Real argument, of any shape. Evaluated elementwise. |
required |
Returns:
| Type | Description |
|---|---|
Array
|
The real part of :math: |
Notes
Measured against mpmath.polylog over 1 <= n <= 20 and
|z| <= 1000, the relative error stays below ~1e-12 in all three
branches. The bound on n is branch-dependent, and the tighter
one bites first: for :math:\lvert z \rvert \ge 2 the inversion formula
needs the Bernoulli table, which stops at :math:B_{60}, so n > 60
there returns nan. The other two branches run to n = 170, where
:math:\Gamma(n+1) overflows.
Examples:
>>> import spexial as sp
Li_1(z) == -log(1 - z):
>>> round(float(sp.polylog(1, 0.5)), 12)
0.69314718056
Li_2(1) == zeta(2):
>>> round(float(sp.polylog(2, 1.0)), 10)
1.6449340668
>>> round(float(sp.polylog(3, -1.0)), 8)
-0.90154268
comb(N: jaxtyping.Shaped[Array, ...] | jaxtyping.Shaped[ndarray, ...] | numpy.bool | numpy.number | bool | int | float | complex, k: jaxtyping.Shaped[Array, ...] | jaxtyping.Shaped[ndarray, ...] | numpy.bool | numpy.number | bool | int | float | complex) -> Shaped[Array, ...]
¤
Compute the number of combinations of N things taken k at a time.
This is the "N choose k" factor :math:N! / (k! (N-k)!). It is the
inexact variant of scipy.special.comb -- the exact=False one -- and
is evaluated through jax.scipy.special.gammaln, so N and k need
not be integers and the result is a float, not a Python int.
Reference: https://docs.scipy.org/doc/scipy/reference/generated/scipy.special.comb.html
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
N
|
Shaped[Array, ...] | Shaped[ndarray, ...] | bool | number | bool | int | float | complex
|
Number of things. Broadcast against |
required |
k
|
Shaped[Array, ...] | Shaped[ndarray, ...] | bool | number | bool | int | float | complex
|
Number of elements taken. Broadcast against |
required |
Returns:
| Type | Description |
|---|---|
Array
|
The total number of combinations. Following |
Examples:
>>> import jax.numpy as jnp
>>> import spexial as sp
>>> round(float(sp.comb(5, 2)), 5)
10.0
Rounded to five places, not twelve, because these examples have to be true
at the library's default precision as well as under x64. Without x64 this
is 9.999995: jax.scipy.special.gammaln returns 2**-21 rather than 0 at
a float32 argument of exactly 1 (spexial#29), so comb(N, 0) -- which
reduces to exp(-gammaln(1)) -- is up to 8 ulps below the exact 1 it
should be.
Up to 8, and not a single value: measured over N from 0 to FLT_MAX, the
shortfall is 0, 4, 6 or 8 ulps depending on N, as the two branch
cross-overs and gammaln's own rounding move under it. It is 0 from about
N = 8.9e6, where the asymptotic form takes over, and 0 again at a
scattering of smaller N where the subtraction happens to round away.
Naming individual N here would be describing the rounding rather than the
library. float16 and bfloat16 round it away entirely, at every N.
float64 is exact throughout.
Out-of-range k gives 0, not nan:
>>> [
... round(float(c), 5)
... for c in sp.comb(jnp.asarray([5, 5, 5]), jnp.asarray([2, 6, -1]))
... ]
[10.0, 0.0, 0.0]
Non-integer arguments are the generalized binomial coefficient:
>>> round(float(sp.comb(5.5, 2)), 5)
12.375
eval_gegenbauer(n: int, alpha: jaxtyping.Shaped[Array, ...] | jaxtyping.Shaped[ndarray, ...] | numpy.bool | numpy.number | bool | int | float | complex, x: jaxtyping.Shaped[Array, ...] | jaxtyping.Shaped[ndarray, ...] | numpy.bool | numpy.number | bool | int | float | complex) -> Shaped[Array, ...]
¤
Evaluate the Gegenbauer polynomial :math:C_n^{(\alpha)}(x).
The Gegenbauer polynomials can be defined via the Gauss hypergeometric
function :math:{}_2F_1 as
.. math::
C_n^{(\alpha)} = \frac{(2\alpha)_n}{\Gamma(n + 1)}
{}_2F_1(-n, 2\alpha + n; \alpha + 1/2; (1 - z)/2).
When :math:n is an integer the result is a polynomial of degree
:math:n. See 22.5.46 in [AS]_ for details.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Degree of the polynomial. Must be a static Python |
required |
alpha
|
Shaped[Array, ...] | Shaped[ndarray, ...] | bool | number | bool | int | float | complex
|
Parameter. |
required |
x
|
Shaped[Array, ...] | Shaped[ndarray, ...] | bool | number | bool | int | float | complex
|
Point(s) at which to evaluate the polynomial. Evaluated elementwise. |
required |
Returns:
| Type | Description |
|---|---|
Array
|
Values of :math: |
See Also
eval_gegenbauers : the same, plus every lower degree. scipy.special.roots_gegenbauer : roots and quadrature weights. jax.scipy.special.hyp2f1 : Gauss hypergeometric function.
References
.. [AS] Milton Abramowitz and Irene A. Stegun, eds. Handbook of Mathematical Functions with Formulas, Graphs, and Mathematical Tables. New York: Dover, 1972.
Examples:
>>> import jax.numpy as jnp
>>> import spexial as sp
>>> float(sp.eval_gegenbauer(3, 1.0, 0.5))
-1.0
It broadcasts over x:
>>> sp.eval_gegenbauer(2, 1.0, jnp.asarray([-1.0, 0.0, 1.0])).tolist()
[3.0, -1.0, 3.0]
eval_gegenbauers(n: int, alpha: jaxtyping.Shaped[Array, ...] | jaxtyping.Shaped[ndarray, ...] | numpy.bool | numpy.number | bool | int | float | complex, x: jaxtyping.Shaped[Array, ...] | jaxtyping.Shaped[ndarray, ...] | numpy.bool | numpy.number | bool | int | float | complex) -> Shaped[Array, ...]
¤
Return the Gegenbauer polynomial of degree n and all lower ones.
There is no scipy.special counterpart; it is the by-product of the
three-term recurrence that eval_gegenbauer runs anyway, and is much
cheaper than n + 1 separate calls.
The Gegenbauer polynomials can be defined via the Gauss hypergeometric
function :math:{}_2F_1 as
.. math::
C_n^{(\alpha)} = \frac{(2\alpha)_n}{\Gamma(n + 1)}
{}_2F_1(-n, 2\alpha + n; \alpha + 1/2; (1 - z)/2).
When :math:n is an integer the result is a polynomial of degree
:math:n. See 22.5.46 in [AS]_ for details.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Degree of the polynomial. Must be a static Python |
required |
alpha
|
Shaped[Array, ...] | Shaped[ndarray, ...] | bool | number | bool | int | float | complex
|
Parameter. Broadcast against |
required |
x
|
Shaped[Array, ...] | Shaped[ndarray, ...] | bool | number | bool | int | float | complex
|
Point(s) at which to evaluate the polynomials. |
required |
Returns:
| Type | Description |
|---|---|
Array[float, (n + 1, ...)]
|
Values of :math: |
References
.. [AS] Milton Abramowitz and Irene A. Stegun, eds. Handbook of Mathematical Functions with Formulas, Graphs, and Mathematical Tables. New York: Dover, 1972.
Examples:
>>> import spexial as sp
>>> sp.eval_gegenbauers(3, 1.0, 0.5).tolist()
[1.0, 1.0, 0.0, -1.0]
The degree-0 case is a single value, not two:
>>> sp.eval_gegenbauers(0, 1.0, 0.5).tolist()
[1.0]
alpha and x broadcast, so a whole table of orders against
parameters comes out of the one recurrence:
>>> import jax.numpy as jnp
>>> alpha = jnp.asarray([1.0, 2.0])[:, None]
>>> x = jnp.asarray([0.0, 0.5, 1.0])
>>> sp.eval_gegenbauers(3, alpha, x).shape
(4, 2, 3)
gamma(x: jaxtyping.Shaped[Array, ...] | jaxtyping.Shaped[ndarray, ...] | numpy.bool | numpy.number | bool | int | float | complex) -> Shaped[Array, ...]
¤
Compute the gamma function :math:\Gamma(x).
The value is jax.scipy.special.gamma, called directly, so it cannot drift
from upstream. What this adds is the derivative: JAX differentiates its own
implementation term by term, while :math:\Gamma'(x) = \Gamma(x)\,\psi(x)
is one extra call. The saving is in memory, not time: measured over
10,000 points, jax.grad keeps 240 kB of residuals through the backward
pass against this rule's 80 kB, while wall-clock is a wash (171 µs against
166 µs, i.e. 1.03x -- neutral within noise, reproduced across grad,
vmap(grad) and jvp harnesses). An earlier revision of this docstring
claimed 4.3x faster; that was a measurement error, and it is not plausible
either -- JAX's gamma is sign * exp(gammaln(x)), whose autodiff already
is the same product, so there is no arithmetic to save.
Reference: https://docs.scipy.org/doc/scipy/reference/generated/scipy.special.gamma.html
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
Shaped[Array, ...] | Shaped[ndarray, ...] | bool | number | bool | int | float | complex
|
Argument, of any shape. Evaluated elementwise. Complex input requires
|
required |
Returns:
| Type | Description |
|---|---|
Array
|
Value(s) of the gamma function. |
Examples:
>>> import jax.numpy as jnp
>>> import spexial as sp
>>> round(float(sp.gamma(5.0)), 10)
24.0
It broadcasts, and handles negative arguments:
>>> [round(float(g), 10) for g in sp.gamma(jnp.asarray([0.5, 1.0, -0.5]))]
[1.7724538509, 1.0, -3.5449077018]
Complex input works on jax >= 0.10.2 and returns the complex gamma
function; it is not shown here because this example is executed against
every supported JAX, including ones that predate it.
incomplete_beta(a: jaxtyping.Shaped[Array, ''] | jaxtyping.Shaped[ndarray, ''] | numpy.bool | numpy.number | bool | int | float | complex, b: jaxtyping.Shaped[Array, ''] | jaxtyping.Shaped[ndarray, ''] | numpy.bool | numpy.number | bool | int | float | complex, z: jaxtyping.Shaped[Array, ...] | jaxtyping.Shaped[ndarray, ...] | numpy.bool | numpy.number | bool | int | float | complex) -> Shaped[Array, ...]
¤
Unregularized incomplete beta function :math:B(a, b, z).
.. math::
B(a, b, z) = \int_0^z t^{a-1}(1-t)^{b-1}\,\mathrm{d}t
the DLMF 8.17.1 form. There is no scipy.special counterpart: SciPy's
betainc is the regularized :math:I_z(a, b), and the two differ by
the complete beta function :math:B(a, b).
Reconstructing this as beta(a, b) * betainc(a, b, z) works only for
:math:b > 0. At :math:b \le 0 the complete beta function has a pole
while the product does not, so that route returns nan over a domain this
function handles without difficulty.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
a
|
Shaped[Array, ''] | Shaped[ndarray, ''] | bool | number | bool | int | float | complex
|
First parameter. Must be positive, and scalar. |
required |
b
|
Shaped[Array, ''] | Shaped[ndarray, ''] | bool | number | bool | int | float | complex
|
Second parameter. Any real value, including zero and negative -- which is the reason this function exists. Scalar. |
required |
z
|
Shaped[Array, ...] | Shaped[ndarray, ...] | bool | number | bool | int | float | complex
|
Upper limit of integration, in :math: |
required |
Returns:
| Type | Description |
|---|---|
Array
|
Shaped like |
See Also
scipy.special.betainc : the regularized form, :math:I_z(a, b).
jax.scipy.special.hyp2f1 : expresses this via DLMF 8.17.7, at the cost of
a data-dependent jax.lax.while_loop.
Notes
Two fixed-length series, switched at :math:z = 1/2, each converging like
:math:2^{-k}; see _small_z and _large_z. The
:math:z-derivative is supplied by a jax.custom_jvp and is exact and
O(1) -- by Leibniz it is just the integrand at the endpoint,
:math:z^{a-1}(1-z)^{b-1} -- rather than differentiating through 64 terms.
It is a custom_jvp rather than a custom_vjp so that jax.hessian's
jacfwd(jacrev(...)) still composes.
Examples:
>>> import jax.numpy as jnp
>>> import jax.scipy.special as jsp
>>> import spexial as sp
It agrees with the regularized form wherever that is defined, including
close to the :math:z \to 1 endpoint:
>>> a, b = 2.0, 1.5
>>> z = jnp.asarray([0.3, 0.999])
>>> bool(
... jnp.allclose(
... sp.incomplete_beta(a, b, z), jsp.beta(a, b) * jsp.betainc(a, b, z)
... )
... )
True
But unlike that product it stays finite for b <= 0:
>>> round(float(sp.incomplete_beta(2.0, 0.0, jnp.asarray(0.5))), 8)
0.19314718
>>> bool(jnp.isnan(jsp.beta(2.0, 0.0) * jsp.betainc(2.0, 0.0, 0.5)))
True
k0(z: jaxtyping.Real[Array, ...] | jaxtyping.Real[ndarray, ...] | numpy.number | int | float) -> Shaped[Array, ...]
¤
Compute the modified Bessel function of the second kind of order 0.
Equivalent to scipy.special.kn(0, z). See Zhang and Jin,
SPECIAL_FUNCTIONS in FORTRAN77, for the algorithm: an ascending series
below z = 9 and an asymptotic expansion above it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
z
|
Real[Array, ...] | Real[ndarray, ...] | number | int | float
|
Real positive argument, of any shape. Evaluated elementwise. |
required |
Returns:
| Type | Description |
|---|---|
Array
|
Value(s) of :math: |
Examples:
>>> import jax.numpy as jnp
>>> import spexial as sp
>>> round(float(sp.k0(1.0)), 8)
0.42102444
Array input is evaluated elementwise, spanning both branches:
>>> [round(float(k), 8) for k in sp.k0(jnp.asarray([0.5, 5.0, 20.0]))]
[0.92441907, 0.0036911, 0.0]
k0e(z: jaxtyping.Real[Array, ...] | jaxtyping.Real[ndarray, ...] | numpy.number | int | float) -> Shaped[Array, ...]
¤
Compute the exponentially scaled :math:e^z K_0(z).
Equivalent to scipy.special.k0e(z), which has no JAX counterpart. This
is the form to reach for beyond z = 705, where :math:K_0(z) itself is
smaller than any normal double and unrepresentable; :math:e^z K_0(z)
decays only as :math:1/\sqrt{z} and stays accurate at any z.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
z
|
Real[Array, ...] | Real[ndarray, ...] | number | int | float
|
Real positive argument, of any shape. Evaluated elementwise. |
required |
Returns:
| Type | Description |
|---|---|
Array
|
Value(s) of :math: |
Examples:
>>> import spexial as sp
>>> round(float(sp.k0e(1.0)), 8)
1.14446308
Where k0 has underflowed to zero, the scaled form is still exact:
>>> float(sp.k0(800.0))
0.0
>>> round(float(sp.k0e(800.0)), 10)
0.0443044275
k1(z: jaxtyping.Real[Array, ...] | jaxtyping.Real[ndarray, ...] | numpy.number | int | float) -> Shaped[Array, ...]
¤
Compute the modified Bessel function of the second kind of order 1.
Obtained from k0 through the Wronskian
:math:I_0(z) K_1(z) + I_1(z) K_0(z) = 1/z.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
z
|
Real[Array, ...] | Real[ndarray, ...] | number | int | float
|
Real positive argument, of any shape. Evaluated elementwise. |
required |
Returns:
| Type | Description |
|---|---|
Array
|
Value(s) of :math: |
Examples:
>>> import jax.numpy as jnp
>>> import spexial as sp
>>> round(float(sp.k1(1.0)), 8)
0.60190723
>>> [round(float(k), 8) for k in sp.k1(jnp.asarray([0.5, 5.0, 20.0]))]
[1.65644112, 0.00404461, 0.0]
k1e(z: jaxtyping.Real[Array, ...] | jaxtyping.Real[ndarray, ...] | numpy.number | int | float) -> Shaped[Array, ...]
¤
Compute the exponentially scaled :math:e^z K_1(z).
Equivalent to scipy.special.k1e(z), which has no JAX counterpart.
Obtained from k0e through the Wronskian
:math:I_0(z) K_1(z) + I_1(z) K_0(z) = 1/z, in the scaled variables.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
z
|
Real[Array, ...] | Real[ndarray, ...] | number | int | float
|
Real positive argument, of any shape. Evaluated elementwise. |
required |
Returns:
| Type | Description |
|---|---|
Array
|
Value(s) of :math: |
Examples:
>>> import spexial as sp
>>> round(float(sp.k1e(1.0)), 8)
1.63615349
>>> round(float(sp.k1e(800.0)), 10)
0.0443321091
k2(z: jaxtyping.Real[Array, ...] | jaxtyping.Real[ndarray, ...] | numpy.number | int | float) -> Shaped[Array, ...]
¤
Compute the modified Bessel function of the second kind of order 2.
Obtained from the recurrence :math:K_2(z) = K_0(z) + (2/z) K_1(z).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
z
|
Real[Array, ...] | Real[ndarray, ...] | number | int | float
|
Real positive argument, of any shape. Evaluated elementwise. |
required |
Returns:
| Type | Description |
|---|---|
Array
|
Value(s) of :math: |
Examples:
>>> import jax.numpy as jnp
>>> import spexial as sp
>>> round(float(sp.k2(1.0)), 8)
1.6248389
>>> [round(float(k), 8) for k in sp.k2(jnp.asarray([0.5, 5.0, 20.0]))]
[7.55018355, 0.00530894, 0.0]
k2e(z: jaxtyping.Real[Array, ...] | jaxtyping.Real[ndarray, ...] | numpy.number | int | float) -> Shaped[Array, ...]
¤
Compute the exponentially scaled :math:e^z K_2(z).
Equivalent to scipy.special.kve(2, z), which has no JAX counterpart.
Obtained from the recurrence :math:K_2(z) = K_0(z) + (2/z) K_1(z), which
the scaling leaves unchanged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
z
|
Real[Array, ...] | Real[ndarray, ...] | number | int | float
|
Real positive argument, of any shape. Evaluated elementwise. |
required |
Returns:
| Type | Description |
|---|---|
Array
|
Value(s) of :math: |
Examples:
>>> import spexial as sp
>>> round(float(sp.k2e(1.0)), 8)
4.41677005
>>> round(float(sp.k2e(800.0)), 10)
0.0444152578
polylog(n: int, z: jaxtyping.Shaped[Array, ...] | jaxtyping.Shaped[ndarray, ...] | numpy.bool | numpy.number | bool | int | float | complex) -> Shaped[Array, ...]
¤
Compute the polylogarithm :math:\mathrm{Li}_n(z).
There is no scipy.special counterpart; mpmath.polylog is the reference
used by the test suite.
Three series are stitched together: the defining sum for
:math:|z| \le 1/2, the Hurwitz-zeta expansion in :math:\log z for
:math:1/2 < |z| < 2, and the inversion formula for :math:|z| \ge 2.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Order of the polylogarithm. Must be a static Python |
required |
z
|
Shaped[Array, ...] | Shaped[ndarray, ...] | bool | number | bool | int | float | complex
|
Real argument, of any shape. Evaluated elementwise. |
required |
Returns:
| Type | Description |
|---|---|
Array
|
The real part of :math: |
Notes
Measured against mpmath.polylog over 1 <= n <= 20 and
|z| <= 1000, the relative error stays below ~1e-12 in all three
branches. The bound on n is branch-dependent, and the tighter
one bites first: for :math:\lvert z \rvert \ge 2 the inversion formula
needs the Bernoulli table, which stops at :math:B_{60}, so n > 60
there returns nan. The other two branches run to n = 170, where
:math:\Gamma(n+1) overflows.
Examples:
>>> import spexial as sp
Li_1(z) == -log(1 - z):
>>> round(float(sp.polylog(1, 0.5)), 12)
0.69314718056
Li_2(1) == zeta(2):
>>> round(float(sp.polylog(2, 1.0)), 10)
1.6449340668
>>> round(float(sp.polylog(3, -1.0)), 8)
-0.90154268
spence(z: jaxtyping.Shaped[Array, ...] | jaxtyping.Shaped[ndarray, ...] | numpy.bool | numpy.number | bool | int | float | complex) -> Shaped[Array, ...]
¤
Compute Spence's function -- the dilogarithm -- for real or complex input.
jax.scipy.special.spence covers the real case at every JAX version
spexial supports, and raises on complex input. This accepts both, and
carries an analytic derivative: differentiating the series instead costs
444x the time and 1652x the residual memory (26.4 MB against 16 kB over
2000 points; the near_root fallback added ~2 MB to the differentiated
series, which is exactly the cost a custom rule avoids).
.. math::
\int_{1}^{z} dt \frac{\log(t)}{1 - t}
There is no upper limit on a real argument. Above z = 1/tiny the value
comes from the inversion formula rather than from upstream, whose opening
1/z is subnormal there and is flushed into the wrong branch; see the
comment on that branch. spence(inf) is nan, as in SciPy.
See Also
scipy.special.spence: the reference implementation in scipy jax.scipy.special.spence: jax implementation for real inputs
sph_harm_y(n: int, m: int, theta: jaxtyping.Real[Array, ...] | jaxtyping.Real[ndarray, ...] | numpy.number | int | float, phi: jaxtyping.Real[Array, ...] | jaxtyping.Real[ndarray, ...] | numpy.number | int | float) -> Complex[Array, ...]
¤
Spherical harmonic :math:Y_n^m(\theta, \phi).
.. math::
Y_n^m(\theta, \phi) = \bar{P}_n^m(\cos\theta)\, e^{im\phi}
with sph_legendre_p supplying :math:\bar{P}_n^m. Same convention as
scipy.special.sph_harm_y and jax.scipy.special.sph_harm_y: theta is
the polar angle and phi the azimuth, and the Condon-Shortley phase is
included.
This differs from jax.scipy.special.sph_harm_y in two ways that are the
reason it exists, both measured on JAX 0.11.1. Upstream pairs n[i] with
theta[i] positionally instead of broadcasting, so a scalar degree
against a batch of angles is right only at index 0 -- up to 1.18 absolute
error for :math:n \le 3 -- and it rejects 0-d input. And upstream's
derivatives are nan at :math:\theta = 0, \pi for every :math:n \ge 1.
Here n and m are static, so there is nothing to mispair, and
theta/phi broadcast against each other at any rank.
.. note::
The **Cartesian** gradient of an :math:`m \ge 1` harmonic is still
exactly zero on the z-axis if it is obtained by differentiating through
:math:`\theta` and :math:`\phi`, which have no directional derivative
there. That is a property of the coordinates, not of any
implementation. Use `sph_harm_y_cart` when the gradient on the axis
matters.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Degree and order, with |
required |
m
|
int
|
Degree and order, with |
required |
theta
|
Real[Array, ...] | Real[ndarray, ...] | number | int | float
|
Polar angle in radians. |
required |
phi
|
Real[Array, ...] | Real[ndarray, ...] | number | int | float
|
Azimuthal angle in radians. Broadcast against |
required |
Returns:
| Type | Description |
|---|---|
Array
|
Complex, of the broadcast shape of |
See Also
sph_harm_y_cart : the same harmonic from a Cartesian direction. scipy.special.sph_harm_y : the SciPy counterpart.
Examples:
>>> import jax.numpy as jnp
>>> import spexial as sp
>>> complex(sp.sph_harm_y(0, 0, 0.4, 1.2)).real
0.28209479177387814
It broadcasts, which upstream does not:
>>> theta = jnp.asarray([0.3, 1.1, 2.0])
>>> sp.sph_harm_y(2, 1, theta, 0.0).real.round(6).tolist()
[-0.218107, -0.312301, 0.292333]
sph_harm_y_cart(n: int, m: int, uvec: jaxtyping.Real[Array, ...] | jaxtyping.Real[ndarray, ...] | numpy.number | int | float) -> Complex[Array, ...]
¤
Spherical harmonic :math:Y_n^m from a Cartesian unit direction.
.. math::
Y_n^m = q_n^m(\hat{z}) \left(\hat{x} + i\hat{y}\right)^m,
\qquad q_n^m(u) = N_{nm} \frac{P_n^m(u)}{(1-u^2)^{m/2}}
Mathematically identical to sph_harm_y(n, m, arccos(uz), arctan2(uy,
ux)), and numerically better behaved in the one place it matters: the
right-hand side is a polynomial in :math:\hat{x} and :math:\hat{y},
so it is smooth on the z-axis, where :math:\theta and :math:\phi are
singular.
That singularity is not cosmetic. Differentiating the :math:(\theta,
\phi) form through the chain rule gives a Cartesian gradient of exactly
zero for every :math:m \ge 1 term on the axis -- neither angle has a
directional derivative there -- against a non-zero true limit. There is no
counterpart to this function in SciPy or JAX; the name is
scipy.special.sph_harm_y plus the argument convention that distinguishes
it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Degree and order, with |
required |
m
|
int
|
Degree and order, with |
required |
uvec
|
Real[Array, ...] | Real[ndarray, ...] | number | int | float
|
Cartesian direction, shape |
required |
Returns:
| Type | Description |
|---|---|
Array
|
Complex, shaped like |
See Also
sph_harm_y : the same harmonic in spherical coordinates.
sph_harm_y_cart_all : every (l, m) up to a maximum degree, in one sweep.
Examples:
>>> import jax, jax.numpy as jnp
>>> import spexial as sp
>>> uvec = jnp.asarray([0.0, 0.0, 1.0])
>>> round(complex(sp.sph_harm_y_cart(1, 0, uvec)).real, 10)
0.4886025119
On the z-axis the gradient of an m = 1 term is finite and non-zero,
where the spherical form gives exactly zero:
>>> g = jax.grad(lambda v: sp.sph_harm_y_cart(1, 1, v).real)(uvec)
>>> round(float(g[0]), 6)
-0.345494
sph_harm_y_cart_all(n: int, m: int, uvec: jaxtyping.Real[Array, ...] | jaxtyping.Real[ndarray, ...] | numpy.number | int | float) -> Complex[Array, ...]
¤
Every :math:Y_l^k with :math:l \le n and :math:\lvert k \rvert \le m.
The same values sph_harm_y_cart returns pair by pair, but with both
recurrences carried across the table instead of restarted for each entry:
one pass per order advances :math:((x+iy)/r)^k by a single complex
multiply and walks the Legendre recurrence up in :math:l from its seed at
:math:l = k. That makes the table :math:O(n^2) rather than cubic.
Since the degree and order are static the saving is in traced operations,
so it shows up as a smaller HLO -- and hence faster tracing and compilation
-- rather than as faster execution, which XLA's fusion had already largely
recovered. Measured on the equivalent code in galax, tracing was 3-4.6x
faster for :math:n \le 20 with run time flat.
Layout and argument order follow scipy.special.sph_harm_y_all, which has
no JAX counterpart; the difference is only that the direction is Cartesian.
.. warning::
Reach for this when you want the **table**. If instead you are about
to index it and reduce -- summing :math:`\\sum_{lm} c_{lm} Y_l^m`,
say -- call `sph_harm_y_cart` per pair and fold each term into the sum
as it is produced. Indexing a stacked table defeats XLA's fusion, so
the whole thing is materialized: measured on a multipole expansion at
:math:`n = 12` over a million directions, the table form ran in 17.7 s
against 10 ms for per-pair calls, for identical values. The saving
here is in *traced* operations -- roughly 2.3x less tracing and
compiling at :math:`n = 20` -- which is worth having only when the
table itself is the thing you need.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Maximum degree. Static: it sets the first axis. |
required |
m
|
int
|
Maximum order, |
required |
uvec
|
Real[Array, ...] | Real[ndarray, ...] | number | int | float
|
Cartesian direction, shape |
required |
Returns:
| Type | Description |
|---|---|
Array
|
Complex, shape |
Examples:
>>> import jax.numpy as jnp
>>> import spexial as sp
>>> uz = jnp.asarray([0.0, 0.0, 1.0])
>>> Y = sp.sph_harm_y_cart_all(2, 2, uz)
>>> Y.shape
(3, 5)
Entries with |j| > i are zero, since no such harmonic exists:
>>> bool((Y[0, 1] == 0) & (Y[1, 2] == 0))
True
Each entry matches the one-pair function, negative orders included:
>>> bool(jnp.isclose(Y[2, -1], sp.sph_harm_y_cart(2, -1, uz)))
True
sph_harm_y_cart_all_terms(n: int, m: int, uvec: jaxtyping.Real[Array, ...] | jaxtyping.Real[ndarray, ...] | numpy.number | int | float) -> tuple[tuple[jaxtyping.Complex[Array, ...], ...], ...]
¤
Every :math:Y_l^k as a separate array, sharing the recurrences.
Same values, same indexing and the same layout as sph_harm_y_cart_all --
terms[i][j] is :math:Y_i^j, with negative orders reachable at the end
of the inner tuple by ordinary negative indexing, as in SciPy. The
difference is entirely the container: a nested tuple of arrays rather than
one stacked array.
That difference is a performance decision, and a large one. Indexing a
stacked table stops XLA folding each term into a caller's reduction as it
is produced, so the whole table is materialized instead. Measured on a
multipole expansion at :math:n = 12 over a million directions, summing
:math:\sum_{lm} c_{lm} Y_l^m from sph_harm_y_cart_all ran in 17.7 s
against 10 ms from these terms, for identical values.
So: use this when you are going to reduce over the table, and
sph_harm_y_cart_all when you want the table itself.
Deliberately not jax.jit-decorated, unlike everything else here.
A jitted function returning a pytree materializes each leaf as its own
output buffer at the call boundary, which is exactly the fusion this exists
to preserve. It is a pure trace-time helper: it runs inside the caller's
trace, and the caller is free to jit around it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Maximum degree. Static. |
required |
m
|
int
|
Maximum order, |
required |
uvec
|
Real[Array, ...] | Real[ndarray, ...] | number | int | float
|
Cartesian direction, shape |
required |
Returns:
| Type | Description |
|---|---|
tuple[tuple[Array, ...], ...]
|
|
See Also
sph_harm_y_cart_all : the same table, stacked into one array.
Examples:
>>> import jax.numpy as jnp
>>> import spexial as sp
>>> uz = jnp.asarray([0.0, 0.0, 1.0])
>>> terms = sp.sph_harm_y_cart_all_terms(2, 2, uz)
>>> len(terms), len(terms[0])
(3, 5)
Indexed exactly as the stacked table is, negative orders included:
>>> bool(jnp.isclose(terms[2][-1], sp.sph_harm_y_cart(2, -1, uz)))
True
sph_legendre_p(n: int, m: int, theta: jaxtyping.Real[Array, ...] | jaxtyping.Real[ndarray, ...] | numpy.number | int | float) -> Shaped[Array, ...]
¤
Spherical Legendre function :math:\bar{P}_n^m(\cos\theta).
.. math::
\bar{P}_n^m(\cos\theta) = \sqrt{\frac{2n+1}{4\pi}
\frac{(n-m)!}{(n+m)!}}\; P_n^m(\cos\theta)
the normalization for which :math:Y_n^m(\theta,\phi) =
\bar{P}_n^m(\cos\theta)\,e^{im\phi}. Includes the Condon-Shortley phase,
matching scipy.special.sph_legendre_p, scipy.special.lpmv and GSL's
gsl_sf_legendre_sphPlm.
jax.scipy.special has no counterpart at any version -- the normalized
Legendre function is reachable only through jax.scipy.special.sph_harm_y,
and then only as part of a complex harmonic.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Degree and order, with |
required |
m
|
int
|
Degree and order, with |
required |
theta
|
Real[Array, ...] | Real[ndarray, ...] | number | int | float
|
Polar angle in radians, of any shape. |
required |
Returns:
| Type | Description |
|---|---|
Array
|
Shaped like |
See Also
sph_harm_y : the full complex harmonic. scipy.special.sph_legendre_p : the SciPy counterpart.
Examples:
>>> import jax.numpy as jnp
>>> import spexial as sp
:math:\bar{P}_0^0 = 1/\sqrt{4\pi} everywhere:
>>> round(float(sp.sph_legendre_p(0, 0, 0.7)), 10)
0.2820947918
Negative orders follow SciPy,
:math:\bar{P}_n^{-m} = (-1)^m \bar{P}_n^{m}:
>>> bool(jnp.isclose(sp.sph_legendre_p(2, -1, 0.3), -sp.sph_legendre_p(2, 1, 0.3)))
True
Unlike upstream's, the derivative is finite at the pole:
>>> import jax
>>> float(jax.grad(lambda t: sp.sph_legendre_p(1, 0, t))(0.0))
-0.0
spherical_jn(n: int, z: jaxtyping.Shaped[Array, ...] | jaxtyping.Shaped[ndarray, ...] | numpy.bool | numpy.number | bool | int | float | complex, derivative: bool = False, *, recurrence: spexial._src.spherical_bessel.SphericalJnRecurrence | typing.Literal['both', 'up', 'down'] = <SphericalJnRecurrence.BOTH: 'both'>) -> Shaped[Array, ...]
¤
Compute the spherical Bessel function of the first kind, :math:j_n(z).
Equivalent to scipy.special.spherical_jn for real z. Computed by
recurrence from :math:j_0 and :math:j_1, and recurrence chooses
which (see SphericalJnRecurrence). Upward recurrence is stable above the
turning point :math:|z| \approx n and unstable below it; Miller's
downward recurrence is the reverse.
SphericalJnRecurrence.BOTH, the default, runs each on its own side. It is right everywhere, down to the smallest values, and is the choice unless you know where your arguments are. It runs only the recurrence a batch needs, so a batch wholly on one side of the turning point costs whatUPorDOWNalone would (within ~30%). Downward is the dearer of the two, 1.7-4.6x upward forn >= 100on CPU, so a batch that straddles the turning point costs 3-5.3xSphericalJnRecurrence.UP-- more at smalln, where upward is nearly free: 7x in float64 and 12x in float32 atn = 10. Underjax.vmapevery batch pays that straddling price, as the check is made per element. It takes 2-3.5x as long asUPto compile.SphericalJnRecurrence.UPis the fastest, and exact wherever :math:|z| \ge n. Use it when every argument is above the turning point, or when only values near the peak matter. Below the turning point it is noise: in float64, values under ~1e-6 of the peak are wrong in sign and magnitude or set to exactly zero, and in float32 the whole region is wrong, by up to 80x the peak.SphericalJnRecurrence.DOWNis for arguments known to be below the turning point, e.g. small :math:krin a large multipole. It is as accurate asSphericalJnRecurrence.BOTHthere, skips the upward pass, and returnsnanat :math:|z| \ge n, for the value and the derivative alike. Orders 0 and 1 are closed forms, exact everywhere.
Each n compiles separately, and so does each recurrence. For many
orders at the same z, use spherical_jn_all, which computes them all at
once.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Order. Must be a static, non-negative Python |
required |
z
|
Shaped[Array, ...] | Shaped[ndarray, ...] | bool | number | bool | int | float | complex
|
Real argument, of any shape. Evaluated elementwise. |
required |
derivative
|
bool
|
If |
False
|
recurrence
|
SphericalJnRecurrence | Literal['both', 'up', 'down']
|
Which recurrence to run: a |
<SphericalJnRecurrence.BOTH: 'both'>
|
Returns:
| Type | Description |
|---|---|
Array
|
Value(s) of :math: |
References
.. [1] J. Lesgourgues and T. Tram, "Fast and accurate CMB computations in non-flat FLRW universes", JCAP 09 (2014) 032, arXiv:1312.2697. .. [2] T. Tram, "Computation of hyperspherical Bessel functions", Communications in Computational Physics 22 (2017) 852, arXiv:1311.0839.
Examples:
>>> import spexial as sp
>>> round(float(sp.spherical_jn(2, 3.0)), 12)
0.298637497076
>>> round(float(sp.spherical_jn(1, 0.0, derivative=True)), 12)
0.333333333333
Far below the turning point the default is right, and upward recurrence alone returns noise, here with the wrong sign:
>>> f"{float(sp.spherical_jn(5, 0.1)):.4e}"
'9.6163e-10'
>>> float(sp.spherical_jn(5, 0.1, recurrence=sp.SphericalJnRecurrence.UP)) < 0
True
spherical_jn_all(n: int, z: jaxtyping.Shaped[Array, ...] | jaxtyping.Shaped[ndarray, ...] | numpy.bool | numpy.number | bool | int | float | complex, derivative: bool = False, *, recurrence: spexial._src.spherical_bessel.SphericalJnRecurrence | typing.Literal['both', 'up', 'down'] = <SphericalJnRecurrence.BOTH: 'both'>) -> Shaped[Array, ...]
¤
Return :math:j_l(z) for every order l = 0 ... n.
There is no scipy.special counterpart. The orders come from the same
recurrences as spherical_jn, chosen the same way by recurrence, with
the same accuracy and cost. Each order has its own turning point, and the
split is made at the highest one, |z| = n: with
SphericalJnRecurrence.BOTH, every row at |z| < n comes from the
downward recurrence, and SphericalJnRecurrence.DOWN is nan for the
whole column at |z| >= n.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Highest order. Must be a static, non-negative Python |
required |
z
|
Shaped[Array, ...] | Shaped[ndarray, ...] | bool | number | bool | int | float | complex
|
Real argument, of any shape. |
required |
derivative
|
bool
|
If |
False
|
recurrence
|
SphericalJnRecurrence | Literal['both', 'up', 'down']
|
As |
<SphericalJnRecurrence.BOTH: 'both'>
|
Returns:
| Type | Description |
|---|---|
Array[float, (n + 1, ...)]
|
The orders |
Examples:
>>> import spexial as sp
>>> [round(v, 12) for v in sp.spherical_jn_all(3, 2.0).tolist()]
[0.454648713413, 0.43539777498, 0.198447949057, 0.060722097663]
zeta(n: jaxtyping.Real[Array, ...] | jaxtyping.Real[ndarray, ...] | numpy.number | int | float) -> Shaped[Array, ...]
¤
Compute the Riemann zeta function :math:\zeta(n).
Differs from jax.scipy.special.zeta in that negative arguments are
supported, through the functional equation
:math:\zeta(-k) = (-1)^k B_{k+1} / (k+1).
Reference: https://docs.scipy.org/doc/scipy/reference/generated/scipy.special.zeta.html
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
Real[Array, ...] | Real[ndarray, ...] | number | int | float
|
Real argument, of any shape. Evaluated elementwise. |
required |
Returns:
| Type | Description |
|---|---|
Array
|
Value(s) of :math: |
Notes
Every real n is covered, by whichever of four methods is accurate there:
n >= 54-- exactly1.0. :math:\zeta(n) - 1 \approx 2^{-n}is below half an eps of 1 from there up, so this is the exact double-precision value rather than an approximation. It also avoidsjax.scipy.special.zeta, which returnsnanfornabove about1e15.n > 1-- delegated tojax.scipy.special.zeta.n = 1-- the pole,inf.-0.5 < n < 1-- Borwein's acceleration of the eta series. This is the critical strip, whichjax.scipy.special.zetadoes not implement, plus a little below zero where the reflection below cannot be used.na negative even integer -- exactly0, at any magnitude.na negative integer down to-59-- from the tabulated :math:B_{1-n}, which is exact to the ulp.-60is covered too, but as an even integer rather than by the table, which ends at :math:B_{60}.- every other
n <= -0.5-- the functional equation :math:\zeta(s) = 2^s \pi^{s-1} \sin(\pi s/2) \Gamma(1-s) \zeta(1-s), evaluated in log space so that :math:\Gamma(1-s)overflowing at :math:s \approx -170.6does not cost a domain the result is finite on.
jax.grad is genuine wherever the eta series or the functional equation
supplies the value, which is everywhere except three sets, all of which
report a finite number that is not :math:\zeta': the tabulated integers
0 >= n >= -59, where a table carries no information about how
:math:\zeta varies between its entries; the negative even integers at
any magnitude, which are a constant 0; and n >= 54, where the value
is the constant 1.0 and the reported derivative is 0 against a true
:math:\zeta'(54) = -3.8\times10^{-17}. The odd integers past the table
are fine -- grad at n = -101 matches :math:\zeta' to 6e-14 --
because those go through the functional equation, which differentiates.
Accuracy is 6e-16 for n > 1, 2.3e-15 on the critical strip
itself and 1e-14 on the window below zero the same series covers, where
it works hardest right against _ETA_FLOOR. On the
negative line it degrades with |n|, because gammaln(1 - n) grows and
the exponential of it carries that magnitude's rounding: 9e-15 out to
n = -10, 2.2e-13 by -100 and 6e-13 by -260, past which
the true value exceeds DBL_MAX and the answer is ±inf -- as it is in
SciPy -- so only the trivial zeros and their neighbours are finite. Just off a
negative even integer, where the sine of the functional equation is near a
zero of its own, it is a few times 1e-13 -- still better than SciPy,
which is 2e-4 there.
Those are the scalar figures. XLA re-associates the 32-term eta sum
differently once there is a batch axis, so an array or jax.jit argument
can differ from the scalar one by up to 1.5e-14 on the strip -- and by
more on the window below zero the same series covers, where a batched call
measures 2.7e-14 against the scalar 8.2e-15. The caveat belongs to
the eta series, not to the strip alone.
Examples:
>>> import jax.numpy as jnp
>>> import spexial as sp
>>> round(float(sp.zeta(2.0)), 10)
1.6449340668
Negative integers use the functional equation:
>>> [round(float(z), 12) for z in sp.zeta(jnp.asarray([0.0, -1.0, -2.0, -3.0]))]
[-0.5, -0.083333333333, 0.0, 0.008333333333]
zeta(-3) == 1 / 120:
>>> float(1 / 120)
0.008333333333333333
Large arguments are exactly 1, where jax.scipy.special.zeta gives nan:
>>> float(sp.zeta(1e16))
1.0