Skip to content

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) (compare mpmath.polylog).
  • eval_gegenbauers, which returns the Gegenbauer polynomial of degree n and 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. z == 0 is the pole and gives inf; z < 0 is outside the domain and gives nan; z = inf gives 0, the limit.

required

Returns:

Type Description
Array

Value(s) of :math:K_0(z), accurate to ~2.0e-7 relative (worst at z = 8.9984, just below the z = 9 cross-over; ~8e-9 out to z = 15, ~2e-13 to z = 30, and ~1e-15 beyond). Those are float64 figures: in float32 the cross-over moves to 4.65 and the worst error is ~7e-3 (see _SMALL_Z). float16 and bfloat16 are computed in float32 and rounded back, so they get what their dtype can hold. Underflows to 0 where the true value falls below the dtype's smallest normal, which is a different place in each: 705.3 in float64, 85.3 in float32, 85.2 in bfloat16 and 16.1 in float16. Use k0e above it.

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. z == 0 is the pole and gives inf; z < 0 is outside the domain and gives nan; z = inf gives 0, the limit.

required

Returns:

Type Description
Array

Value(s) of :math:e^z K_0(z), accurate to ~2.0e-7 relative (worst at z = 8.9984, just below the z = 9 cross-over; ~8e-9 out to z = 15, ~2e-13 to z = 30, and ~1e-15 beyond). Those are float64 figures: in float32 the cross-over moves to 4.65 and the worst error is ~7e-3 (see _SMALL_Z). float16 and bfloat16 are computed in float32 and rounded back, so they get what their dtype can hold.

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. z == 0 is the pole and gives inf; z < 0 is outside the domain and gives nan; z = inf gives 0, the limit.

required

Returns:

Type Description
Array

Value(s) of :math:K_1(z), accurate to ~1.8e-7 relative (worst at z = 8.9984, just below the z = 9 cross-over; ~8e-9 out to z = 15, ~2e-13 to z = 30, and ~1e-15 beyond). Those are float64 figures: in float32 the cross-over moves to 4.65 and the worst error is ~7e-3 (see _SMALL_Z). float16 and bfloat16 are computed in float32 and rounded back, so they get what their dtype can hold. Underflows to 0 where the true value falls below the dtype's smallest normal, which is a different place in each: 705.3 in float64, 85.3 in float32, 85.2 in bfloat16 and 16.2 in float16. Use k1e above it.

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. z == 0 is the pole and gives inf; z < 0 is outside the domain and gives nan; z = inf gives 0, the limit.

required

Returns:

Type Description
Array

Value(s) of :math:e^z K_1(z), accurate to ~1.8e-7 relative (worst at z = 8.9984, just below the z = 9 cross-over; ~8e-9 out to z = 15, ~2e-13 to z = 30, and ~1e-15 beyond). Those are float64 figures: in float32 the cross-over moves to 4.65 and the worst error is ~7e-3 (see _SMALL_Z). float16 and bfloat16 are computed in float32 and rounded back, so they get what their dtype can hold.

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. z == 0 is the pole and gives inf; z < 0 is outside the domain and gives nan; z = inf gives 0, the limit.

required

Returns:

Type Description
Array

Value(s) of :math:K_2(z), accurate to ~1.3e-7 relative (worst at z = 8.9984, just below the z = 9 cross-over; ~8e-9 out to z = 15, ~2e-13 to z = 30, and ~1e-15 beyond). Those are float64 figures: in float32 the cross-over moves to 4.65 and the worst error is ~7e-3 (see _SMALL_Z). float16 and bfloat16 are computed in float32 and rounded back, so they get what their dtype can hold. Underflows to 0 where the true value falls below the dtype's smallest normal, which is a different place in each: 705.3 in float64, 85.4 in float32, 85.2 in bfloat16 and 16.3 in float16. Use k2e above it.

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. z == 0 is the pole and gives inf; z < 0 is outside the domain and gives nan; z = inf gives 0, the limit.

required

Returns:

Type Description
Array

Value(s) of :math:e^z K_2(z), accurate to ~1.3e-7 relative (worst at z = 8.9984, just below the z = 9 cross-over; ~8e-9 out to z = 15, ~2e-13 to z = 30, and ~1e-15 beyond). Those are float64 figures: in float32 the cross-over moves to 4.65 and the worst error is ~7e-3 (see _SMALL_Z). float16 and bfloat16 are computed in float32 and rounded back, so they get what their dtype can hold.

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 int and >= 1. A non-integer order -- including a whole-number float such as polylog(2.0, z) -- is rejected by the runtime type checker with a TypeError; an integer below 1 raises ValueError.

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:\mathrm{Li}_n(z). For z > 1 the polylogarithm is genuinely complex; only its real part is returned.

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 k.

required
k Shaped[Array, ...] | Shaped[ndarray, ...] | bool | number | bool | int | float | complex

Number of elements taken. Broadcast against N.

required

Returns:

Type Description
Array

The total number of combinations. Following scipy.special.comb, this is 0 wherever k > N, k < 0, or N < 0.

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 int; non-integer degrees are not supported yet.

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:C_n^{(\alpha)}(x).

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 int; non-integer degrees are not supported yet.

required
alpha Shaped[Array, ...] | Shaped[ndarray, ...] | bool | number | bool | int | float | complex

Parameter. Broadcast against x.

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:C_i^{(\alpha)}(x) for i = 0 ... n, stacked on a leading axis over the broadcast shape of alpha and x. Scalar arguments therefore give (n + 1,), as before.

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 jax >= 0.10.2; below that jax.scipy.special.gamma raises, since it branches on floor(x) internally.

required

Returns:

Type Description
Array

Value(s) of the gamma function. x = 0 gives inf; the negative integers give nan, matching both jax.scipy.special.gamma and scipy.special.gamma from 1.18.

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:[0, 1]. Any shape.

required

Returns:

Type Description
Array

Shaped like z.

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. z == 0 is the pole and gives inf; z < 0 is outside the domain and gives nan; z = inf gives 0, the limit.

required

Returns:

Type Description
Array

Value(s) of :math:K_0(z), accurate to ~2.0e-7 relative (worst at z = 8.9984, just below the z = 9 cross-over; ~8e-9 out to z = 15, ~2e-13 to z = 30, and ~1e-15 beyond). Those are float64 figures: in float32 the cross-over moves to 4.65 and the worst error is ~7e-3 (see _SMALL_Z). float16 and bfloat16 are computed in float32 and rounded back, so they get what their dtype can hold. Underflows to 0 where the true value falls below the dtype's smallest normal, which is a different place in each: 705.3 in float64, 85.3 in float32, 85.2 in bfloat16 and 16.1 in float16. Use k0e above it.

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. z == 0 is the pole and gives inf; z < 0 is outside the domain and gives nan; z = inf gives 0, the limit.

required

Returns:

Type Description
Array

Value(s) of :math:e^z K_0(z), accurate to ~2.0e-7 relative (worst at z = 8.9984, just below the z = 9 cross-over; ~8e-9 out to z = 15, ~2e-13 to z = 30, and ~1e-15 beyond). Those are float64 figures: in float32 the cross-over moves to 4.65 and the worst error is ~7e-3 (see _SMALL_Z). float16 and bfloat16 are computed in float32 and rounded back, so they get what their dtype can hold.

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. z == 0 is the pole and gives inf; z < 0 is outside the domain and gives nan; z = inf gives 0, the limit.

required

Returns:

Type Description
Array

Value(s) of :math:K_1(z), accurate to ~1.8e-7 relative (worst at z = 8.9984, just below the z = 9 cross-over; ~8e-9 out to z = 15, ~2e-13 to z = 30, and ~1e-15 beyond). Those are float64 figures: in float32 the cross-over moves to 4.65 and the worst error is ~7e-3 (see _SMALL_Z). float16 and bfloat16 are computed in float32 and rounded back, so they get what their dtype can hold. Underflows to 0 where the true value falls below the dtype's smallest normal, which is a different place in each: 705.3 in float64, 85.3 in float32, 85.2 in bfloat16 and 16.2 in float16. Use k1e above it.

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. z == 0 is the pole and gives inf; z < 0 is outside the domain and gives nan; z = inf gives 0, the limit.

required

Returns:

Type Description
Array

Value(s) of :math:e^z K_1(z), accurate to ~1.8e-7 relative (worst at z = 8.9984, just below the z = 9 cross-over; ~8e-9 out to z = 15, ~2e-13 to z = 30, and ~1e-15 beyond). Those are float64 figures: in float32 the cross-over moves to 4.65 and the worst error is ~7e-3 (see _SMALL_Z). float16 and bfloat16 are computed in float32 and rounded back, so they get what their dtype can hold.

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. z == 0 is the pole and gives inf; z < 0 is outside the domain and gives nan; z = inf gives 0, the limit.

required

Returns:

Type Description
Array

Value(s) of :math:K_2(z), accurate to ~1.3e-7 relative (worst at z = 8.9984, just below the z = 9 cross-over; ~8e-9 out to z = 15, ~2e-13 to z = 30, and ~1e-15 beyond). Those are float64 figures: in float32 the cross-over moves to 4.65 and the worst error is ~7e-3 (see _SMALL_Z). float16 and bfloat16 are computed in float32 and rounded back, so they get what their dtype can hold. Underflows to 0 where the true value falls below the dtype's smallest normal, which is a different place in each: 705.3 in float64, 85.4 in float32, 85.2 in bfloat16 and 16.3 in float16. Use k2e above it.

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. z == 0 is the pole and gives inf; z < 0 is outside the domain and gives nan; z = inf gives 0, the limit.

required

Returns:

Type Description
Array

Value(s) of :math:e^z K_2(z), accurate to ~1.3e-7 relative (worst at z = 8.9984, just below the z = 9 cross-over; ~8e-9 out to z = 15, ~2e-13 to z = 30, and ~1e-15 beyond). Those are float64 figures: in float32 the cross-over moves to 4.65 and the worst error is ~7e-3 (see _SMALL_Z). float16 and bfloat16 are computed in float32 and rounded back, so they get what their dtype can hold.

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 int and >= 1. A non-integer order -- including a whole-number float such as polylog(2.0, z) -- is rejected by the runtime type checker with a TypeError; an integer below 1 raises ValueError.

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:\mathrm{Li}_n(z). For z > 1 the polylogarithm is genuinely complex; only its real part is returned.

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 abs(m) <= n. Static Python ints; unlike scipy.special.sph_harm_y they may not be arrays.

required
m int

Degree and order, with abs(m) <= n. Static Python ints; unlike scipy.special.sph_harm_y they may not be arrays.

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 theta.

required

Returns:

Type Description
Array

Complex, of the broadcast shape of theta and phi.

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 abs(m) <= n. Static Python ints.

required
m int

Degree and order, with abs(m) <= n. Static Python ints.

required
uvec Real[Array, ...] | Real[ndarray, ...] | number | int | float

Cartesian direction, shape (..., 3). Assumed already normalized; this deliberately does not normalize, both to avoid repeating a caller's own work and because the caller owns the policy at :math:r = 0. A zero vector is well defined and finite here -- it gives :math:Y_0^0 = N_{00} and zero for every :math:m \ge 1 -- which normalizing internally would turn into nan.

required

Returns:

Type Description
Array

Complex, shaped like uvec without its trailing axis.

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, 0 <= m <= n. Static: it sets the second axis.

required
uvec Real[Array, ...] | Real[ndarray, ...] | number | int | float

Cartesian direction, shape (..., 3), assumed normalized. See sph_harm_y_cart on why this does not normalize.

required

Returns:

Type Description
Array

Complex, shape (n + 1, 2 * m + 1, ...), where entry [i, j] is :math:Y_i^j for :math:0 \le i \le n and :math:-m \le j \le m -- so negative orders live at the end of the second axis, reachable by ordinary negative indexing, exactly as in SciPy. Entries with :math:\lvert j \rvert > i are zero, since no such harmonic exists.

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, 0 <= m <= n. Static.

required
uvec Real[Array, ...] | Real[ndarray, ...] | number | int | float

Cartesian direction, shape (..., 3), assumed normalized. See sph_harm_y_cart on why this does not normalize.

required

Returns:

Type Description
tuple[tuple[Array, ...], ...]

n + 1 rows of 2 * m + 1 complex arrays, each shaped like uvec without its trailing axis. Entries with |j| > i are zero.

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 abs(m) <= n. Static Python ints, as for eval_gegenbauer; unlike scipy.special.sph_legendre_p they may not be arrays, and the returned shape is therefore theta's own rather than a broadcast against them.

required
m int

Degree and order, with abs(m) <= n. Static Python ints, as for eval_gegenbauer; unlike scipy.special.sph_legendre_p they may not be arrays, and the returned shape is therefore theta's own rather than a broadcast against them.

required
theta Real[Array, ...] | Real[ndarray, ...] | number | int | float

Polar angle in radians, of any shape.

required

Returns:

Type Description
Array

Shaped like theta.

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 what UP or DOWN alone would (within ~30%). Downward is the dearer of the two, 1.7-4.6x upward for n >= 100 on CPU, so a batch that straddles the turning point costs 3-5.3x SphericalJnRecurrence.UP -- more at small n, where upward is nearly free: 7x in float64 and 12x in float32 at n = 10. Under jax.vmap every batch pays that straddling price, as the check is made per element. It takes 2-3.5x as long as UP to compile.
  • SphericalJnRecurrence.UP is 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.DOWN is for arguments known to be below the turning point, e.g. small :math:kr in a large multipole. It is as accurate as SphericalJnRecurrence.BOTH there, skips the upward pass, and returns nan at :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 int.

required
z Shaped[Array, ...] | Shaped[ndarray, ...] | bool | number | bool | int | float | complex

Real argument, of any shape. Evaluated elementwise.

required
derivative bool

If True, return :math:j_n'(z) instead.

False
recurrence SphericalJnRecurrence | Literal['both', 'up', 'down']

Which recurrence to run: a SphericalJnRecurrence, or its string value ("both", "up", "down"). Static.

<SphericalJnRecurrence.BOTH: 'both'>

Returns:

Type Description
Array

Value(s) of :math:j_n(z) or :math:j_n'(z). For :math:|z| \ge n the error is below 1e-12 of :math:\sqrt{j_n^2 + y_n^2} for :math:n \le 10^4, growing roughly as :math:n beyond (in float32, :math:\max(10^{-5}, 10^{-7} n) of the peak). Below the turning point, SphericalJnRecurrence.BOTH and SphericalJnRecurrence.DOWN are accurate relatively, down to the smallest representable values: for :math:n \le 1000, 5e-13 in float64 (1e-12 for the derivative) and 5e-5 in float32. SphericalJnRecurrence.UP is accurate there only absolutely, to 1e-6 of :math:\max_z |j_n(z)| in float64 for :math:n \le 10^4 (3e-6 for the derivative).

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 int.

required
z Shaped[Array, ...] | Shaped[ndarray, ...] | bool | number | bool | int | float | complex

Real argument, of any shape.

required
derivative bool

If True, return :math:j_l'(z) instead.

False
recurrence SphericalJnRecurrence | Literal['both', 'up', 'down']

As spherical_jn.

<SphericalJnRecurrence.BOTH: 'both'>

Returns:

Type Description
Array[float, (n + 1, ...)]

The orders 0 ... n stacked on a new leading axis.

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:\zeta(n), or nan outside the supported domain (see below). n = 1 is the pole and gives inf.

Notes

Every real n is covered, by whichever of four methods is accurate there:

  • n >= 54 -- exactly 1.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 avoids jax.scipy.special.zeta, which returns nan for n above about 1e15.
  • n > 1 -- delegated to jax.scipy.special.zeta.
  • n = 1 -- the pole, inf.
  • -0.5 < n < 1 -- Borwein's acceleration of the eta series. This is the critical strip, which jax.scipy.special.zeta does not implement, plus a little below zero where the reflection below cannot be used.
  • n a negative even integer -- exactly 0, at any magnitude.
  • n a negative integer down to -59 -- from the tabulated :math:B_{1-n}, which is exact to the ulp. -60 is 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.6 does 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