thermite_special/specialized/generic/jacobi_elliptic.rs
1//! The Jacobi elliptic functions `sn`, `cn` and `dn`.
2//!
3//! # What they are
4//!
5//! All three are built from one quantity, the **amplitude** `$\varphi = \mathrm{am}(u, k)$`,
6//! defined as the angle whose incomplete elliptic integral of the first kind is `u`
7//! (`$F(\varphi, k) = u$`, see [`ellint_impl`](super::elliptic::ellint_impl)). Then
8//!
9//! ```math
10//! \mathrm{sn}(u, k) = \sin\varphi, \qquad
11//! \mathrm{cn}(u, k) = \cos\varphi, \qquad
12//! \mathrm{dn}(u, k) = \sqrt{1 - k^2\sin^2\varphi}
13//! ```
14//!
15//! Hence the names: sine amplitude, cosine amplitude, delta amplitude. At `k = 0` the
16//! amplitude is `u` itself and they degenerate to `sin u`, `cos u` and `1`. At `k = 1` they
17//! become `tanh u`, `sech u` and `sech u`.
18//!
19//! They are returned together because they are a closed system, not merely because it is
20//! cheaper: differentiating any one of them produces a product of the other two
21//! (`$\mathrm{sn}' = \mathrm{cn}\,\mathrm{dn}$`,
22//! `$\mathrm{cn}' = -\mathrm{sn}\,\mathrm{dn}$`,
23//! `$\mathrm{dn}' = -k^2\mathrm{sn}\,\mathrm{cn}$`), exactly the way `sin` and `cos` close
24//! under differentiation. The other nine Jacobi functions in Glaisher's notation (`ns`,
25//! `nc`, `nd`, `sc`, `sd`, `cs`, `cd`, `ds`, `dc`) are reciprocals and ratios of these
26//! three, so a caller holding the triple holds all twelve.
27//!
28//! # Algorithm
29//!
30//! The descending Landen transformation, in the arithmetic-only form due to Bulirsch
31//! (1965) rather than the textbook one. Both walk the same AGM ladder down from `k` to
32//! modulus zero and then climb back up, but they differ in what the climb costs:
33//!
34//! - The textbook descent (A&S 16.4, and Boost's `jacobi_elliptic`) carries an _angle_
35//! back up, `$\varphi_{n-1} = \tfrac12(\varphi_n + \arcsin(\tfrac{c_n}{a_n}\sin\varphi_n))$`.
36//! That is one `sin` and one `asin` per level, on a ladder several levels deep, the
37//! worst possible shape for a vector unit, where every lane pays for both.
38//! - Bulirsch carries the _tangent_ of the angle instead. The half-angle step becomes
39//! rational, so the entire climb is multiplies and divides, and the whole function needs
40//! exactly **one `sin_cos`**, at the bottom of the ladder where the modulus is zero and
41//! the amplitude is just the argument.
42//!
43//! Measured against mpmath at 40 digits over `k` in `[0, 1)` and `|u| <= 8`, worst absolute
44//! error 8.3 eps for `sn`, 4.1 for `cn`, 3.8 for `dn`. Absolute is the honest metric here:
45//! all three are bounded by 1 and all three have zeros, so relative error at a zero is
46//! governed by how well the zero's location is known, exactly as for `sin`.
47//!
48//! Accuracy degrades with `|u|` the way `sin`'s does and for the same reason: the one
49//! trig call takes `u` scaled by the AGM limit, so a large `|u|` is a large argument to
50//! reduce. The error above was measured to `|u| = 8`, and grows slowly beyond that.
51//!
52//! # The ladder is bounded, and short
53//!
54//! `$k' = \sqrt{1 - k^2}$` is what the AGM starts from, and for any `k` strictly below 1 in
55//! binary64 the cancellation-free `one_minus_sq` bottoms out at `$2^{-52}$`, so `k'` never
56//! falls below about `1.5e-8` and the ladder is never deeper than 8 rungs (measured, 4 to 6
57//! is typical). [`NMAX`] carries two rungs of margin on top of that.
58//!
59//! Lanes converge at different depths, so the loop runs until _every_ lane has converged
60//! and the climb then runs the full depth for all of them. That is safe: past convergence
61//! `$a_n = b_n$`, so the extra rungs are identity transformations, and running them for
62//! every lane unconditionally was measured to give bit-identical results to stopping each
63//! lane at its own depth.
64
65use thermite::{
66 element::FloatElement,
67 math::{
68 CoreMathWithPolicy as _, TranscendentalMathWithPolicy as _, policy::Policy,
69 specialized::SpecializedTranscendentalMath,
70 },
71 prelude::*,
72};
73
74/// Maximum depth of the AGM ladder.
75///
76/// Eight rungs is the measured worst case over the whole domain, reached only as `k`
77/// approaches 1. Two more are carried as margin. The forward pass stops as soon as every
78/// lane has converged, so this is a bound and not a trip count.
79pub const NMAX: usize = 10;
80
81/// `(sn, cn, dn)` at argument `u` and modulus `k`.
82///
83/// Only `$k^2$` enters, so the sign of `k` is irrelevant and `|k| > 1` is out of domain:
84/// `$1 - k^2$` goes negative, its square root is NaN, and the NaN propagates on its own
85/// without a guard. `k = 1` is the one modulus the ladder cannot walk (it starts at
86/// `$k' = 0$` and never converges), and is taken by the hyperbolic limit instead.
87#[inline(always)]
88pub fn jacobi_elliptic<P, E, V>(u: V, k: V) -> (V, V, V)
89where
90 E: FloatElement,
91 V: FloatVector<Element = E> + SpecializedTranscendentalMath<E>,
92 P: Policy,
93{
94 let half = V::HALF;
95
96 // The AGM ladder on (a, b) = (1, k'), recording both sequences: the climb needs every
97 // rung, and `b` cannot be recovered from `a` alone (b_i = 2 a_{i+1} - a_i loses all of
98 // b_0's digits when k' is small, which is exactly the case that needs the depth).
99 let mut asq = [V::ZERO; NMAX];
100 let mut bsq = [V::ZERO; NMAX];
101
102 let mut a = V::ONE;
103 let mut b = k.one_minus_sq(); // k'^2, cancellation-free as |k| -> 1
104 let mut c = V::ONE;
105
106 // Same threshold and same pre-update gap test as `agm_complete_ke`: the limit sits near
107 // the midpoint of the pair, so the gap going _into_ a rung is what bounds the error
108 // coming out of it. See the load-bearing note on that function.
109 let thresh = V::SQRT_EPSILON;
110 let mut depth = NMAX - 1;
111 for i in 0..NMAX {
112 V::_loop_hint();
113
114 asq[i] = a;
115 b = b.sqrt();
116 bsq[i] = b;
117 c = (a + b) * half;
118
119 if (a - b).abs().cmp_le(a * thresh).all() {
120 depth = i;
121 break;
122 }
123
124 b *= a;
125 a = c;
126 }
127
128 // Bottom of the ladder: modulus zero, where the amplitude is the argument itself. This
129 // is the only transcendental in the function.
130 let (sin_u, cos_u) = (u * c).sin_cos_p::<P>();
131
132 // The climb carries t = cot(amplitude) rather than the amplitude, which is what keeps it
133 // rational. A zero sine is a pole of the cotangent. Those lanes are recovered at the end,
134 // so all this has to do is keep the division finite.
135 let at_zero = sin_u.is_zero();
136 let sin_safe = at_zero.select(V::ONE, sin_u);
137
138 let mut t = cos_u.approx_div_p::<P>(sin_safe);
139 let mut w = c * t;
140 let mut dn = V::ONE;
141
142 let mut i = depth;
143 loop {
144 V::_loop_hint();
145
146 let ai = asq[i];
147 t *= w;
148 w *= dn;
149 dn = (bsq[i] + t).approx_div_p::<P>(ai + t);
150 t = w.approx_div_p::<P>(ai);
151
152 if i == 0 {
153 break;
154 }
155 i -= 1;
156 }
157
158 // sin and cos recovered from the cotangent: |sn| = 1/sqrt(w^2 + 1), and the sign is the
159 // one the bottom-of-ladder sine already carried.
160 let mag = w.mul_adde(w, V::ONE).inverse_sqrt_p::<P>();
161 let mut sn = mag.copysign(sin_u);
162 let mut cn = w * sn;
163
164 // Where the amplitude's sine vanished, the triple is (0, +-1, 1) exactly: the cotangent
165 // route cannot produce it, and cos_u is already the correct +-1.
166 sn = at_zero.select(V::ZERO, sn);
167 cn = at_zero.select(cos_u, cn);
168 dn = at_zero.select(V::ONE, dn);
169
170 if const { P::POLICY.check_overflow } {
171 // k = 1 makes k' = 0: the ladder starts at its own fixed point and never converges,
172 // so the limit is substituted whole. sn -> tanh, cn and dn -> sech, and the three
173 // stop being periodic.
174 let unit = k.abs().cmp_eq(V::ONE);
175 if const { P::POLICY.avoid_branching } || thermite::unlikely(unit.any()) {
176 let sech = V::ONE.approx_div_p::<P>(u.cosh_p::<P>());
177 sn = unit.select(u.tanh_p::<P>(), sn);
178 cn = unit.select(sech, cn);
179 dn = unit.select(sech, dn);
180 }
181 }
182
183 (sn, cn, dn)
184}