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', '__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', '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.dev2' 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'.

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

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