elliptic-functions 4.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
elliptic/__init__.py ADDED
@@ -0,0 +1,101 @@
1
+ """elliptic — Elliptic integrals and functions for NumPy, PyTorch, and JAX.
2
+
3
+ Standalone implementation (no scipy runtime dependency).
4
+ Pass a torch.Tensor or jax.Array for automatic GPU/TPU dispatch.
5
+
6
+ Public API
7
+ ----------
8
+ Incomplete integrals:
9
+ elliptic12(u, m) -> F, E, Z
10
+ elliptic3(u, m, n) -> Pi
11
+ elliptic12i(u, m) -> Fi, Ei, Zi (complex u)
12
+
13
+ Jacobi elliptic functions:
14
+ ellipj(u, m) -> sn, cn, dn, am
15
+ ellipji(u, m) -> sn, cn, dn (complex u)
16
+
17
+ Associate integrals (incomplete):
18
+ ellipticBDJ(phi, m[, n]) -> B, D, J
19
+
20
+ Complete associate integrals:
21
+ ellipticBD(m) -> B, D, S
22
+
23
+ Jacobi-argument forms:
24
+ jacobiEDJ(u, m[, n]) -> Eu, Du, Ju
25
+
26
+ Carlson symmetric forms:
27
+ carlsonRF(x, y, z)
28
+ carlsonRD(x, y, z)
29
+ carlsonRJ(x, y, z, p)
30
+ carlsonRC(x, y)
31
+
32
+ Bulirsch generalised complete integrals:
33
+ cel(kc, p, a, b)
34
+ cel1(kc)
35
+ cel2(kc, a, b)
36
+ cel3(kc, p)
37
+
38
+ Jacobi theta functions:
39
+ jacobiThetaEta(u, m) -> Th, H
40
+ theta(j, v, m) -> Th_j
41
+ theta_prime(j, v, m) -> Th_j, dTh_j/dv
42
+
43
+ Weierstrass functions:
44
+ weierstrassP(z, e1, e2, e3)
45
+ weierstrassZeta(z, e1, e2, e3)
46
+ weierstrassSigma(z, e1, e2, e3)
47
+ weierstrassPPrime(z, e1, e2, e3)
48
+ weierstrassInvariants(e1, e2, e3) -> g2, g3, Delta
49
+
50
+ Nome and inverse:
51
+ nomeq(m) -> q
52
+ inversenomeq(q) -> m
53
+
54
+ Inverse integral:
55
+ inverselliptic2(E, m) -> phi
56
+
57
+ AGM:
58
+ agm(a, b) -> agm(a, b)
59
+
60
+ Applications:
61
+ arclength_ellipse(a, b[, theta0, theta1])
62
+ """
63
+
64
+ from .elliptic12 import elliptic12
65
+ from .elliptic3 import elliptic3
66
+ from .ellipj import ellipj
67
+ from .ellipticBDJ import ellipticBDJ
68
+ from .ellipticBD import ellipticBD
69
+ from .jacobi_edj import jacobiEDJ
70
+ from .carlson import carlsonRF, carlsonRD, carlsonRJ, carlsonRC
71
+ from .bulirsch import cel, cel1, cel2, cel3
72
+ from .weierstrass import (weierstrassP, weierstrassZeta, weierstrassSigma,
73
+ weierstrassPPrime, weierstrassInvariants)
74
+ from .theta import jacobiThetaEta, theta, theta_prime
75
+ from .complex_elliptic import elliptic12i, ellipji
76
+ from .nome import nomeq, inversenomeq
77
+ from .inverse import inverselliptic2
78
+ from .agm import agm
79
+ from .applications import arclength_ellipse
80
+
81
+ __all__ = [
82
+ # integrals
83
+ "elliptic12", "elliptic3", "elliptic12i",
84
+ # Jacobi
85
+ "ellipj", "ellipji",
86
+ # associate
87
+ "ellipticBDJ", "ellipticBD", "jacobiEDJ",
88
+ # Carlson
89
+ "carlsonRF", "carlsonRD", "carlsonRJ", "carlsonRC",
90
+ # Bulirsch
91
+ "cel", "cel1", "cel2", "cel3",
92
+ # theta
93
+ "jacobiThetaEta", "theta", "theta_prime",
94
+ # Weierstrass
95
+ "weierstrassP", "weierstrassZeta", "weierstrassSigma",
96
+ "weierstrassPPrime", "weierstrassInvariants",
97
+ # nome / inverse
98
+ "nomeq", "inversenomeq", "inverselliptic2",
99
+ # misc
100
+ "agm", "arclength_ellipse",
101
+ ]
elliptic/_agm.py ADDED
@@ -0,0 +1,57 @@
1
+ """AGM kernel shared by elliptic12 and ellipj.
2
+
3
+ All loops run a *fixed* 25 iterations so the code is JAX-traceable
4
+ (no dynamic stopping condition). For float64 the AGM converges in at
5
+ most ~25 halvings, so the extra no-op iterations cost nothing.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ _AGM_ITERS = 25 # safe upper bound for float64
10
+
11
+
12
+ def agm_coeffs(m, xp):
13
+ """Compute AGM sequences a, b, c of shape (N, _AGM_ITERS+1).
14
+
15
+ Returns
16
+ -------
17
+ a, b, c : (N, iters+1) arrays
18
+ n : (N,) int array — converged iteration index per element
19
+ """
20
+ N = m.shape[0]
21
+ iters = _AGM_ITERS
22
+
23
+ # Allocate: we'll build columns one by one (works for numpy/torch/jax)
24
+ a = [xp.ones(N, dtype=xp.float64)]
25
+ b = [xp.sqrt(1.0 - m)]
26
+ c = [xp.sqrt(m)]
27
+
28
+ for _ in range(iters):
29
+ a_new = 0.5 * (a[-1] + b[-1])
30
+ b_new = xp.sqrt(a[-1] * b[-1])
31
+ c_new = 0.5 * (a[-1] - b[-1])
32
+ a.append(a_new)
33
+ b.append(b_new)
34
+ c.append(c_new)
35
+
36
+ return a, b, c
37
+
38
+
39
+ def agm_n(c, xp):
40
+ """Return per-element convergence index n from the c sequence."""
41
+ import numpy as _np
42
+ # n[k] = first i where |c[i]| ≈ 0 (i.e., c[i+1] < eps)
43
+ eps = float(xp.finfo(xp.float64).eps) if hasattr(xp, 'finfo') else 2.2e-16
44
+ iters = len(c) - 1
45
+ # Build as numpy for indexing simplicity; convert back if needed
46
+ try:
47
+ c_np = _np.stack([_np.asarray(ci) for ci in c], axis=0) # (iters+1, N)
48
+ except Exception:
49
+ c_np = _np.array([_np.array(ci) for ci in c])
50
+ N = c_np.shape[1]
51
+ n = _np.zeros(N, dtype=_np.intp)
52
+ for i in range(1, iters + 1):
53
+ just_converged = (abs(c_np[i]) <= eps) & (abs(c_np[i - 1]) > eps)
54
+ n[just_converged & (n == 0)] = i - 1
55
+ # anything still 0 gets maximum
56
+ n[n == 0] = iters - 1
57
+ return n
elliptic/_utils.py ADDED
@@ -0,0 +1,86 @@
1
+ """Shared utilities: backend detection, type coercion, parallel chunking."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import math
6
+ from concurrent.futures import ProcessPoolExecutor
7
+ from typing import Any
8
+
9
+ from array_api_compat import array_namespace, is_numpy_array
10
+ import numpy as _np
11
+
12
+
13
+ def get_xp(*args: Any):
14
+ """Return the array namespace for the inputs (numpy, torch, jax.numpy, …).
15
+
16
+ Scalars and Python lists are treated as numpy by default.
17
+ """
18
+ # Convert any non-array args to numpy so array_namespace can detect them
19
+ converted = []
20
+ for a in args:
21
+ if isinstance(a, _np.ndarray):
22
+ converted.append(a)
23
+ else:
24
+ try:
25
+ # torch.Tensor, jax.Array, etc.
26
+ _ = a.shape
27
+ converted.append(a)
28
+ except AttributeError:
29
+ converted.append(_np.asarray(a))
30
+ if not converted:
31
+ return _np
32
+ try:
33
+ return array_namespace(*converted)
34
+ except TypeError:
35
+ return _np
36
+
37
+
38
+ def to_float64(x: Any, xp):
39
+ """Cast *x* to float64 in namespace *xp*, preserving device."""
40
+ return xp.asarray(x, dtype=xp.float64)
41
+
42
+
43
+ def broadcast_arrays(*args, xp):
44
+ """Broadcast all arrays to a common shape using *xp*."""
45
+ import numpy as np # used only for shape computation
46
+ shapes = [xp.asarray(a).shape for a in args]
47
+ try:
48
+ out_shape = np.broadcast_shapes(*shapes)
49
+ except AttributeError: # numpy < 1.20
50
+ out_shape = np.broadcast(*[np.empty(s) for s in shapes]).shape
51
+ return tuple(xp.broadcast_to(xp.asarray(a, dtype=xp.float64), out_shape) for a in args)
52
+
53
+
54
+ # ---------------------------------------------------------------------------
55
+ # Parallel chunking for NumPy path
56
+ # ---------------------------------------------------------------------------
57
+ _PARALLEL_THRESHOLD = 50_000
58
+
59
+
60
+ def _n_workers() -> int:
61
+ return os.cpu_count() or 1
62
+
63
+
64
+ def parallel_apply(fn, *arrays, threshold: int = _PARALLEL_THRESHOLD):
65
+ """Run *fn* across chunks if the input is large enough to benefit.
66
+
67
+ Falls back to serial when the array is small or only 1 CPU is available.
68
+ *fn* must accept flat numpy arrays and return a tuple of flat arrays.
69
+ """
70
+ import numpy as np
71
+ N = arrays[0].size
72
+ n = _n_workers()
73
+ if n <= 1 or N < threshold:
74
+ return fn(*arrays)
75
+ orig_shape = arrays[0].shape
76
+ flat = [a.ravel() for a in arrays]
77
+ chunk = math.ceil(N / n)
78
+ slices = [slice(i * chunk, min((i + 1) * chunk, N)) for i in range(n)]
79
+ chunks = [[f[s] for f in flat] for s in slices]
80
+ with ProcessPoolExecutor(max_workers=n) as pool:
81
+ results = list(pool.map(lambda c: fn(*c), chunks))
82
+ # results is a list of tuples (or single arrays); re-assemble
83
+ if isinstance(results[0], tuple):
84
+ return tuple(np.concatenate([r[i] for r in results]).reshape(orig_shape)
85
+ for i in range(len(results[0])))
86
+ return np.concatenate(results).reshape(orig_shape)
elliptic/_xputils.py ADDED
@@ -0,0 +1,13 @@
1
+ """Array-namespace detection helper shared across all modules."""
2
+ from __future__ import annotations
3
+
4
+ from array_api_compat import array_namespace, is_array_api_obj
5
+ import numpy as np
6
+
7
+
8
+ def get_xp(*args):
9
+ """Return the array namespace for *args*, defaulting to numpy for plain scalars."""
10
+ api_objs = [a for a in args if is_array_api_obj(a)]
11
+ if api_objs:
12
+ return array_namespace(*api_objs)
13
+ return np
elliptic/agm.py ADDED
@@ -0,0 +1,19 @@
1
+ """Arithmetic-geometric mean — native on any array backend."""
2
+ from __future__ import annotations
3
+ from ._xputils import get_xp
4
+
5
+
6
+ def agm(a0, b0):
7
+ """Arithmetic-geometric mean of *a0* and *b0*.
8
+
9
+ Iterates a_{n+1} = (a_n + b_n)/2, b_{n+1} = sqrt(a_n * b_n)
10
+ for 25 fixed steps (safe for float64). Runs natively on NumPy,
11
+ PyTorch CUDA, and JAX.
12
+ """
13
+ xp = get_xp(a0, b0)
14
+ a = xp.asarray(a0, dtype=xp.float64)
15
+ b = xp.asarray(b0, dtype=xp.float64)
16
+ a, b = xp.broadcast_arrays(a, b)
17
+ for _ in range(25):
18
+ a, b = 0.5 * (a + b), xp.sqrt(a * b)
19
+ return a
@@ -0,0 +1,61 @@
1
+ """Application-level helpers built on top of the core elliptic functions."""
2
+ from __future__ import annotations
3
+ import numpy as np
4
+ from array_api_compat import array_namespace
5
+ from .elliptic12 import elliptic12
6
+
7
+
8
+ def arclength_ellipse(a, b, theta0=0.0, theta1=None):
9
+ """Arc length of an ellipse from angle theta0 to theta1.
10
+
11
+ The ellipse is parameterised as x = a cos t, y = b sin t.
12
+ Angle t is measured from the positive a-axis (semi-major or semi-minor).
13
+
14
+ Parameters
15
+ ----------
16
+ a : float
17
+ First semi-axis.
18
+ b : float
19
+ Second semi-axis.
20
+ theta0 : float, optional
21
+ Start angle in radians. Default 0.
22
+ theta1 : float, optional
23
+ End angle in radians. Default 2π (full perimeter).
24
+
25
+ Returns
26
+ -------
27
+ arc : float
28
+ Arc length.
29
+
30
+ Examples
31
+ --------
32
+ Full perimeter of ellipse with a=5, b=10 (matches Mathematica):
33
+
34
+ >>> arclength_ellipse(5, 10) # doctest: +ELLIPSIS
35
+ 48.4422...
36
+
37
+ Notes
38
+ -----
39
+ For a circle (a == b) the formula reduces to a*(theta1 - theta0).
40
+ When b > a the standard formula is used: b * E(theta1|1-(a/b)^2).
41
+ When a > b the complement formula applies: a * E(π/2-theta|1-(b/a)^2).
42
+ """
43
+ if theta1 is None:
44
+ theta1 = 2.0 * np.pi
45
+
46
+ a = float(a); b = float(b)
47
+ theta0 = float(theta0); theta1 = float(theta1)
48
+
49
+ if a == b:
50
+ return a * abs(theta1 - theta0)
51
+
52
+ if b > a:
53
+ m = 1.0 - (a / b) ** 2
54
+ _, E1, _ = elliptic12(np.asarray(theta1), np.asarray(m))
55
+ _, E0, _ = elliptic12(np.asarray(theta0), np.asarray(m))
56
+ return float(b * (float(E1) - float(E0)))
57
+ else: # a > b
58
+ m = 1.0 - (b / a) ** 2
59
+ _, E1, _ = elliptic12(np.asarray(np.pi / 2.0 - theta1), np.asarray(m))
60
+ _, E0, _ = elliptic12(np.asarray(np.pi / 2.0 - theta0), np.asarray(m))
61
+ return float(a * (float(E0) - float(E1)))
elliptic/bulirsch.py ADDED
@@ -0,0 +1,70 @@
1
+ """Bulirsch's generalised complete elliptic integral and special cases.
2
+
3
+ cel(kc, p, a, b) = integral_0^inf (a + b t^2) / ((1 + p t^2) sqrt((1+t^2)(1+kc^2 t^2))) dt
4
+
5
+ Special cases:
6
+ K(m) = cel(sqrt(1-m), 1, 1, 1)
7
+ E(m) = cel(sqrt(1-m), 1, 1, 1-m)
8
+ B(m) = cel(sqrt(1-m), 1, 1, 0)
9
+ D(m) = cel(sqrt(1-m), 1, 0, 1)
10
+ Pi(n|m) = cel(sqrt(1-m), 1-n, 1, 1)
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import math
15
+ import numpy as np
16
+
17
+ from ._xputils import get_xp
18
+ from .ellipticBD import _bd_xp
19
+ from .elliptic12 import _elliptic12_xp
20
+ from .carlson import _rj_xp
21
+
22
+
23
+ def cel(kc, p, a, b):
24
+ """Bulirsch generalised complete elliptic integral."""
25
+ xp = get_xp(kc, p, a, b)
26
+ kc = xp.asarray(kc, dtype=xp.float64)
27
+ p = xp.asarray(p, dtype=xp.float64)
28
+ a = xp.asarray(a, dtype=xp.float64)
29
+ b = xp.asarray(b, dtype=xp.float64)
30
+ kc, p, a, b = xp.broadcast_arrays(kc, p, a, b)
31
+ return _cel_xp(xp, kc, p, a, b)
32
+
33
+
34
+ def _cel_xp(xp, kc, p, a, b):
35
+ m = 1.0 - kc * kc
36
+ phi = xp.full_like(m, math.pi * 0.5)
37
+ K, _, _ = _elliptic12_xp(xp, phi, m)
38
+ B, D, _ = _bd_xp(xp, m)
39
+
40
+ # p ≈ 1 branch: C = a*B + b*D
41
+ C_p1 = a * B + b * D
42
+
43
+ # p ≠ 1 branch: C = a*K + (b - a*p)*(Pi - K)/(1-p)
44
+ n_val = 1.0 - p
45
+ mc = 1.0 - m
46
+ n_safe = xp.where(xp.abs(n_val) < 1e-14, xp.ones_like(n_val), n_val)
47
+ RJ = _rj_xp(xp, xp.zeros_like(m), mc, xp.ones_like(m), p)
48
+ J_n = RJ / 3.0
49
+ Pi_n = K + n_val * J_n
50
+ C_pn = a * K + (b - a * p) * (Pi_n - K) / n_safe
51
+
52
+ C = xp.where(xp.abs(p - 1.0) < 1e-12, C_p1, C_pn)
53
+ C = xp.where(kc < 0.0, xp.full_like(C, math.nan), C)
54
+ C = xp.where(p <= 0.0, xp.full_like(C, math.inf), C)
55
+ return C
56
+
57
+
58
+ def cel1(kc):
59
+ """K(m) via Bulirsch: cel(kc, 1, 1, 1) where m = 1 - kc^2."""
60
+ return cel(kc, 1.0, 1.0, 1.0)
61
+
62
+
63
+ def cel2(kc, a, b):
64
+ """Bulirsch cel2(kc, a, b) = cel(kc, 1, a, b)."""
65
+ return cel(kc, 1.0, a, b)
66
+
67
+
68
+ def cel3(kc, p):
69
+ """Pi(n|m) via Bulirsch: cel(kc, p, 1, 1) where n = 1-p, m = 1-kc^2."""
70
+ return cel(kc, p, 1.0, 1.0)
elliptic/carlson.py ADDED
@@ -0,0 +1,186 @@
1
+ """Carlson symmetric elliptic integrals RF, RD, RJ, RC.
2
+
3
+ All use Carlson's duplication algorithm with fixed iteration counts
4
+ (20 for RF, 30 for RD/RJ) so they are JAX-traceable and run natively on
5
+ any array backend (NumPy, PyTorch CUDA, JAX).
6
+
7
+ References
8
+ ----------
9
+ NIST DLMF §19.36 — https://dlmf.nist.gov/19.36
10
+ B.C. Carlson, Numer. Algorithms 10 (1995), 13–26.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import math
15
+ import numpy as np
16
+
17
+ from ._xputils import get_xp
18
+
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # RC — degenerate RF(x, y, y) (closed-form)
22
+ # ---------------------------------------------------------------------------
23
+
24
+ def carlsonRC(x, y):
25
+ """Carlson RC(x, y) = RF(x, y, y). Closed-form (DLMF 19.2.17)."""
26
+ xp = get_xp(x, y)
27
+ x = xp.asarray(x, dtype=xp.float64)
28
+ y = xp.asarray(y, dtype=xp.float64)
29
+ x, y = xp.broadcast_arrays(x, y)
30
+ return _rc_xp(xp, x, y)
31
+
32
+
33
+ def _rc_xp(xp, x, y):
34
+ EPS = 1e-300
35
+ diff = y - x
36
+
37
+ # safe arguments for each branch (avoid div-by-zero when not selected)
38
+ x_safe = xp.where(x > EPS, x, xp.full_like(x, 1.0))
39
+ yd_safe = xp.where(diff > EPS, diff, xp.full_like(diff, 1.0))
40
+ yd_safe2 = xp.where(-diff > EPS, -diff, xp.full_like(diff, 1.0))
41
+ y_safe = xp.where(y > EPS, y, xp.full_like(y, 1.0))
42
+
43
+ rc_gt = xp.arctan(xp.sqrt(xp.clip(diff / x_safe, 0.0, None))) / xp.sqrt(yd_safe)
44
+ rc_lt = xp.arctanh(xp.sqrt(xp.clip(-diff / x_safe, 0.0, None))) / xp.sqrt(yd_safe2)
45
+ rc_eq = 1.0 / xp.sqrt(x_safe)
46
+ rc_x0 = (math.pi * 0.5) / xp.sqrt(y_safe)
47
+
48
+ TOL = 1e-14
49
+ out = xp.where(diff > TOL, rc_gt, xp.where(diff < -TOL, rc_lt, rc_eq))
50
+ out = xp.where(x < EPS, rc_x0, out)
51
+ out = xp.where(y < EPS, xp.full_like(out, math.inf), out)
52
+ return out
53
+
54
+
55
+ # keep old numpy version for any legacy callers
56
+ def _rc_numpy(x: np.ndarray, y: np.ndarray) -> np.ndarray:
57
+ return _rc_xp(np, x, y)
58
+
59
+
60
+ # ---------------------------------------------------------------------------
61
+ # RF — symmetric first kind
62
+ # ---------------------------------------------------------------------------
63
+
64
+ def carlsonRF(x, y, z):
65
+ """Carlson RF(x, y, z) — symmetric elliptic integral of the first kind.
66
+
67
+ F(phi|m) = sin(phi) * RF(cos²phi, 1 - m sin²phi, 1).
68
+ """
69
+ xp = get_xp(x, y, z)
70
+ x = xp.asarray(x, dtype=xp.float64)
71
+ y = xp.asarray(y, dtype=xp.float64)
72
+ z = xp.asarray(z, dtype=xp.float64)
73
+ x, y, z = xp.broadcast_arrays(x, y, z)
74
+ return _rf_xp(xp, x, y, z)
75
+
76
+
77
+ def _rf_xp(xp, x, y, z):
78
+ for _ in range(20):
79
+ lam = xp.sqrt(x * y) + xp.sqrt(y * z) + xp.sqrt(z * x)
80
+ x = (x + lam) * 0.25
81
+ y = (y + lam) * 0.25
82
+ z = (z + lam) * 0.25
83
+ A = (x + y + z) / 3.0
84
+ X = (A - x) / A
85
+ Y = (A - y) / A
86
+ Z = -X - Y
87
+ E2 = X * Y - Z * Z
88
+ E3 = X * Y * Z
89
+ return A ** (-0.5) * (1.0 - E2/10.0 + E3/14.0 + E2**2/24.0 - 3.0*E2*E3/44.0)
90
+
91
+
92
+ def _rf_numpy(x: np.ndarray, y: np.ndarray, z: np.ndarray) -> np.ndarray:
93
+ return _rf_xp(np, x, y, z)
94
+
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # RD — symmetric second kind
98
+ # ---------------------------------------------------------------------------
99
+
100
+ def carlsonRD(x, y, z):
101
+ """Carlson RD(x, y, z) — symmetric elliptic integral of the second kind.
102
+
103
+ D(phi|m) = (sin³phi/3) * RD(cos²phi, 1 - m sin²phi, 1).
104
+ """
105
+ xp = get_xp(x, y, z)
106
+ x = xp.asarray(x, dtype=xp.float64)
107
+ y = xp.asarray(y, dtype=xp.float64)
108
+ z = xp.asarray(z, dtype=xp.float64)
109
+ x, y, z = xp.broadcast_arrays(x, y, z)
110
+ return _rd_xp(xp, x, y, z)
111
+
112
+
113
+ def _rd_xp(xp, x, y, z):
114
+ S = xp.zeros_like(x)
115
+ fac = xp.ones_like(x)
116
+ for _ in range(30):
117
+ lam = xp.sqrt(x * y) + xp.sqrt(y * z) + xp.sqrt(z * x)
118
+ S = S + fac / (xp.sqrt(z) * (z + lam))
119
+ fac = fac * 0.25
120
+ x = (x + lam) * 0.25
121
+ y = (y + lam) * 0.25
122
+ z = (z + lam) * 0.25
123
+ A = (x + y + 3.0 * z) / 5.0
124
+ X = (A - x) / A
125
+ Y = (A - y) / A
126
+ Z = -(X + Y) / 3.0
127
+ E2 = X * Y - 6.0 * Z**2
128
+ E3 = (3.0 * X * Y - 8.0 * Z**2) * Z
129
+ E4 = 3.0 * (X * Y - Z**2) * Z**2
130
+ E5 = X * Y * Z**3
131
+ poly = (1.0 - 3.0*E2/14.0 + E3/6.0 + 9.0*E2**2/88.0
132
+ - 3.0*E4/22.0 - 9.0*E2*E3/52.0 + 3.0*E5/26.0)
133
+ return 3.0 * S + fac * A**(-1.5) * poly
134
+
135
+
136
+ def _rd_numpy(x: np.ndarray, y: np.ndarray, z: np.ndarray) -> np.ndarray:
137
+ return _rd_xp(np, x, y, z)
138
+
139
+
140
+ # ---------------------------------------------------------------------------
141
+ # RJ — symmetric third kind
142
+ # ---------------------------------------------------------------------------
143
+
144
+ def carlsonRJ(x, y, z, p):
145
+ """Carlson RJ(x, y, z, p) — symmetric elliptic integral of the third kind.
146
+
147
+ J(phi,n|m) = (sin³phi/3) * RJ(cos²phi, 1-m sin²phi, 1, 1-n sin²phi).
148
+ """
149
+ xp = get_xp(x, y, z, p)
150
+ x = xp.asarray(x, dtype=xp.float64)
151
+ y = xp.asarray(y, dtype=xp.float64)
152
+ z = xp.asarray(z, dtype=xp.float64)
153
+ p = xp.asarray(p, dtype=xp.float64)
154
+ x, y, z, p = xp.broadcast_arrays(x, y, z, p)
155
+ return _rj_xp(xp, x, y, z, p)
156
+
157
+
158
+ def _rj_xp(xp, x, y, z, p):
159
+ S = xp.zeros_like(x)
160
+ fac = xp.ones_like(x)
161
+ for _ in range(30):
162
+ lam = xp.sqrt(x * y) + xp.sqrt(y * z) + xp.sqrt(z * x)
163
+ alpha = (p * (xp.sqrt(x) + xp.sqrt(y) + xp.sqrt(z)) + xp.sqrt(x * y * z)) ** 2
164
+ beta = p * (p + lam) ** 2
165
+ S = S + fac * _rc_xp(xp, alpha, beta)
166
+ fac = fac * 0.25
167
+ x = (x + lam) * 0.25
168
+ y = (y + lam) * 0.25
169
+ z = (z + lam) * 0.25
170
+ p = (p + lam) * 0.25
171
+ A = (x + y + z + 2.0 * p) / 5.0
172
+ X = (A - x) / A
173
+ Y = (A - y) / A
174
+ Z = (A - z) / A
175
+ P = -(X + Y + Z) / 2.0
176
+ E2 = X*Y + X*Z + Y*Z - 3.0*P**2
177
+ E3 = X*Y*Z + 2.0*E2*P + 3.0*P**3
178
+ E4 = (2.0*X*Y*Z + E2*P + 3.0*P**3) * P
179
+ E5 = X*Y*Z * P**2
180
+ poly = (1.0 - 3.0*E2/14.0 + E3/6.0 + 9.0*E2**2/88.0
181
+ - 3.0*E4/22.0 - 9.0*E2*E3/52.0 + 3.0*E5/26.0)
182
+ return 3.0 * S + fac * A**(-1.5) * poly
183
+
184
+
185
+ def _rj_numpy(x: np.ndarray, y: np.ndarray, z: np.ndarray, p: np.ndarray) -> np.ndarray:
186
+ return _rj_xp(np, x, y, z, p)