use num_traits::Float;
use crate::{
bulirsch::{constants::BulirschConst, N_MAX_ITERATIONS},
crate_util::{check, declare},
ellipeinc, ellipf, ellippi, ellippiinc, StrErr,
};
pub fn el3<T: Float>(x: T, kc: T, p: T) -> Result<T, StrErr> {
if core::mem::size_of::<T>() <= 4 {
el3_with_const::<T, f32>(x, kc, p)
} else {
el3_with_const::<T, f64>(x, kc, p)
}
}
#[numeric_literals::replace_float_literals(T::from(literal).unwrap())]
#[inline]
pub fn el3_with_const<T: Float, C: BulirschConst<T>>(x: T, kc: T, p: T) -> Result<T, StrErr> {
let m = 1.0 - kc * kc;
let n = 1.0 - p;
if kc.abs() < epsilon!() {
return Err("el3: kc must not be zero.");
}
if x == 0.0 {
return Ok(0.0);
}
if x == inf!() {
return ellippi(n, m);
}
if kc == 1.0 {
if p == 0.0 {
return Ok(x);
}
if p > 0.0 {
let vcr = p.sqrt();
return Ok((vcr * x).atan() / vcr);
} else {
let vcr = (-p).sqrt();
let arg = vcr * x;
if arg.abs() == 1.0 {
return Err("el3: 1 + px² cannot be zero.");
}
let log_ratio = if arg.abs() < 1.0 {
arg.ln_1p() - (-arg).ln_1p()
} else {
(1.0 / arg).ln_1p() - (-1.0 / arg).ln_1p()
};
return Ok(log_ratio / (2.0 * vcr));
}
}
if p == 0.0 && m != 1.0 {
let phi = x.atan();
let sp2 = phi.sin() * phi.sin();
let mut result = (1.0 - m * sp2).sqrt() * x - ellipeinc(phi, m)?;
result = result / (1.0 - m);
result = result + ellipf(phi, m)?;
return Ok(result);
}
if kc.abs() < C::lim_kc_p() && p > C::lim_kc_p() {
let phi = x.atan();
return ellippiinc(phi, n, m);
}
let x_abs = x.abs();
declare!(mut [c, d, de, e, f, fa, g, h, hh]);
declare!(mut [pm, pz, q, r, s = kc, t, u, v, w, y, ye = T::zero(), z]);
let zd;
declare!(mut [k = 0, l, mm, nn]);
declare!(mut [bo, bk = false]);
hh = x * x;
f = p * hh;
t = s * s;
pm = t * 0.5;
e = hh * t;
z = f.abs();
r = p.abs();
h = 1.0 + hh;
if e < 0.1 && z < 0.1 && t < 1.0 && r < 1.0 {
declare!(mut [rb, ra, rr] = [T::zero(); MAX_ND]);
for k in 2..=C::ND {
let k_float = T::from(k).unwrap();
rb[k - 2] = 0.5 / k_float;
ra[k - 2] = 1.0 - rb[k - 2];
}
zd = 0.5 / (T::from(C::ND).unwrap() + 1.0);
s = p + pm;
for k in 0..C::ND - 2 {
rr[k] = s;
pm = pm * t * ra[k];
s = s * p + pm;
}
s = s * zd;
u = s;
bo = false;
for k in (0..C::ND - 2).rev() {
u = u + (rr[k] - u) * rb[k];
bo = !bo;
let v = if bo { -u } else { u };
s = s * hh + v;
}
if bo {
s = -s;
}
u = 0.5 * (u + 1.0);
return Ok((u - s * h) * h.sqrt() * x + u * x.asinh());
}
w = 1.0 + f;
if w == 0.0 {
return Err("el3: 1 + px² cannot be zero.");
}
let p1 = p;
s = s.abs();
y = x_abs;
g = if p == 1.0 { C::cb() } else { p1 - 1.0 };
f = p1 - t;
if f == 0.0 {
f = C::cb() * t;
}
let am = 1.0 - t;
let ap = 1.0 + e;
r = p1 * h;
fa = g / (f * p1);
bo = fa > 0.0;
fa = fa.abs();
pz = (g * f).abs();
de = pz.sqrt();
q = p1.abs().sqrt();
pm = pm.min(0.5);
pm = p1 - pm;
if pm >= 0.0 {
u = (r * ap).sqrt();
v = y * de;
if g < 0.0 {
v = -v;
}
d = 1.0 / q;
c = 1.0;
} else {
u = (h * ap * pz).sqrt();
ye = y * q;
v = am * ye;
q = -de / g;
d = -am / de;
c = 0.0;
pz = ap - r;
}
if bo {
r = v / u;
z = 1.0;
k = 1;
if pm < 0.0 {
h = y * (h / ap / fa).sqrt();
h = 1.0 / h - h;
z = h - r - r;
r = 2.0 + r * h;
z = r / z;
r = z;
w = pz;
}
u = u / w;
v = v / w;
} else {
t = u + v.abs();
bk = true;
if p1 < 0.0 {
de = v / pz;
ye = u * ye;
ye = ye + ye;
u = t / pz;
v = (-f - g * e) / t;
t = pz * w.abs();
z = (hh * r * f - g * ap + ye) / t;
ye = ye / t;
} else {
de = v / w;
ye = 0.0;
u = (e + p1) / t;
v = t / w;
z = 1.0;
}
if s > 1.0 {
h = u;
u = v;
v = h;
}
}
y = 1.0 / y;
e = s;
nn = 1;
t = 1.0;
l = 0;
mm = 0;
for _ in 0..N_MAX_ITERATIONS {
y = y - e / y;
f = c;
c = d / q + c;
g = e / q;
d = f * g + d;
d = d + d;
q = g + q;
g = t;
t = s + t;
nn = nn + nn;
mm = mm + mm;
if bo {
if z < 0.0 {
mm += k;
}
k = if r < 0.0 { -1 } else { 1 };
h = e / (u * u + v * v);
u = u * (1.0 + h);
v = v * (1.0 - h);
} else {
r = u / v;
h = z * r;
z = h * z;
hh = e / v;
de = de / u;
ye = ye * (h + 1.0 / h) + de * (1.0 + r);
de = de * (u - hh);
bk = ye.abs() < 1.0;
}
if (g - s).abs() > C::ca() * g {
if bo {
g = (1.0 / r - r) * 0.5;
hh = u + v * g;
h = g * u - v;
z = r * h;
r = hh / h;
} else {
u = u + e / u;
v = v + hh;
}
s = e.sqrt();
s = s + s;
e = s * t;
l *= 2;
if y < 0.0 {
l += 1;
}
continue;
}
if y < 0.0 {
l += 1;
}
e = (t / y).atan() + pi!() * T::from(l).unwrap();
e = e * (c * t + d) / (t * (t + q));
if bo {
h = v / (t + u);
z = 1.0 - r * h;
h = r + h;
s = (h / z).atan() + T::from(mm).unwrap() * pi!();
} else {
s = if bk {
ye.asinh()
} else {
z.ln() + T::from(mm).unwrap() * ln_2!()
};
s = s * 0.5;
}
e = (e + fa.sqrt() * s) / T::from(nn).unwrap();
let ans = x.signum() * e;
if ans.is_finite() {
return Ok(ans);
}
check!(@nan, el3, [x, kc, p]);
break;
}
let phi = x.atan();
let ans = ellippiinc(phi, n, m);
if ans.is_ok() {
return ans;
}
Err("el3: Failed to converge.")
}
const MAX_ND: usize = 50;
#[cfg(not(feature = "test_force_fail"))]
#[cfg(test)]
mod tests {
use super::*;
use crate::{assert_close, compare_test_data_wolfram};
#[test]
fn test_el3() {
compare_test_data_wolfram!("el3_data.csv", el3, 3, 3e-12);
compare_test_data_wolfram!("el3_pv.csv", el3, 3, 5e-15);
}
#[test]
fn test_el3_references() {
fn test_reference(x: f64, kc: f64, p: f64, expected: f64) {
assert_close!(expected, el3(x, kc, p).unwrap(), 2.0 * f64::EPSILON);
}
test_reference(1.3, 0.11, 4.21, 6.6220785847015254e-1);
test_reference(1.3, 0.11, 0.82, 1.1307046442074609);
test_reference(1.3, 0.92, 0.71, 1.0058286266977115);
test_reference(1.3, 0.92, 0.23, 1.1884070823345123);
test_reference(1.3, 0.12, -0.11, 1.7259650355348878);
test_reference(1.3, 0.12, -2.11, 2.4416814520721179e-1);
test_reference(1.3, 0.40, 0.1600001, 1.4004165258366944);
test_reference(1.3, 1.0e-10, 0.82, 1.1341505395282723);
test_reference(1.3e-10, 1.0e-10, 1.0e-10, 1.3e-10);
test_reference(1.6, 1.90, 9.81, 3.8572324379967252e-1);
test_reference(1.6, 1.90, 1.22, 7.6656179311956402e-1);
test_reference(1.6, 1.90, 0.87, 8.3210591112618096e-1);
test_reference(1.6, 1.90, 0.21, 1.0521272221906806);
test_reference(1.6, 1.50, 2.24999, 7.0057431688357934e-1);
test_reference(1.6, 1e10, 1.20, 2.3734774669772208e-9);
test_reference(-1.6, 1e10, 1.20, -2.3734774669772208e-9);
test_reference(1.0, 0.31, 9.90e-2, 1.0903577921777398);
test_reference(1.6, 1.90, -0.21, 1.473043981995436);
test_reference(1.6, 1.90, -4.30, 0.2547695139719361);
assert_close!(0.3950170978760504, el3(1.6, 1.01e1, -1.0e-5).unwrap(), 1e-9);
}
#[test]
fn test_el3_special_cases() {
use crate::cel;
use std::f64::{INFINITY, NAN};
assert_eq!(el3(0.0, 0.5, 0.5).unwrap(), 0.0);
assert_eq!(el3(0.5, 0.0, 0.5), Err("el3: kc must not be zero."));
let complete_el3 = el3(INFINITY, 0.5, 0.5).unwrap();
assert_close!(complete_el3, cel(0.5, 0.5, 1.0, 1.0).unwrap(), 1e-15);
assert_close!(complete_el3, ellippi(0.5, 0.75).unwrap(), 1e-15);
assert_eq!(
el3(4.0, 1.0, 0.5).unwrap(),
(0.5.sqrt() * 4.0).atan() / 0.5.sqrt()
);
assert_close!(0.5225504573804798, el3(0.5, 1.0, -0.5).unwrap(), 1e-15);
assert_eq!(el3(1.0, 0.5, -1.0), Err("el3: 1 + px² cannot be zero."));
assert_eq!(el3(NAN, 0.5, 0.5), Err("el3: Arguments cannot be NAN."));
assert_eq!(el3(0.5, NAN, 0.5), Err("el3: Arguments cannot be NAN."));
assert_eq!(el3(0.5, 0.5, NAN), Err("el3: Arguments cannot be NAN."));
}
#[test]
fn test_small_nonzero_characteristic() {
for x in [1e6, -1e6] {
assert_close!(
el3(x, 0.5, 1e-11).unwrap(),
x.signum() * 799749.3224180657,
3e-14
);
}
assert_close!(el3(1.0, 0.5, 0.0).unwrap(), 1.1006047874101814, 3e-14);
}
#[test]
fn test_elementary_real_values() {
for x in [1.0, -1.0, 2.0, -2.0] {
assert_eq!(el3(x, 1.0, 0.0).unwrap(), x);
}
for x in [2.0, -2.0] {
assert_close!(
el3(x, 1.0, -1.0).unwrap(),
x.signum() * 0.5493061443340548,
2e-15
);
}
assert!(el3(1.0, 1.0, -1.0).is_err());
assert_close!(el3(1.0, 1.0, 1e-20).unwrap(), 1.0, 1e-15);
}
}
#[cfg(feature = "test_force_fail")]
crate::test_force_unreachable! {
use crate::bulirsch::constants::DefaultPrecision;
assert_eq!(el3_with_const::<f64, DefaultPrecision>(0.5, 0.5, 0.5), Err("el3: Failed to converge."));
}