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 +101 -0
- elliptic/_agm.py +57 -0
- elliptic/_utils.py +86 -0
- elliptic/_xputils.py +13 -0
- elliptic/agm.py +19 -0
- elliptic/applications.py +61 -0
- elliptic/bulirsch.py +70 -0
- elliptic/carlson.py +186 -0
- elliptic/complex_elliptic.py +142 -0
- elliptic/ellipj.py +77 -0
- elliptic/elliptic12.py +102 -0
- elliptic/elliptic3.py +67 -0
- elliptic/ellipticBD.py +54 -0
- elliptic/ellipticBDJ.py +74 -0
- elliptic/inverse.py +62 -0
- elliptic/jacobi_edj.py +48 -0
- elliptic/nome.py +75 -0
- elliptic/theta.py +250 -0
- elliptic/weierstrass.py +209 -0
- elliptic_functions-4.0.0.dist-info/METADATA +160 -0
- elliptic_functions-4.0.0.dist-info/RECORD +22 -0
- elliptic_functions-4.0.0.dist-info/WHEEL +4 -0
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
|
elliptic/applications.py
ADDED
|
@@ -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)
|